@skspwork/config-doc 2.0.4 → 2.0.6
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 -3
- package/packages/web/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/packages/web/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/packages/web/.next/standalone/.next/server/app/_not-found.rsc +2 -2
- package/packages/web/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/packages/web/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
- package/packages/web/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/packages/web/.next/standalone/.next/server/app/api/config/save/route.js +1 -1
- package/packages/web/.next/standalone/.next/server/app/api/config/save/route.js.nft.json +1 -1
- package/packages/web/.next/standalone/.next/server/app/api/export/route.js +3 -3
- package/packages/web/.next/standalone/.next/server/app/api/export/route.js.nft.json +1 -1
- package/packages/web/.next/standalone/.next/server/app/index.html +1 -1
- package/packages/web/.next/standalone/.next/server/app/index.rsc +3 -3
- package/packages/web/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/packages/web/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
- package/packages/web/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
- package/packages/web/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/packages/web/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/packages/web/.next/standalone/.next/server/chunks/[root-of-the-server]__40e87302._.js +3 -0
- package/packages/web/.next/standalone/.next/server/chunks/[root-of-the-server]__93da9fce._.js +1 -1
- package/packages/web/.next/standalone/.next/server/chunks/[root-of-the-server]__c9655ac8._.js +3 -0
- package/packages/web/.next/standalone/.next/server/chunks/[root-of-the-server]__e19366f6._.js +1 -1
- package/packages/web/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_d09de205.js +345 -27
- package/packages/web/.next/standalone/.next/server/chunks/ssr/app_page_tsx_55b2e5ee._.js +1 -1
- package/packages/web/.next/standalone/.next/server/pages/404.html +1 -1
- package/packages/web/.next/standalone/.next/static/chunks/83ebf4df338dae62.js +1 -0
- package/packages/web/.next/standalone/.next/static/chunks/862e384b52cfebf3.css +3 -0
- package/packages/web/.next/standalone/app/api/config/metadata/route.ts +5 -3
- package/packages/web/.next/standalone/playwright-report/index.html +1 -1
- package/packages/web/.next/static/chunks/83ebf4df338dae62.js +1 -0
- package/packages/web/.next/static/chunks/862e384b52cfebf3.css +3 -0
- package/packages/web/.next/standalone/.next/server/chunks/[root-of-the-server]__1a68b1f3._.js +0 -3
- package/packages/web/.next/standalone/.next/server/chunks/[root-of-the-server]__2c94dfea._.js +0 -3
- package/packages/web/.next/standalone/.next/static/chunks/9726c2cde77e0916.js +0 -1
- package/packages/web/.next/standalone/.next/static/chunks/cd878566fda12635.css +0 -3
- package/packages/web/.next/standalone/app/api/config/load/route.ts +0 -57
- package/packages/web/.next/standalone/app/api/config/save/route.ts +0 -73
- package/packages/web/.next/standalone/app/api/export/route.ts +0 -75
- package/packages/web/.next/standalone/app/api/export/settings/route.ts +0 -144
- package/packages/web/.next/standalone/app/api/files/browse/route.ts +0 -46
- package/packages/web/.next/standalone/app/api/project/route.ts +0 -41
- package/packages/web/.next/standalone/app/globals.css +0 -26
- package/packages/web/.next/standalone/app/icon.svg +0 -41
- package/packages/web/.next/standalone/app/layout.tsx +0 -34
- package/packages/web/.next/standalone/app/page.tsx +0 -135
- package/packages/web/.next/standalone/components/ConfigFileTabs.tsx +0 -188
- package/packages/web/.next/standalone/components/ConfigTree.tsx +0 -176
- package/packages/web/.next/standalone/components/EditableList.tsx +0 -337
- package/packages/web/.next/standalone/components/ExportDialog.tsx +0 -234
- package/packages/web/.next/standalone/components/FieldsEditor.tsx +0 -92
- package/packages/web/.next/standalone/components/FileBrowser.tsx +0 -290
- package/packages/web/.next/standalone/components/Header.tsx +0 -37
- package/packages/web/.next/standalone/components/PropertyEditor.tsx +0 -102
- package/packages/web/.next/standalone/components/TagEditor.tsx +0 -86
- package/packages/web/.next/standalone/components/Toast.tsx +0 -91
- package/packages/web/.next/standalone/eslint.config.mjs +0 -18
- package/packages/web/.next/standalone/hooks/useConfigManager.ts +0 -653
- package/packages/web/.next/standalone/lib/configManagerUtils.ts +0 -84
- package/packages/web/.next/standalone/lib/configParser.ts +0 -155
- package/packages/web/.next/standalone/lib/fileSystem.ts +0 -186
- package/packages/web/.next/standalone/lib/getRootPath.ts +0 -45
- package/packages/web/.next/standalone/lib/htmlGenerator.ts +0 -865
- package/packages/web/.next/standalone/lib/jsonUtils.ts +0 -26
- package/packages/web/.next/standalone/lib/markdownGenerator.ts +0 -110
- package/packages/web/.next/standalone/lib/markdownTableGenerator.ts +0 -103
- package/packages/web/.next/standalone/lib/storage.ts +0 -104
- package/packages/web/.next/standalone/lib/utils.ts +0 -89
- package/packages/web/.next/standalone/next.config.ts +0 -10
- package/packages/web/.next/standalone/package-lock.json +0 -8216
- package/packages/web/.next/standalone/playwright.config.ts +0 -27
- package/packages/web/.next/standalone/postcss.config.mjs +0 -7
- package/packages/web/.next/standalone/test-results/.last-run.json +0 -4
- package/packages/web/.next/standalone/tsconfig.json +0 -34
- package/packages/web/.next/standalone/tsconfig.tsbuildinfo +0 -1
- package/packages/web/.next/standalone/types/index.ts +0 -74
- package/packages/web/.next/standalone/vitest.config.ts +0 -14
- package/packages/web/.next/static/chunks/9726c2cde77e0916.js +0 -1
- package/packages/web/.next/static/chunks/cd878566fda12635.css +0 -3
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { useState, useMemo } from 'react';
|
|
4
|
-
import { PencilIcon, PlusIcon, XIcon, SaveIcon, GripVerticalIcon } from 'lucide-react';
|
|
5
|
-
|
|
6
|
-
export interface EditingItem {
|
|
7
|
-
originalName: string;
|
|
8
|
-
newName: string;
|
|
9
|
-
isNew: boolean;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface EditableListWrapperProps {
|
|
13
|
-
/** セクションのラベル */
|
|
14
|
-
label: string;
|
|
15
|
-
/** 現在のアイテム一覧 */
|
|
16
|
-
items: string[];
|
|
17
|
-
/** アイテム一覧が変更されたときのコールバック */
|
|
18
|
-
onItemsChange: (items: string[]) => void;
|
|
19
|
-
/** 編集ボタンのtitle属性 */
|
|
20
|
-
editButtonTitle: string;
|
|
21
|
-
/** 編集モードの説明テキスト */
|
|
22
|
-
editModeDescription: string;
|
|
23
|
-
/** 入力欄のプレースホルダー */
|
|
24
|
-
inputPlaceholder: string;
|
|
25
|
-
/** 新規追加入力欄のプレースホルダー */
|
|
26
|
-
newItemPlaceholder: string;
|
|
27
|
-
/** 削除ボタンのtitle属性 */
|
|
28
|
-
deleteButtonTitle: string;
|
|
29
|
-
/** 追加ボタンのtitle属性 */
|
|
30
|
-
addButtonTitle: string;
|
|
31
|
-
/** 重複エラーメッセージ */
|
|
32
|
-
duplicateErrorMessage: string;
|
|
33
|
-
/** 名前変更時に呼ばれるコールバック(旧名 -> 新名のマップ) */
|
|
34
|
-
onRename?: (renamedMap: Record<string, string>) => void;
|
|
35
|
-
/** 通常モードの表示内容 */
|
|
36
|
-
children: React.ReactNode;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* 編集可能なリストを管理する共通コンポーネント
|
|
41
|
-
* タグやフィールドの編集UIで使用
|
|
42
|
-
*/
|
|
43
|
-
export function EditableListWrapper({
|
|
44
|
-
label,
|
|
45
|
-
items,
|
|
46
|
-
onItemsChange,
|
|
47
|
-
editButtonTitle,
|
|
48
|
-
editModeDescription,
|
|
49
|
-
inputPlaceholder,
|
|
50
|
-
newItemPlaceholder,
|
|
51
|
-
deleteButtonTitle,
|
|
52
|
-
addButtonTitle,
|
|
53
|
-
duplicateErrorMessage,
|
|
54
|
-
onRename,
|
|
55
|
-
children
|
|
56
|
-
}: EditableListWrapperProps) {
|
|
57
|
-
const [isEditMode, setIsEditMode] = useState(false);
|
|
58
|
-
const [editingItems, setEditingItems] = useState<EditingItem[]>([]);
|
|
59
|
-
const [newItemName, setNewItemName] = useState('');
|
|
60
|
-
|
|
61
|
-
// ドラッグ&ドロップ用の状態
|
|
62
|
-
const [dragIndex, setDragIndex] = useState<number | null>(null);
|
|
63
|
-
const [dragOverIndex, setDragOverIndex] = useState<number | null>(null);
|
|
64
|
-
|
|
65
|
-
// ドラッグ中のプレビュー用に並び替えた配列を計算
|
|
66
|
-
const displayItems = useMemo(() => {
|
|
67
|
-
if (dragIndex === null || dragOverIndex === null || dragIndex === dragOverIndex) {
|
|
68
|
-
return editingItems.map((item, index) => ({ item, originalIndex: index }));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ドラッグ中は見た目上の並び替えを表示
|
|
72
|
-
const result = editingItems.map((item, index) => ({ item, originalIndex: index }));
|
|
73
|
-
const [draggedItem] = result.splice(dragIndex, 1);
|
|
74
|
-
result.splice(dragOverIndex, 0, draggedItem);
|
|
75
|
-
return result;
|
|
76
|
-
}, [editingItems, dragIndex, dragOverIndex]);
|
|
77
|
-
|
|
78
|
-
// 編集モードに入る
|
|
79
|
-
const enterEditMode = () => {
|
|
80
|
-
const itemList = items.map(name => ({
|
|
81
|
-
originalName: name,
|
|
82
|
-
newName: name,
|
|
83
|
-
isNew: false
|
|
84
|
-
}));
|
|
85
|
-
setEditingItems(itemList);
|
|
86
|
-
setIsEditMode(true);
|
|
87
|
-
setNewItemName('');
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
// 編集モードをキャンセル
|
|
91
|
-
const cancelEditMode = () => {
|
|
92
|
-
setIsEditMode(false);
|
|
93
|
-
setEditingItems([]);
|
|
94
|
-
setNewItemName('');
|
|
95
|
-
setDragIndex(null);
|
|
96
|
-
setDragOverIndex(null);
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
// アイテムを追加(編集モード内)
|
|
100
|
-
const handleAddItem = () => {
|
|
101
|
-
const trimmedName = newItemName.trim();
|
|
102
|
-
if (trimmedName && !editingItems.some(item => item.newName === trimmedName)) {
|
|
103
|
-
setEditingItems([...editingItems, {
|
|
104
|
-
originalName: '',
|
|
105
|
-
newName: trimmedName,
|
|
106
|
-
isNew: true
|
|
107
|
-
}]);
|
|
108
|
-
setNewItemName('');
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
// アイテムを削除(編集モード内)
|
|
113
|
-
const handleRemoveItem = (originalIndex: number) => {
|
|
114
|
-
setEditingItems(editingItems.filter((_, i) => i !== originalIndex));
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
// アイテム名を変更(編集モード内)
|
|
118
|
-
const handleNameChange = (originalIndex: number, newName: string) => {
|
|
119
|
-
setEditingItems(editingItems.map((item, i) =>
|
|
120
|
-
i === originalIndex ? { ...item, newName } : item
|
|
121
|
-
));
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
// ドラッグ開始
|
|
125
|
-
const handleDragStart = (originalIndex: number) => {
|
|
126
|
-
setDragIndex(originalIndex);
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
// ドラッグオーバー(表示上のインデックスで処理)
|
|
130
|
-
const handleDragOver = (e: React.DragEvent, displayIndex: number) => {
|
|
131
|
-
e.preventDefault();
|
|
132
|
-
if (dragIndex === null) return;
|
|
133
|
-
|
|
134
|
-
// 表示上のインデックスをそのまま使用
|
|
135
|
-
if (dragOverIndex !== displayIndex) {
|
|
136
|
-
setDragOverIndex(displayIndex);
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
// ドロップ時にデータを確定
|
|
141
|
-
const handleDrop = () => {
|
|
142
|
-
if (dragIndex === null || dragOverIndex === null || dragIndex === dragOverIndex) {
|
|
143
|
-
setDragIndex(null);
|
|
144
|
-
setDragOverIndex(null);
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// 配列の並び替えを確定
|
|
149
|
-
const newItems = [...editingItems];
|
|
150
|
-
const [draggedItem] = newItems.splice(dragIndex, 1);
|
|
151
|
-
newItems.splice(dragOverIndex, 0, draggedItem);
|
|
152
|
-
setEditingItems(newItems);
|
|
153
|
-
|
|
154
|
-
setDragIndex(null);
|
|
155
|
-
setDragOverIndex(null);
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
// ドラッグ終了(キャンセル時など)
|
|
159
|
-
const handleDragEnd = () => {
|
|
160
|
-
setDragIndex(null);
|
|
161
|
-
setDragOverIndex(null);
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
// 保存処理
|
|
165
|
-
const handleSave = () => {
|
|
166
|
-
const newItems: string[] = [];
|
|
167
|
-
const renamedMap: Record<string, string> = {};
|
|
168
|
-
|
|
169
|
-
for (const item of editingItems) {
|
|
170
|
-
const trimmedName = item.newName.trim();
|
|
171
|
-
if (!trimmedName) continue;
|
|
172
|
-
|
|
173
|
-
newItems.push(trimmedName);
|
|
174
|
-
|
|
175
|
-
// 名前変更の追跡
|
|
176
|
-
if (!item.isNew && item.originalName !== trimmedName) {
|
|
177
|
-
renamedMap[item.originalName] = trimmedName;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// 入力中のアイテムがあれば追加
|
|
182
|
-
const pendingName = newItemName.trim();
|
|
183
|
-
if (pendingName && !newItems.includes(pendingName)) {
|
|
184
|
-
newItems.push(pendingName);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// 名前変更コールバック
|
|
188
|
-
if (onRename && Object.keys(renamedMap).length > 0) {
|
|
189
|
-
onRename(renamedMap);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
onItemsChange(newItems);
|
|
193
|
-
setIsEditMode(false);
|
|
194
|
-
setEditingItems([]);
|
|
195
|
-
setNewItemName('');
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
199
|
-
if (e.key === 'Enter') {
|
|
200
|
-
e.preventDefault();
|
|
201
|
-
handleAddItem();
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
// バリデーション: 重複チェック
|
|
206
|
-
const hasDuplicateNames = () => {
|
|
207
|
-
const names = editingItems.map(item => item.newName.trim()).filter(Boolean);
|
|
208
|
-
return new Set(names).size !== names.length;
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
// バリデーション: 空の名前チェック
|
|
212
|
-
const hasEmptyNames = () => {
|
|
213
|
-
return editingItems.some(item => !item.newName.trim());
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
const canSave = !hasDuplicateNames() && !hasEmptyNames();
|
|
217
|
-
|
|
218
|
-
return (
|
|
219
|
-
<div>
|
|
220
|
-
<div className="flex items-center justify-between mb-2">
|
|
221
|
-
<label className="block text-sm font-semibold text-gray-700">
|
|
222
|
-
{label}
|
|
223
|
-
</label>
|
|
224
|
-
{!isEditMode && (
|
|
225
|
-
<button
|
|
226
|
-
onClick={enterEditMode}
|
|
227
|
-
className="text-gray-500 hover:text-blue-600 transition-colors p-1"
|
|
228
|
-
title={editButtonTitle}
|
|
229
|
-
>
|
|
230
|
-
<PencilIcon className="w-4 h-4" />
|
|
231
|
-
</button>
|
|
232
|
-
)}
|
|
233
|
-
</div>
|
|
234
|
-
|
|
235
|
-
{isEditMode ? (
|
|
236
|
-
<div className="space-y-3 p-4 bg-blue-50 rounded-lg border-2 border-blue-200">
|
|
237
|
-
<div className="text-xs font-medium text-blue-700 mb-2">
|
|
238
|
-
{editModeDescription}(ドラッグで並び替え可能)
|
|
239
|
-
</div>
|
|
240
|
-
|
|
241
|
-
{/* 既存アイテムの編集 */}
|
|
242
|
-
<div className="space-y-2">
|
|
243
|
-
{displayItems.map(({ item, originalIndex }, displayIndex) => {
|
|
244
|
-
const isDragging = dragIndex === originalIndex;
|
|
245
|
-
|
|
246
|
-
return (
|
|
247
|
-
<div
|
|
248
|
-
key={originalIndex}
|
|
249
|
-
draggable
|
|
250
|
-
onDragStart={() => handleDragStart(originalIndex)}
|
|
251
|
-
onDragOver={(e) => handleDragOver(e, displayIndex)}
|
|
252
|
-
onDrop={handleDrop}
|
|
253
|
-
onDragEnd={handleDragEnd}
|
|
254
|
-
className={`flex items-center gap-2 transition-all ${
|
|
255
|
-
isDragging ? 'opacity-50' : ''
|
|
256
|
-
}`}
|
|
257
|
-
>
|
|
258
|
-
<div
|
|
259
|
-
className="cursor-grab text-gray-400 hover:text-gray-600 p-1"
|
|
260
|
-
title="ドラッグして並び替え"
|
|
261
|
-
>
|
|
262
|
-
<GripVerticalIcon className="w-4 h-4" />
|
|
263
|
-
</div>
|
|
264
|
-
<input
|
|
265
|
-
type="text"
|
|
266
|
-
value={item.newName}
|
|
267
|
-
onChange={(e) => handleNameChange(originalIndex, e.target.value)}
|
|
268
|
-
placeholder={inputPlaceholder}
|
|
269
|
-
className={`flex-1 px-3 py-2 border-2 rounded-lg text-sm focus:ring-2 transition-all duration-200 ${
|
|
270
|
-
!item.newName.trim()
|
|
271
|
-
? 'border-red-300 focus:border-red-400 focus:ring-red-200'
|
|
272
|
-
: 'border-gray-200 focus:border-blue-400 focus:ring-blue-200'
|
|
273
|
-
}`}
|
|
274
|
-
/>
|
|
275
|
-
<button
|
|
276
|
-
onClick={() => handleRemoveItem(originalIndex)}
|
|
277
|
-
className="text-gray-400 hover:text-red-600 transition-colors p-2"
|
|
278
|
-
title={deleteButtonTitle}
|
|
279
|
-
>
|
|
280
|
-
<XIcon className="w-4 h-4" />
|
|
281
|
-
</button>
|
|
282
|
-
</div>
|
|
283
|
-
);
|
|
284
|
-
})}
|
|
285
|
-
</div>
|
|
286
|
-
|
|
287
|
-
{/* 新規アイテム追加 */}
|
|
288
|
-
<div className="flex items-center gap-2 pt-2 border-t border-blue-200">
|
|
289
|
-
<input
|
|
290
|
-
type="text"
|
|
291
|
-
value={newItemName}
|
|
292
|
-
onChange={(e) => setNewItemName(e.target.value)}
|
|
293
|
-
onKeyDown={handleKeyDown}
|
|
294
|
-
placeholder={newItemPlaceholder}
|
|
295
|
-
className="flex-1 px-3 py-2 border-2 border-gray-200 rounded-lg text-sm focus:border-blue-400 focus:ring-2 focus:ring-blue-200 transition-all duration-200"
|
|
296
|
-
/>
|
|
297
|
-
<button
|
|
298
|
-
onClick={handleAddItem}
|
|
299
|
-
disabled={!newItemName.trim() || editingItems.some(item => item.newName.trim() === newItemName.trim())}
|
|
300
|
-
className="px-3 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors disabled:bg-gray-300 disabled:cursor-not-allowed"
|
|
301
|
-
title={addButtonTitle}
|
|
302
|
-
>
|
|
303
|
-
<PlusIcon className="w-4 h-4" />
|
|
304
|
-
</button>
|
|
305
|
-
</div>
|
|
306
|
-
|
|
307
|
-
{/* エラーメッセージ */}
|
|
308
|
-
{hasDuplicateNames() && (
|
|
309
|
-
<div className="text-xs text-red-600 mt-2">
|
|
310
|
-
{duplicateErrorMessage}
|
|
311
|
-
</div>
|
|
312
|
-
)}
|
|
313
|
-
|
|
314
|
-
{/* 保存・キャンセルボタン */}
|
|
315
|
-
<div className="flex items-center gap-2 pt-3 border-t border-blue-200">
|
|
316
|
-
<button
|
|
317
|
-
onClick={handleSave}
|
|
318
|
-
disabled={!canSave}
|
|
319
|
-
className="flex-1 flex items-center justify-center gap-2 px-4 py-2 bg-green-500 text-white rounded-lg hover:bg-green-600 transition-colors disabled:bg-gray-300 disabled:cursor-not-allowed text-sm font-medium"
|
|
320
|
-
>
|
|
321
|
-
<SaveIcon className="w-4 h-4" />
|
|
322
|
-
保存
|
|
323
|
-
</button>
|
|
324
|
-
<button
|
|
325
|
-
onClick={cancelEditMode}
|
|
326
|
-
className="flex-1 px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-colors text-sm font-medium"
|
|
327
|
-
>
|
|
328
|
-
キャンセル
|
|
329
|
-
</button>
|
|
330
|
-
</div>
|
|
331
|
-
</div>
|
|
332
|
-
) : (
|
|
333
|
-
children
|
|
334
|
-
)}
|
|
335
|
-
</div>
|
|
336
|
-
);
|
|
337
|
-
}
|
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useMemo } from 'react';
|
|
2
|
-
import { ExportSettings, ExportFormat } from '@/types';
|
|
3
|
-
import { XIcon, DownloadIcon, FolderIcon } from 'lucide-react';
|
|
4
|
-
import { FileBrowser } from './FileBrowser';
|
|
5
|
-
|
|
6
|
-
interface ExportDialogProps {
|
|
7
|
-
isOpen: boolean;
|
|
8
|
-
onClose: () => void;
|
|
9
|
-
onExport: (settings: ExportSettings) => void;
|
|
10
|
-
currentSettings?: ExportSettings;
|
|
11
|
-
rootPath?: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
const DEFAULT_SETTINGS: ExportSettings = {
|
|
15
|
-
format: 'html',
|
|
16
|
-
autoExport: true,
|
|
17
|
-
fileName: 'config-doc',
|
|
18
|
-
outputDir: '.config_doc/output'
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export function ExportDialog({ isOpen, onClose, onExport, currentSettings, rootPath = '.' }: ExportDialogProps) {
|
|
22
|
-
const [settings, setSettings] = useState<ExportSettings>(currentSettings || DEFAULT_SETTINGS);
|
|
23
|
-
const [isExporting, setIsExporting] = useState(false);
|
|
24
|
-
const [isFolderBrowserOpen, setIsFolderBrowserOpen] = useState(false);
|
|
25
|
-
|
|
26
|
-
useEffect(() => {
|
|
27
|
-
if (currentSettings) {
|
|
28
|
-
setSettings(currentSettings);
|
|
29
|
-
}
|
|
30
|
-
}, [currentSettings]);
|
|
31
|
-
|
|
32
|
-
// フォーマットに応じて出力先パスを決定
|
|
33
|
-
const absoluteOutputPath = useMemo(() => {
|
|
34
|
-
const normalized = rootPath.replace(/\//g, '\\');
|
|
35
|
-
const outputDir = settings.outputDir?.trim() || '';
|
|
36
|
-
const fileName = settings.fileName || 'config-doc';
|
|
37
|
-
const extension = (settings.format === 'markdown' || settings.format === 'markdown-table') ? 'md' : 'html';
|
|
38
|
-
if (outputDir) {
|
|
39
|
-
const normalizedOutputDir = outputDir.replace(/\//g, '\\');
|
|
40
|
-
return `${normalized}\\${normalizedOutputDir}\\${fileName}.${extension}`;
|
|
41
|
-
}
|
|
42
|
-
return `${normalized}\\${fileName}.${extension}`;
|
|
43
|
-
}, [settings.format, settings.fileName, settings.outputDir, rootPath]);
|
|
44
|
-
|
|
45
|
-
if (!isOpen) return null;
|
|
46
|
-
|
|
47
|
-
const handleExport = async () => {
|
|
48
|
-
setIsExporting(true);
|
|
49
|
-
try {
|
|
50
|
-
await onExport(settings);
|
|
51
|
-
onClose();
|
|
52
|
-
} catch (error) {
|
|
53
|
-
console.error('Export failed:', error);
|
|
54
|
-
} finally {
|
|
55
|
-
setIsExporting(false);
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
return (
|
|
60
|
-
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
61
|
-
<div className="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
|
|
62
|
-
{/* ヘッダー */}
|
|
63
|
-
<div className="flex items-center justify-between p-6 border-b">
|
|
64
|
-
<h2 className="text-xl font-semibold text-gray-800">エクスポート設定</h2>
|
|
65
|
-
<button
|
|
66
|
-
onClick={onClose}
|
|
67
|
-
className="text-gray-400 hover:text-gray-600 transition-colors"
|
|
68
|
-
>
|
|
69
|
-
<XIcon className="w-5 h-5" />
|
|
70
|
-
</button>
|
|
71
|
-
</div>
|
|
72
|
-
|
|
73
|
-
{/* コンテンツ */}
|
|
74
|
-
<div className="p-6 space-y-6">
|
|
75
|
-
|
|
76
|
-
{/* 出力先フォルダ */}
|
|
77
|
-
<div>
|
|
78
|
-
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
79
|
-
出力先フォルダ
|
|
80
|
-
</label>
|
|
81
|
-
<div className="flex gap-2">
|
|
82
|
-
<input
|
|
83
|
-
type="text"
|
|
84
|
-
value={settings.outputDir || ''}
|
|
85
|
-
onChange={(e) => setSettings({ ...settings, outputDir: e.target.value })}
|
|
86
|
-
className="flex-1 px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
87
|
-
placeholder="(空欄でプロジェクトルート)"
|
|
88
|
-
/>
|
|
89
|
-
<button
|
|
90
|
-
type="button"
|
|
91
|
-
onClick={() => setIsFolderBrowserOpen(true)}
|
|
92
|
-
className="px-3 py-2 border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
93
|
-
title="フォルダを選択"
|
|
94
|
-
>
|
|
95
|
-
<FolderIcon className="w-5 h-5 text-gray-500" />
|
|
96
|
-
</button>
|
|
97
|
-
</div>
|
|
98
|
-
<p className="mt-1 text-xs text-gray-500">
|
|
99
|
-
相対パスを入力またはフォルダを選択(空欄でプロジェクトルート、チーム共有設定)
|
|
100
|
-
</p>
|
|
101
|
-
</div>
|
|
102
|
-
|
|
103
|
-
{/* ファイル名 */}
|
|
104
|
-
<div>
|
|
105
|
-
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
106
|
-
ファイル名
|
|
107
|
-
</label>
|
|
108
|
-
<input
|
|
109
|
-
type="text"
|
|
110
|
-
value={settings.fileName ?? ''}
|
|
111
|
-
onChange={(e) => setSettings({ ...settings, fileName: e.target.value })}
|
|
112
|
-
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
113
|
-
placeholder="config-doc"
|
|
114
|
-
/>
|
|
115
|
-
<p className="mt-1 text-xs text-gray-500">
|
|
116
|
-
拡張子なしのファイル名を指定します(空欄でconfig-doc、チーム共有設定)
|
|
117
|
-
</p>
|
|
118
|
-
</div>
|
|
119
|
-
|
|
120
|
-
{/* 出力形式 */}
|
|
121
|
-
<div>
|
|
122
|
-
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
123
|
-
出力形式
|
|
124
|
-
</label>
|
|
125
|
-
<select
|
|
126
|
-
value={settings.format}
|
|
127
|
-
onChange={(e) => setSettings({ ...settings, format: e.target.value as ExportFormat })}
|
|
128
|
-
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
129
|
-
>
|
|
130
|
-
<option value="html">HTML</option>
|
|
131
|
-
<option value="markdown">Markdown</option>
|
|
132
|
-
<option value="markdown-table">Markdown (テーブル形式)</option>
|
|
133
|
-
</select>
|
|
134
|
-
<p className="mt-1 text-xs text-gray-500">
|
|
135
|
-
{settings.format === 'html'
|
|
136
|
-
? 'スタイル付きのHTMLファイルとして出力します'
|
|
137
|
-
: settings.format === 'markdown-table'
|
|
138
|
-
? 'Markdownテーブル形式で出力します(プロパティ名、説明、値、備考)'
|
|
139
|
-
: 'テキストベースのMarkdownファイルとして出力します'}
|
|
140
|
-
</p>
|
|
141
|
-
</div>
|
|
142
|
-
|
|
143
|
-
{/* 出力先パス */}
|
|
144
|
-
<div>
|
|
145
|
-
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
146
|
-
出力先パス
|
|
147
|
-
</label>
|
|
148
|
-
<div className="px-3 py-2 bg-gray-50 border border-gray-300 rounded-md text-sm text-gray-700 font-mono break-all">
|
|
149
|
-
{absoluteOutputPath}
|
|
150
|
-
</div>
|
|
151
|
-
</div>
|
|
152
|
-
|
|
153
|
-
{/* 自動エクスポート */}
|
|
154
|
-
<div className="flex items-start">
|
|
155
|
-
<div className="flex items-center h-5">
|
|
156
|
-
<input
|
|
157
|
-
id="auto-export"
|
|
158
|
-
type="checkbox"
|
|
159
|
-
checked={settings.autoExport}
|
|
160
|
-
onChange={(e) => setSettings({ ...settings, autoExport: e.target.checked })}
|
|
161
|
-
className="w-4 h-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
|
162
|
-
/>
|
|
163
|
-
</div>
|
|
164
|
-
<div className="ml-3">
|
|
165
|
-
<label htmlFor="auto-export" className="text-sm font-medium text-gray-700 cursor-pointer">
|
|
166
|
-
保存時に自動エクスポート
|
|
167
|
-
</label>
|
|
168
|
-
<p className="text-xs text-gray-500 mt-1">
|
|
169
|
-
ドキュメントを保存したときに自動的にHTMLファイルを更新します
|
|
170
|
-
</p>
|
|
171
|
-
</div>
|
|
172
|
-
</div>
|
|
173
|
-
|
|
174
|
-
{/* 最終エクスポート日時 */}
|
|
175
|
-
{settings.lastExportedAt && (
|
|
176
|
-
<div className="pt-4 border-t">
|
|
177
|
-
<p className="text-xs text-gray-500">
|
|
178
|
-
最終エクスポート: {new Date(settings.lastExportedAt).toLocaleString('ja-JP')}
|
|
179
|
-
</p>
|
|
180
|
-
</div>
|
|
181
|
-
)}
|
|
182
|
-
</div>
|
|
183
|
-
|
|
184
|
-
{/* フッター */}
|
|
185
|
-
<div className="flex items-center justify-end gap-3 p-6 border-t bg-gray-50 rounded-b-lg">
|
|
186
|
-
<button
|
|
187
|
-
onClick={onClose}
|
|
188
|
-
className="px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
189
|
-
>
|
|
190
|
-
キャンセル
|
|
191
|
-
</button>
|
|
192
|
-
<button
|
|
193
|
-
onClick={handleExport}
|
|
194
|
-
disabled={isExporting}
|
|
195
|
-
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
196
|
-
>
|
|
197
|
-
<DownloadIcon className="w-4 h-4" />
|
|
198
|
-
{isExporting ? 'エクスポート中...' : 'エクスポート'}
|
|
199
|
-
</button>
|
|
200
|
-
</div>
|
|
201
|
-
</div>
|
|
202
|
-
|
|
203
|
-
{/* フォルダ選択ダイアログ */}
|
|
204
|
-
<FileBrowser
|
|
205
|
-
isOpen={isFolderBrowserOpen}
|
|
206
|
-
currentPath={rootPath}
|
|
207
|
-
onSelect={(paths) => {
|
|
208
|
-
if (paths.length > 0) {
|
|
209
|
-
// 絶対パスから相対パスに変換
|
|
210
|
-
const selectedPath = paths[0];
|
|
211
|
-
const normalizedRoot = rootPath.replace(/\\/g, '/');
|
|
212
|
-
const normalizedSelected = selectedPath.replace(/\\/g, '/');
|
|
213
|
-
let relativePath = normalizedSelected;
|
|
214
|
-
if (normalizedSelected.startsWith(normalizedRoot)) {
|
|
215
|
-
relativePath = normalizedSelected.slice(normalizedRoot.length);
|
|
216
|
-
if (relativePath.startsWith('/')) {
|
|
217
|
-
relativePath = relativePath.slice(1);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
// 空の場合は現在のディレクトリ
|
|
221
|
-
if (!relativePath) {
|
|
222
|
-
relativePath = '.';
|
|
223
|
-
}
|
|
224
|
-
setSettings({ ...settings, outputDir: relativePath });
|
|
225
|
-
}
|
|
226
|
-
setIsFolderBrowserOpen(false);
|
|
227
|
-
}}
|
|
228
|
-
onClose={() => setIsFolderBrowserOpen(false)}
|
|
229
|
-
folderSelectMode={true}
|
|
230
|
-
title="出力先フォルダを選択"
|
|
231
|
-
/>
|
|
232
|
-
</div>
|
|
233
|
-
);
|
|
234
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { EditableListWrapper } from './EditableList';
|
|
4
|
-
|
|
5
|
-
interface FieldsEditorProps {
|
|
6
|
-
fields: Record<string, string>;
|
|
7
|
-
projectFields: Record<string, string>;
|
|
8
|
-
onFieldsChange: (fields: Record<string, string>) => void;
|
|
9
|
-
onUpdateProjectFields?: (fields: Record<string, string>) => void;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function FieldsEditor({
|
|
13
|
-
fields,
|
|
14
|
-
projectFields,
|
|
15
|
-
onFieldsChange,
|
|
16
|
-
onUpdateProjectFields
|
|
17
|
-
}: FieldsEditorProps) {
|
|
18
|
-
// フィールド名の一覧
|
|
19
|
-
const fieldNames = Object.keys(projectFields);
|
|
20
|
-
|
|
21
|
-
// 名前変更時にフィールドの値も更新
|
|
22
|
-
const handleRename = (renamedMap: Record<string, string>) => {
|
|
23
|
-
const newFields: Record<string, string> = {};
|
|
24
|
-
for (const [oldName, value] of Object.entries(fields)) {
|
|
25
|
-
const newName = renamedMap[oldName] || oldName;
|
|
26
|
-
newFields[newName] = value;
|
|
27
|
-
}
|
|
28
|
-
onFieldsChange(newFields);
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// フィールド一覧が変更されたとき
|
|
32
|
-
const handleFieldNamesChange = (newNames: string[]) => {
|
|
33
|
-
// 新しいフィールド構造を構築
|
|
34
|
-
const newFields: Record<string, string> = {};
|
|
35
|
-
for (const name of newNames) {
|
|
36
|
-
newFields[name] = fields[name] || '';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// フィールドを更新(ローカル状態)
|
|
40
|
-
onFieldsChange(newFields);
|
|
41
|
-
|
|
42
|
-
// プロジェクト全体に適用
|
|
43
|
-
if (onUpdateProjectFields) {
|
|
44
|
-
const newProjectFields: Record<string, string> = {};
|
|
45
|
-
for (const name of newNames) {
|
|
46
|
-
newProjectFields[name] = '';
|
|
47
|
-
}
|
|
48
|
-
onUpdateProjectFields(newProjectFields);
|
|
49
|
-
}
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
// フィールド値の変更(通常モード)
|
|
53
|
-
const handleFieldValueChange = (label: string, value: string) => {
|
|
54
|
-
onFieldsChange({
|
|
55
|
-
...fields,
|
|
56
|
-
[label]: value
|
|
57
|
-
});
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<EditableListWrapper
|
|
62
|
-
label="フィールド"
|
|
63
|
-
items={fieldNames}
|
|
64
|
-
onItemsChange={handleFieldNamesChange}
|
|
65
|
-
editButtonTitle="フィールドを編集"
|
|
66
|
-
editModeDescription="フィールドの追加・削除・名前変更"
|
|
67
|
-
inputPlaceholder="フィールド名"
|
|
68
|
-
newItemPlaceholder="新しいフィールド名を入力"
|
|
69
|
-
deleteButtonTitle="フィールドを削除"
|
|
70
|
-
addButtonTitle="フィールドを追加"
|
|
71
|
-
duplicateErrorMessage="同じ名前のフィールドがあります"
|
|
72
|
-
onRename={handleRename}
|
|
73
|
-
>
|
|
74
|
-
{/* 通常モード(フィールド値の入力) */}
|
|
75
|
-
<div className="space-y-3">
|
|
76
|
-
{Object.entries(fields).map(([label, value]) => (
|
|
77
|
-
<div key={label}>
|
|
78
|
-
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
79
|
-
{label}
|
|
80
|
-
</label>
|
|
81
|
-
<textarea
|
|
82
|
-
value={value}
|
|
83
|
-
onChange={(e) => handleFieldValueChange(label, e.target.value)}
|
|
84
|
-
className="w-full border-2 border-gray-200 rounded-lg p-3 min-h-[80px] text-sm focus:border-blue-400 focus:ring-2 focus:ring-blue-200 transition-all duration-200 shadow-sm hover:shadow-md"
|
|
85
|
-
placeholder={`${label}を入力してください`}
|
|
86
|
-
/>
|
|
87
|
-
</div>
|
|
88
|
-
))}
|
|
89
|
-
</div>
|
|
90
|
-
</EditableListWrapper>
|
|
91
|
-
);
|
|
92
|
-
}
|