@skspwork/config-doc 2.0.3 → 2.0.5

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.
Files changed (79) hide show
  1. package/package.json +3 -2
  2. package/packages/web/.next/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  3. package/packages/web/.next/standalone/.next/server/app/_not-found.html +1 -1
  4. package/packages/web/.next/standalone/.next/server/app/_not-found.rsc +2 -2
  5. package/packages/web/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
  6. package/packages/web/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +2 -2
  7. package/packages/web/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  8. package/packages/web/.next/standalone/.next/server/app/api/config/save/route.js +1 -1
  9. package/packages/web/.next/standalone/.next/server/app/api/config/save/route.js.nft.json +1 -1
  10. package/packages/web/.next/standalone/.next/server/app/api/export/route.js +3 -3
  11. package/packages/web/.next/standalone/.next/server/app/api/export/route.js.nft.json +1 -1
  12. package/packages/web/.next/standalone/.next/server/app/index.html +1 -1
  13. package/packages/web/.next/standalone/.next/server/app/index.rsc +3 -3
  14. package/packages/web/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  15. package/packages/web/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +3 -3
  16. package/packages/web/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +2 -2
  17. package/packages/web/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  18. package/packages/web/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  19. package/packages/web/.next/standalone/.next/server/chunks/[root-of-the-server]__40e87302._.js +3 -0
  20. package/packages/web/.next/standalone/.next/server/chunks/[root-of-the-server]__93da9fce._.js +1 -1
  21. package/packages/web/.next/standalone/.next/server/chunks/[root-of-the-server]__c9655ac8._.js +3 -0
  22. package/packages/web/.next/standalone/.next/server/chunks/[root-of-the-server]__e19366f6._.js +1 -1
  23. package/packages/web/.next/standalone/.next/server/chunks/node_modules_next_dist_esm_build_templates_app-route_d09de205.js +368 -29
  24. package/packages/web/.next/standalone/.next/server/chunks/ssr/app_page_tsx_55b2e5ee._.js +1 -1
  25. package/packages/web/.next/standalone/.next/server/pages/404.html +1 -1
  26. package/packages/web/.next/standalone/.next/static/chunks/02de70e4c30afe2f.js +1 -0
  27. package/packages/web/.next/standalone/.next/static/chunks/862e384b52cfebf3.css +3 -0
  28. package/packages/web/.next/standalone/app/api/config/metadata/route.ts +5 -3
  29. package/packages/web/.next/standalone/package.json +2 -0
  30. package/packages/web/.next/standalone/playwright-report/index.html +1 -1
  31. package/packages/web/.next/static/chunks/02de70e4c30afe2f.js +1 -0
  32. package/packages/web/.next/static/chunks/862e384b52cfebf3.css +3 -0
  33. package/packages/web/package.json +2 -0
  34. package/packages/web/.next/standalone/.next/server/chunks/[root-of-the-server]__1a68b1f3._.js +0 -3
  35. package/packages/web/.next/standalone/.next/server/chunks/[root-of-the-server]__2c94dfea._.js +0 -3
  36. package/packages/web/.next/standalone/.next/static/chunks/4bbca8cd642026de.css +0 -3
  37. package/packages/web/.next/standalone/.next/static/chunks/54e2bd8f072e7d4e.js +0 -1
  38. package/packages/web/.next/standalone/app/api/config/load/route.ts +0 -57
  39. package/packages/web/.next/standalone/app/api/config/save/route.ts +0 -73
  40. package/packages/web/.next/standalone/app/api/export/route.ts +0 -75
  41. package/packages/web/.next/standalone/app/api/export/settings/route.ts +0 -144
  42. package/packages/web/.next/standalone/app/api/files/browse/route.ts +0 -46
  43. package/packages/web/.next/standalone/app/api/project/route.ts +0 -41
  44. package/packages/web/.next/standalone/app/globals.css +0 -26
  45. package/packages/web/.next/standalone/app/icon.svg +0 -41
  46. package/packages/web/.next/standalone/app/layout.tsx +0 -34
  47. package/packages/web/.next/standalone/app/page.tsx +0 -135
  48. package/packages/web/.next/standalone/components/ConfigFileTabs.tsx +0 -188
  49. package/packages/web/.next/standalone/components/ConfigTree.tsx +0 -176
  50. package/packages/web/.next/standalone/components/EditableList.tsx +0 -337
  51. package/packages/web/.next/standalone/components/ExportDialog.tsx +0 -234
  52. package/packages/web/.next/standalone/components/FieldsEditor.tsx +0 -92
  53. package/packages/web/.next/standalone/components/FileBrowser.tsx +0 -290
  54. package/packages/web/.next/standalone/components/Header.tsx +0 -37
  55. package/packages/web/.next/standalone/components/PropertyEditor.tsx +0 -102
  56. package/packages/web/.next/standalone/components/TagEditor.tsx +0 -86
  57. package/packages/web/.next/standalone/components/Toast.tsx +0 -91
  58. package/packages/web/.next/standalone/eslint.config.mjs +0 -18
  59. package/packages/web/.next/standalone/hooks/useConfigManager.ts +0 -633
  60. package/packages/web/.next/standalone/lib/configParser.ts +0 -155
  61. package/packages/web/.next/standalone/lib/fileSystem.ts +0 -186
  62. package/packages/web/.next/standalone/lib/getRootPath.ts +0 -45
  63. package/packages/web/.next/standalone/lib/htmlGenerator.ts +0 -839
  64. package/packages/web/.next/standalone/lib/jsonUtils.ts +0 -26
  65. package/packages/web/.next/standalone/lib/markdownGenerator.ts +0 -79
  66. package/packages/web/.next/standalone/lib/markdownTableGenerator.ts +0 -107
  67. package/packages/web/.next/standalone/lib/storage.ts +0 -104
  68. package/packages/web/.next/standalone/lib/utils.ts +0 -72
  69. package/packages/web/.next/standalone/next.config.ts +0 -10
  70. package/packages/web/.next/standalone/package-lock.json +0 -7977
  71. package/packages/web/.next/standalone/playwright.config.ts +0 -27
  72. package/packages/web/.next/standalone/postcss.config.mjs +0 -7
  73. package/packages/web/.next/standalone/test-results/.last-run.json +0 -4
  74. package/packages/web/.next/standalone/tsconfig.json +0 -34
  75. package/packages/web/.next/standalone/tsconfig.tsbuildinfo +0 -1
  76. package/packages/web/.next/standalone/types/index.ts +0 -74
  77. package/packages/web/.next/standalone/vitest.config.ts +0 -14
  78. package/packages/web/.next/static/chunks/4bbca8cd642026de.css +0 -3
  79. package/packages/web/.next/static/chunks/54e2bd8f072e7d4e.js +0 -1
@@ -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
- }
@@ -1,290 +0,0 @@
1
- 'use client';
2
-
3
- import { useState, useEffect } from 'react';
4
- import { FileSystemItem } from '@/types';
5
- import { FolderIcon, FileIcon, ArrowUpIcon } from 'lucide-react';
6
-
7
- interface FileBrowserProps {
8
- isOpen: boolean;
9
- currentPath: string;
10
- onSelect: (filePaths: string[]) => void;
11
- onClose: () => void;
12
- multiSelect?: boolean;
13
- filterJsonOnly?: boolean;
14
- /** フォルダ選択モード(trueの場合、ファイルは表示せずフォルダのみ選択可能) */
15
- folderSelectMode?: boolean;
16
- /** ダイアログのタイトル */
17
- title?: string;
18
- }
19
-
20
- export function FileBrowser({
21
- isOpen,
22
- currentPath,
23
- onSelect,
24
- onClose,
25
- multiSelect = false,
26
- filterJsonOnly = false,
27
- folderSelectMode = false,
28
- title
29
- }: FileBrowserProps) {
30
- const [path, setPath] = useState(currentPath);
31
- const [items, setItems] = useState<FileSystemItem[]>([]);
32
- const [loading, setLoading] = useState(false);
33
- const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
34
-
35
- useEffect(() => {
36
- if (isOpen) {
37
- // ダイアログが開かれた時、currentPathでパスをリセット
38
- setPath(currentPath);
39
- setSelectedFiles([]);
40
- }
41
- }, [isOpen, currentPath]);
42
-
43
- useEffect(() => {
44
- if (isOpen) {
45
- // パスが変更された時にディレクトリをロード
46
- loadDirectory(path);
47
- }
48
- }, [isOpen, path]);
49
-
50
- const loadDirectory = async (dirPath: string) => {
51
- setLoading(true);
52
- try {
53
- const response = await fetch('/api/files/browse', {
54
- method: 'POST',
55
- headers: { 'Content-Type': 'application/json' },
56
- body: JSON.stringify({ directory: dirPath })
57
- });
58
- const data = await response.json();
59
- if (data.success) {
60
- // フィルタリング
61
- let filtered = data.data.items;
62
- if (folderSelectMode) {
63
- // フォルダ選択モード:フォルダのみ表示
64
- filtered = data.data.items.filter((item: FileSystemItem) =>
65
- item.type === 'directory'
66
- );
67
- } else if (filterJsonOnly) {
68
- filtered = data.data.items.filter((item: FileSystemItem) =>
69
- item.type === 'directory' || item.extension === '.json'
70
- );
71
- }
72
- setItems(filtered);
73
- } else {
74
- console.error('API returned error:', data.error);
75
- }
76
- } catch (error) {
77
- console.error('Failed to load directory:', error);
78
- } finally {
79
- setLoading(false);
80
- }
81
- };
82
-
83
- const handleItemClick = (item: FileSystemItem) => {
84
- if (item.type === 'directory') {
85
- if (folderSelectMode) {
86
- // フォルダ選択モード:ダブルクリックで移動、シングルクリックで選択
87
- // ここではシングルクリックで選択のみ(移動はダブルクリックで)
88
- setSelectedFiles([item.path]);
89
- } else {
90
- // 通常モード:ディレクトリはクリックで移動
91
- setPath(item.path);
92
- }
93
- } else {
94
- if (multiSelect) {
95
- // 複数選択モード
96
- setSelectedFiles(prev =>
97
- prev.includes(item.path)
98
- ? prev.filter(p => p !== item.path)
99
- : [...prev, item.path]
100
- );
101
- } else {
102
- // 単一選択モード
103
- setSelectedFiles([item.path]);
104
- }
105
- }
106
- };
107
-
108
- const handleItemDoubleClick = (item: FileSystemItem) => {
109
- if (item.type === 'directory') {
110
- // ダブルクリックでディレクトリに移動
111
- setPath(item.path);
112
- setSelectedFiles([]);
113
- }
114
- };
115
-
116
- // パス区切り文字を正規化する(クロスプラットフォーム対応)
117
- const normalizePath = (p: string): string => {
118
- // バックスラッシュをスラッシュに統一
119
- return p.replace(/\\/g, '/');
120
- };
121
-
122
- // 親ディレクトリへの移動が可能かどうか
123
- const canGoUp = (): boolean => {
124
- const normalizedPath = normalizePath(path).toLowerCase();
125
- const normalizedRoot = normalizePath(currentPath).toLowerCase();
126
- // 現在のパスがルートパスと同じなら上に移動できない
127
- return normalizedPath !== normalizedRoot;
128
- };
129
-
130
- const handleGoUp = () => {
131
- // ルートパスより上には移動できない
132
- if (!canGoUp()) {
133
- return;
134
- }
135
-
136
- // パスを正規化(スラッシュに統一)
137
- const normalizedPath = normalizePath(path);
138
- const pathParts = normalizedPath.split('/').filter(p => p);
139
-
140
- if (pathParts.length > 1) {
141
- // 親ディレクトリへ移動
142
- const parentParts = pathParts.slice(0, -1);
143
-
144
- // パスを再構築
145
- let parentPath: string;
146
- if (parentParts.length === 1 && parentParts[0].includes(':')) {
147
- // Windowsドライブルートの場合(例: C:/ )
148
- parentPath = parentParts[0] + '/';
149
- } else if (parentParts[0].includes(':')) {
150
- // Windowsドライブレターを含むパスの場合(例: C:/sksp)
151
- parentPath = parentParts[0] + '/' + parentParts.slice(1).join('/');
152
- } else if (normalizedPath.startsWith('/')) {
153
- // Unix絶対パスの場合(例: /tmp/foo)
154
- parentPath = '/' + parentParts.join('/');
155
- } else {
156
- // 相対パスの場合
157
- parentPath = parentParts.join('/');
158
- }
159
-
160
- // 親パスがルートパスより上でなければ移動
161
- const normalizedParent = normalizePath(parentPath).toLowerCase();
162
- const normalizedRoot = normalizePath(currentPath).toLowerCase();
163
- // 親パスがルートパスを含んでいれば(ルートパス以下であれば)移動可能
164
- if (normalizedParent.startsWith(normalizedRoot) || normalizedParent === normalizedRoot) {
165
- setPath(parentPath);
166
- }
167
- } else if (pathParts.length === 1 && !pathParts[0].includes(':')) {
168
- // ルート以外の単一ディレクトリの場合
169
- const normalizedRoot = normalizePath(currentPath).toLowerCase();
170
- if (normalizedRoot === '.') {
171
- return; // ルートが'.'なら上に移動しない
172
- }
173
- setPath('.');
174
- }
175
- // ドライブルート(例: C:/)やUnixルート(/)の場合は何もしない
176
- // ディレクトリ移動時は選択を保持
177
- };
178
-
179
- const handleConfirm = () => {
180
- if (folderSelectMode) {
181
- // フォルダ選択モード:選択されたフォルダがあればそれを、なければ現在のパスを返す
182
- const selectedPath = selectedFiles.length > 0 ? selectedFiles : [path];
183
- onSelect(selectedPath);
184
- setSelectedFiles([]);
185
- onClose();
186
- } else if (selectedFiles.length > 0) {
187
- onSelect(selectedFiles);
188
- setSelectedFiles([]);
189
- onClose();
190
- }
191
- };
192
-
193
- const handleClose = () => {
194
- setSelectedFiles([]);
195
- onClose();
196
- };
197
-
198
- if (!isOpen) return null;
199
-
200
- return (
201
- <div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
202
- <div className="bg-white rounded-lg shadow-xl w-full max-w-2xl max-h-[80vh] flex flex-col">
203
- {/* ヘッダー */}
204
- <div className="flex items-center justify-between p-4 border-b">
205
- <h2 className="text-lg font-semibold">{title || (folderSelectMode ? 'フォルダを選択' : 'ファイルを選択')}</h2>
206
- <button
207
- onClick={handleClose}
208
- className="text-gray-500 hover:text-gray-700"
209
- >
210
-
211
- </button>
212
- </div>
213
-
214
- {/* パスとナビゲーション */}
215
- <div className="flex items-center gap-2 p-4 border-b bg-gray-50">
216
- <button
217
- onClick={handleGoUp}
218
- disabled={!canGoUp()}
219
- className={`p-2 rounded flex-shrink-0 ${canGoUp() ? 'hover:bg-gray-200' : 'opacity-50 cursor-not-allowed'}`}
220
- title={canGoUp() ? '上のディレクトリへ' : 'ルートディレクトリです'}
221
- >
222
- <ArrowUpIcon className="w-5 h-5" />
223
- </button>
224
- <span className="text-sm text-gray-600 flex-1 truncate">パス: {path}</span>
225
- </div>
226
-
227
- {/* ファイル一覧 */}
228
- <div className="flex-1 overflow-y-auto p-4">
229
- {loading ? (
230
- <div className="text-center text-gray-500">読み込み中...</div>
231
- ) : (
232
- <div className="space-y-1">
233
- {items.map((item) => {
234
- const isSelected = selectedFiles.includes(item.path);
235
- return (
236
- <div
237
- key={item.path}
238
- onClick={() => handleItemClick(item)}
239
- onDoubleClick={() => handleItemDoubleClick(item)}
240
- className={`flex items-center gap-2 p-2 rounded cursor-pointer hover:bg-gray-100 ${
241
- isSelected ? 'bg-blue-100' : ''
242
- }`}
243
- >
244
- {item.type === 'directory' ? (
245
- <FolderIcon className="w-5 h-5 text-blue-500" />
246
- ) : (
247
- <FileIcon className="w-5 h-5 text-green-500" />
248
- )}
249
- <span className="text-sm">{item.name}</span>
250
- {isSelected && (multiSelect || folderSelectMode) && (
251
- <span className="ml-auto text-blue-500">✓</span>
252
- )}
253
- </div>
254
- );
255
- })}
256
- </div>
257
- )}
258
- </div>
259
-
260
- {/* フッター */}
261
- <div className="flex items-center justify-between p-4 border-t">
262
- {folderSelectMode ? (
263
- <div className="text-sm text-gray-600">
264
- {selectedFiles.length > 0 ? `選択中: ${selectedFiles[0].split(/[/\\]/).pop()}` : `現在のフォルダ: ${path.split(/[/\\]/).pop() || path}`}
265
- </div>
266
- ) : multiSelect && selectedFiles.length > 0 ? (
267
- <div className="text-sm text-gray-600">
268
- {selectedFiles.length}件選択中
269
- </div>
270
- ) : null}
271
- <div className="flex items-center gap-2 ml-auto">
272
- <button
273
- onClick={handleClose}
274
- className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded"
275
- >
276
- キャンセル
277
- </button>
278
- <button
279
- onClick={handleConfirm}
280
- disabled={!folderSelectMode && selectedFiles.length === 0}
281
- className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 disabled:bg-gray-300 disabled:cursor-not-allowed"
282
- >
283
- 選択
284
- </button>
285
- </div>
286
- </div>
287
- </div>
288
- </div>
289
- );
290
- }
@@ -1,37 +0,0 @@
1
- 'use client';
2
-
3
- import { DownloadIcon } from 'lucide-react';
4
-
5
- interface HeaderProps {
6
- onExportClick: () => void;
7
- }
8
-
9
- export function Header({ onExportClick }: HeaderProps) {
10
- return (
11
- <header className="bg-white/80 backdrop-blur-md border-b border-gray-200 shadow-sm sticky top-0 z-50">
12
- <div className="container mx-auto px-6 py-4 flex items-center justify-between">
13
- <div className="flex items-center gap-3">
14
- <img
15
- src="/logo.svg"
16
- alt="ConfigDoc"
17
- className="w-12 h-12 rounded-lg"
18
- />
19
- <div>
20
- <h1 className="text-2xl font-bold bg-gradient-to-r from-blue-600 to-purple-600 bg-clip-text text-transparent">
21
- ConfigDoc
22
- </h1>
23
- <p className="text-xs text-gray-500">Configuration Documentation Tool</p>
24
- </div>
25
- </div>
26
- <button
27
- onClick={onExportClick}
28
- className="flex items-center gap-2 px-5 py-2.5 bg-gradient-to-r from-green-500 to-emerald-600 text-white rounded-lg hover:from-green-600 hover:to-emerald-700 shadow-md hover:shadow-lg transition-all duration-200 transform"
29
- title="エクスポート"
30
- >
31
- <DownloadIcon className="w-5 h-5" />
32
- <span className="font-medium">エクスポート</span>
33
- </button>
34
- </div>
35
- </header>
36
- );
37
- }