@interactive-inc/docs-client 0.0.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.
Files changed (91) hide show
  1. package/.tanstack/tmp/router-generator-Eq20Yx/50b11a0f488e61cf92364a7e585c1448 +123 -0
  2. package/.tanstack/tmp/router-generator-KQIYSa/aecb094e4891f9475b91de590abc8809 +117 -0
  3. package/.tanstack/tmp/router-generator-ekW2pI/aecb094e4891f9475b91de590abc8809 +117 -0
  4. package/app/components/archived-file-list-view.tsx +99 -0
  5. package/app/components/directory-file-list-view.tsx +32 -0
  6. package/app/components/directory-file-tree-node.tsx +96 -0
  7. package/app/components/directory-layout-sidebar.tsx +181 -0
  8. package/app/components/directory-page-view.tsx +208 -0
  9. package/app/components/directory-table-view.tsx +383 -0
  10. package/app/components/editable-table-cell/boolean-editable-cell.tsx +44 -0
  11. package/app/components/editable-table-cell/multi-boolean-editable-cell.tsx +59 -0
  12. package/app/components/editable-table-cell/multi-number-editable-cell.tsx +62 -0
  13. package/app/components/editable-table-cell/multi-relation-editable-cell.tsx +19 -0
  14. package/app/components/editable-table-cell/multi-select-number-editable-cell.tsx +82 -0
  15. package/app/components/editable-table-cell/multi-select-text-editable-cell.tsx +82 -0
  16. package/app/components/editable-table-cell/multi-text-editable-cell.tsx +62 -0
  17. package/app/components/editable-table-cell/number-editable-cell.tsx +54 -0
  18. package/app/components/editable-table-cell/relation-editable-cell.tsx +20 -0
  19. package/app/components/editable-table-cell/select-number-editable-cell.tsx +43 -0
  20. package/app/components/editable-table-cell/select-text-editable-cell.tsx +33 -0
  21. package/app/components/editable-table-cell/string-editable-cell.tsx +42 -0
  22. package/app/components/editable-table-cell.tsx +129 -0
  23. package/app/components/file-card.tsx +120 -0
  24. package/app/components/file-tree-icon.tsx +26 -0
  25. package/app/components/file-view/archive-restore-banner.tsx +86 -0
  26. package/app/components/file-view/csv-file-view.tsx +50 -0
  27. package/app/components/file-view/default-file-view.tsx +11 -0
  28. package/app/components/file-view/editable-csv-cell.tsx +37 -0
  29. package/app/components/file-view/editable-front-matter-view.tsx +105 -0
  30. package/app/components/file-view/file-csv-columns-view.tsx +181 -0
  31. package/app/components/file-view/file-csv-table.tsx +142 -0
  32. package/app/components/file-view/file-header.tsx +115 -0
  33. package/app/components/file-view/file-page-view.tsx +165 -0
  34. package/app/components/file-view/front-matter-input-field.tsx +157 -0
  35. package/app/components/file-view/front-matter-view.tsx +30 -0
  36. package/app/components/file-view/json-file-editor.tsx +38 -0
  37. package/app/components/file-view/markdown-file-view.tsx +71 -0
  38. package/app/components/file-view/multi-relation-select.tsx +129 -0
  39. package/app/components/file-view/render-front-matter-value.tsx +57 -0
  40. package/app/components/file-view/single-relation-select.tsx +41 -0
  41. package/app/components/file-view/sortable-csv-row.tsx +57 -0
  42. package/app/components/file-view/sortable-header.tsx +30 -0
  43. package/app/components/file-view/utils.ts +8 -0
  44. package/app/components/milestones-editor.tsx +95 -0
  45. package/app/components/page-header.tsx +20 -0
  46. package/app/components/project-view/feature-item.tsx +139 -0
  47. package/app/components/project-view/project-page-group.tsx +125 -0
  48. package/app/components/project-view/project-view.tsx +230 -0
  49. package/app/components/project-view/unlinked-features-section.tsx +73 -0
  50. package/app/components/providers.tsx +21 -0
  51. package/app/components/reload-button.tsx +25 -0
  52. package/app/components/sidebar-button.tsx +13 -0
  53. package/app/components/ui/alert.tsx +66 -0
  54. package/app/components/ui/badge.tsx +46 -0
  55. package/app/components/ui/button.tsx +59 -0
  56. package/app/components/ui/card.tsx +92 -0
  57. package/app/components/ui/dialog.tsx +133 -0
  58. package/app/components/ui/emoji-picker.tsx +133 -0
  59. package/app/components/ui/icons.tsx +3 -0
  60. package/app/components/ui/input.tsx +21 -0
  61. package/app/components/ui/select.tsx +183 -0
  62. package/app/components/ui/separator.tsx +26 -0
  63. package/app/components/ui/sheet.tsx +137 -0
  64. package/app/components/ui/sidebar.tsx +724 -0
  65. package/app/components/ui/simple-sidebar.tsx +50 -0
  66. package/app/components/ui/skeleton.tsx +13 -0
  67. package/app/components/ui/sonner.tsx +23 -0
  68. package/app/components/ui/table.tsx +114 -0
  69. package/app/components/ui/tabs.tsx +53 -0
  70. package/app/components/ui/textarea.tsx +18 -0
  71. package/app/components/ui/tooltip.tsx +59 -0
  72. package/app/components/vscode-button.tsx +23 -0
  73. package/app/hooks/use-directory-query.ts +22 -0
  74. package/app/hooks/use-file-properties-mutation.ts +38 -0
  75. package/app/hooks/use-file-query.ts +21 -0
  76. package/app/hooks/use-mobile.ts +19 -0
  77. package/app/main.css +122 -0
  78. package/app/main.tsx +38 -0
  79. package/app/routes/$.index.tsx +25 -0
  80. package/app/routes/$.tsx +20 -0
  81. package/app/routes/__root.tsx +20 -0
  82. package/app/routes/_index.tsx +17 -0
  83. package/app/routes/apps.client.$.tsx +21 -0
  84. package/build/assets/index.css +1 -0
  85. package/build/assets/index.js +298 -0
  86. package/build/index.d.ts +2 -0
  87. package/build/index.d.ts.map +1 -0
  88. package/build/index.js +23 -0
  89. package/components.json +21 -0
  90. package/package.json +54 -0
  91. package/tsconfig.index.json +17 -0
@@ -0,0 +1,82 @@
1
+ import { X } from "lucide-react"
2
+ import { useState } from "react"
3
+ import { Badge } from "@/components/ui/badge"
4
+ import { Button } from "@/components/ui/button"
5
+
6
+ type Props = {
7
+ value: string[]
8
+ options: string[]
9
+ onUpdate: (value: string[]) => void
10
+ }
11
+
12
+ /**
13
+ * 複数選択テキストセル
14
+ */
15
+ export function MultiSelectTextEditableCell(props: Props) {
16
+ const [isOpen, setIsOpen] = useState(false)
17
+ const currentValues = props.value || []
18
+
19
+ const handleToggle = (option: string) => {
20
+ const isSelected = currentValues.includes(option)
21
+ const newValues = isSelected
22
+ ? currentValues.filter((v) => v !== option)
23
+ : [...currentValues, option]
24
+ props.onUpdate(newValues)
25
+ }
26
+
27
+ const handleRemove = (value: string) => {
28
+ const newValues = currentValues.filter((v) => v !== value)
29
+ props.onUpdate(newValues)
30
+ }
31
+
32
+ return (
33
+ <div className="relative">
34
+ <div className="flex min-h-8 flex-wrap gap-1 rounded-md border p-2">
35
+ {currentValues.map((value) => (
36
+ <Badge
37
+ key={value}
38
+ variant="secondary"
39
+ className="flex items-center gap-1"
40
+ >
41
+ {value}
42
+ <Button
43
+ size="sm"
44
+ variant="ghost"
45
+ className="h-4 w-4 p-0"
46
+ onClick={() => handleRemove(value)}
47
+ >
48
+ <X className="h-3 w-3" />
49
+ </Button>
50
+ </Badge>
51
+ ))}
52
+ <Button
53
+ variant="ghost"
54
+ size="sm"
55
+ onClick={() => setIsOpen(!isOpen)}
56
+ className="text-muted-foreground"
57
+ >
58
+ 追加
59
+ </Button>
60
+ </div>
61
+ {isOpen && (
62
+ <div className="absolute z-10 mt-1 w-full rounded-md border bg-white shadow-lg">
63
+ {props.options
64
+ .filter((option) => !currentValues.includes(option))
65
+ .map((option) => (
66
+ <button
67
+ type="button"
68
+ key={option}
69
+ className="w-full px-3 py-2 text-left hover:bg-gray-100"
70
+ onClick={() => {
71
+ handleToggle(option)
72
+ setIsOpen(false)
73
+ }}
74
+ >
75
+ {option}
76
+ </button>
77
+ ))}
78
+ </div>
79
+ )}
80
+ </div>
81
+ )
82
+ }
@@ -0,0 +1,62 @@
1
+ import { useEffect, useState } from "react"
2
+ import { Input } from "@/components/ui/input"
3
+
4
+ type Props = {
5
+ value: unknown
6
+ onUpdate: (value: unknown) => void
7
+ }
8
+
9
+ export function MultiTextEditableCell(props: Props) {
10
+ const formatValue = (value: unknown): string => {
11
+ if (value === undefined || value === null) {
12
+ return ""
13
+ }
14
+ return Array.isArray(value) ? value.join(", ") : ""
15
+ }
16
+
17
+ const [editValue, setEditValue] = useState(formatValue(props.value))
18
+
19
+ // biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
20
+ useEffect(() => {
21
+ setEditValue(formatValue(props.value))
22
+ }, [props.value])
23
+
24
+ const parseValue = (text: string): unknown => {
25
+ if (text === "") {
26
+ return undefined
27
+ }
28
+ return text
29
+ .split(",")
30
+ .map((s) => s.trim())
31
+ .filter(Boolean)
32
+ }
33
+
34
+ const displayValue = formatValue(props.value)
35
+
36
+ const handleBlur = () => {
37
+ const parsedValue = parseValue(editValue)
38
+ if (parsedValue !== props.value) {
39
+ props.onUpdate(parsedValue)
40
+ }
41
+ }
42
+
43
+ const handleKeyDown = (e: React.KeyboardEvent) => {
44
+ if (e.key === "Enter") {
45
+ handleBlur()
46
+ }
47
+ if (e.key === "Escape") {
48
+ setEditValue(displayValue)
49
+ }
50
+ }
51
+
52
+ return (
53
+ <Input
54
+ type="text"
55
+ value={editValue}
56
+ onChange={(e) => setEditValue(e.target.value)}
57
+ onBlur={handleBlur}
58
+ onKeyDown={handleKeyDown}
59
+ placeholder={displayValue || "-"}
60
+ />
61
+ )
62
+ }
@@ -0,0 +1,54 @@
1
+ import { useEffect, useState } from "react"
2
+ import { Input } from "@/components/ui/input"
3
+
4
+ type Props = {
5
+ value: number | null
6
+ onUpdate: (value: number | null) => void
7
+ }
8
+
9
+ export function NumberEditableCell(props: Props) {
10
+ const [_isEditing, setIsEditing] = useState(false)
11
+
12
+ const displayValue =
13
+ props.value !== undefined && props.value !== null ? String(props.value) : ""
14
+ const [editValue, setEditValue] = useState(displayValue)
15
+
16
+ useEffect(() => {
17
+ setEditValue(displayValue)
18
+ }, [displayValue])
19
+
20
+ const _handleClick = () => {
21
+ setIsEditing(true)
22
+ setEditValue(props.value ? String(props.value) : "")
23
+ }
24
+
25
+ const handleBlur = () => {
26
+ setIsEditing(false)
27
+ const num = Number(editValue)
28
+ const parsedValue =
29
+ editValue === "" ? undefined : Number.isNaN(num) ? undefined : num
30
+ if (parsedValue !== props.value) {
31
+ props.onUpdate(parsedValue ?? null)
32
+ }
33
+ }
34
+
35
+ const handleKeyDown = (e: React.KeyboardEvent) => {
36
+ if (e.key === "Enter") {
37
+ handleBlur()
38
+ }
39
+ if (e.key === "Escape") {
40
+ setIsEditing(false)
41
+ setEditValue(props.value ? String(props.value) : "")
42
+ }
43
+ }
44
+
45
+ return (
46
+ <Input
47
+ type="number"
48
+ value={editValue}
49
+ onChange={(e) => setEditValue(e.target.value)}
50
+ onBlur={handleBlur}
51
+ onKeyDown={handleKeyDown}
52
+ />
53
+ )
54
+ }
@@ -0,0 +1,20 @@
1
+ import { SingleRelationSelect } from "@/components/file-view/single-relation-select"
2
+ import type { DocRelationFile } from "@/lib/types"
3
+
4
+ type Props = {
5
+ value: unknown
6
+ onUpdate: (value: unknown) => void
7
+ relationOptions?: DocRelationFile[]
8
+ }
9
+
10
+ export function RelationEditableCell(props: Props) {
11
+ return (
12
+ <SingleRelationSelect
13
+ value={(props.value as string) || ""}
14
+ relationOptions={props.relationOptions}
15
+ onValueChange={(value) => {
16
+ props.onUpdate(value)
17
+ }}
18
+ />
19
+ )
20
+ }
@@ -0,0 +1,43 @@
1
+ import {
2
+ Select,
3
+ SelectContent,
4
+ SelectItem,
5
+ SelectTrigger,
6
+ SelectValue,
7
+ } from "@/components/ui/select"
8
+
9
+ type Props = {
10
+ value: number
11
+ options: number[]
12
+ onValueChange: (value: number) => void
13
+ }
14
+
15
+ /**
16
+ * 数値選択セル
17
+ */
18
+ export function SelectNumberEditableCell(props: Props) {
19
+ const handleValueChange = (value: string) => {
20
+ const numValue = Number.parseInt(value, 10)
21
+ if (!Number.isNaN(numValue)) {
22
+ props.onValueChange(numValue)
23
+ }
24
+ }
25
+
26
+ return (
27
+ <Select
28
+ value={props.value?.toString() || ""}
29
+ onValueChange={handleValueChange}
30
+ >
31
+ <SelectTrigger className="w-full bg-background">
32
+ <SelectValue placeholder="選択してください" />
33
+ </SelectTrigger>
34
+ <SelectContent>
35
+ {props.options.map((option) => (
36
+ <SelectItem key={option} value={option.toString()}>
37
+ {option}
38
+ </SelectItem>
39
+ ))}
40
+ </SelectContent>
41
+ </Select>
42
+ )
43
+ }
@@ -0,0 +1,33 @@
1
+ import {
2
+ Select,
3
+ SelectContent,
4
+ SelectItem,
5
+ SelectTrigger,
6
+ SelectValue,
7
+ } from "@/components/ui/select"
8
+
9
+ type Props = {
10
+ value: string
11
+ options: string[]
12
+ onValueChange: (value: string) => void
13
+ }
14
+
15
+ /**
16
+ * テキスト選択セル
17
+ */
18
+ export function SelectTextEditableCell(props: Props) {
19
+ return (
20
+ <Select value={props.value || ""} onValueChange={props.onValueChange}>
21
+ <SelectTrigger className="w-full bg-background">
22
+ <SelectValue placeholder="選択してください" />
23
+ </SelectTrigger>
24
+ <SelectContent>
25
+ {props.options.map((option) => (
26
+ <SelectItem key={option} value={option}>
27
+ {option}
28
+ </SelectItem>
29
+ ))}
30
+ </SelectContent>
31
+ </Select>
32
+ )
33
+ }
@@ -0,0 +1,42 @@
1
+ import { useEffect, useState } from "react"
2
+ import { Input } from "@/components/ui/input"
3
+
4
+ type Props = {
5
+ value: unknown
6
+ onUpdate: (value: unknown) => void
7
+ }
8
+
9
+ export function StringEditableCell(props: Props) {
10
+ const displayValue = props.value ? String(props.value) : ""
11
+ const [editValue, setEditValue] = useState(displayValue)
12
+
13
+ useEffect(() => {
14
+ setEditValue(displayValue)
15
+ }, [displayValue])
16
+
17
+ const handleBlur = () => {
18
+ if (editValue !== props.value) {
19
+ props.onUpdate(editValue || undefined)
20
+ }
21
+ }
22
+
23
+ const handleKeyDown = (e: React.KeyboardEvent) => {
24
+ if (e.key === "Enter") {
25
+ handleBlur()
26
+ }
27
+ if (e.key === "Escape") {
28
+ setEditValue(displayValue)
29
+ }
30
+ }
31
+
32
+ return (
33
+ <Input
34
+ type="text"
35
+ value={editValue}
36
+ onChange={(e) => setEditValue(e.target.value)}
37
+ onBlur={handleBlur}
38
+ onKeyDown={handleKeyDown}
39
+ placeholder={displayValue || "-"}
40
+ />
41
+ )
42
+ }
@@ -0,0 +1,129 @@
1
+ import type { DocRelationFile } from "@/lib/types"
2
+ import { BooleanEditableCell } from "./editable-table-cell/boolean-editable-cell"
3
+ import { MultiBooleanEditableCell } from "./editable-table-cell/multi-boolean-editable-cell"
4
+ import { MultiNumberEditableCell } from "./editable-table-cell/multi-number-editable-cell"
5
+ import { MultiRelationEditableCell } from "./editable-table-cell/multi-relation-editable-cell"
6
+ import { MultiSelectNumberEditableCell } from "./editable-table-cell/multi-select-number-editable-cell"
7
+ import { MultiSelectTextEditableCell } from "./editable-table-cell/multi-select-text-editable-cell"
8
+ import { MultiTextEditableCell } from "./editable-table-cell/multi-text-editable-cell"
9
+ import { NumberEditableCell } from "./editable-table-cell/number-editable-cell"
10
+ import { RelationEditableCell } from "./editable-table-cell/relation-editable-cell"
11
+ import { SelectNumberEditableCell } from "./editable-table-cell/select-number-editable-cell"
12
+ import { SelectTextEditableCell } from "./editable-table-cell/select-text-editable-cell"
13
+ import { StringEditableCell } from "./editable-table-cell/string-editable-cell"
14
+
15
+ type Props = {
16
+ value: unknown
17
+ type: string
18
+ onUpdate: (value: unknown) => void
19
+ path?: string | null
20
+ relationOptions?: DocRelationFile[]
21
+ selectOptions?: string[] | number[]
22
+ }
23
+
24
+ export function EditableTableCell(props: Props) {
25
+ // Boolean型の場合
26
+ if (props.type === "boolean") {
27
+ return <BooleanEditableCell value={props.value} onUpdate={props.onUpdate} />
28
+ }
29
+
30
+ // 数値型の場合
31
+ if (props.type === "number") {
32
+ return (
33
+ <NumberEditableCell
34
+ value={props.value as number}
35
+ onUpdate={props.onUpdate}
36
+ />
37
+ )
38
+ }
39
+
40
+ // 単一リレーション型の場合
41
+ if (props.type === "relation") {
42
+ return (
43
+ <RelationEditableCell
44
+ value={props.value}
45
+ onUpdate={props.onUpdate}
46
+ relationOptions={props.relationOptions}
47
+ />
48
+ )
49
+ }
50
+
51
+ // 複数リレーション型の場合
52
+ if (props.type === "multi-relation") {
53
+ return (
54
+ <MultiRelationEditableCell
55
+ value={props.value}
56
+ onUpdate={props.onUpdate}
57
+ relationOptions={props.relationOptions}
58
+ />
59
+ )
60
+ }
61
+
62
+ // 複数テキスト型の場合
63
+ if (props.type === "multi-text") {
64
+ return (
65
+ <MultiTextEditableCell value={props.value} onUpdate={props.onUpdate} />
66
+ )
67
+ }
68
+
69
+ // 複数数値型の場合
70
+ if (props.type === "multi-number") {
71
+ return (
72
+ <MultiNumberEditableCell value={props.value} onUpdate={props.onUpdate} />
73
+ )
74
+ }
75
+
76
+ // 複数Boolean型の場合
77
+ if (props.type === "multi-boolean") {
78
+ return (
79
+ <MultiBooleanEditableCell value={props.value} onUpdate={props.onUpdate} />
80
+ )
81
+ }
82
+
83
+ // 選択テキスト型の場合
84
+ if (props.type === "select-text" && props.selectOptions) {
85
+ return (
86
+ <SelectTextEditableCell
87
+ value={props.value as string}
88
+ options={props.selectOptions as string[]}
89
+ onValueChange={props.onUpdate}
90
+ />
91
+ )
92
+ }
93
+
94
+ // 選択数値型の場合
95
+ if (props.type === "select-number" && props.selectOptions) {
96
+ return (
97
+ <SelectNumberEditableCell
98
+ value={props.value as number}
99
+ options={props.selectOptions as number[]}
100
+ onValueChange={props.onUpdate}
101
+ />
102
+ )
103
+ }
104
+
105
+ // 複数選択テキスト型の場合
106
+ if (props.type === "multi-select-text" && props.selectOptions) {
107
+ return (
108
+ <MultiSelectTextEditableCell
109
+ value={props.value as string[]}
110
+ options={props.selectOptions as string[]}
111
+ onUpdate={props.onUpdate}
112
+ />
113
+ )
114
+ }
115
+
116
+ // 複数選択数値型の場合
117
+ if (props.type === "multi-select-number" && props.selectOptions) {
118
+ return (
119
+ <MultiSelectNumberEditableCell
120
+ value={props.value as number[]}
121
+ options={props.selectOptions as number[]}
122
+ onUpdate={props.onUpdate}
123
+ />
124
+ )
125
+ }
126
+
127
+ // その他(文字列型)の場合
128
+ return <StringEditableCell value={props.value} onUpdate={props.onUpdate} />
129
+ }
@@ -0,0 +1,120 @@
1
+ import { useMutation } from "@tanstack/react-query"
2
+ import { Link } from "@tanstack/react-router"
3
+ import { Archive, Trash2 } from "lucide-react"
4
+ import { useState } from "react"
5
+ import { Button } from "@/components/ui/button"
6
+ import { apiClient } from "@/lib/api-client"
7
+ import { normalizePath } from "@/lib/path-utils"
8
+ import type { DocFileUnknown } from "@/lib/types"
9
+
10
+ type Props = {
11
+ file: DocFileUnknown
12
+ onDataChanged?: () => void
13
+ }
14
+
15
+ export function FileCard(props: Props) {
16
+ const [deleteConfirmFiles, setDeleteConfirmFiles] = useState<Set<string>>(
17
+ new Set(),
18
+ )
19
+
20
+ const deleteFileMutation = useMutation({
21
+ async mutationFn(filePath: string) {
22
+ const path = normalizePath(filePath)
23
+ const pathSegments = path
24
+ .split("/")
25
+ .map((segment) => encodeURIComponent(segment))
26
+ const encodedPath = pathSegments.join("/")
27
+ const resp = await fetch(`/api/files/${encodedPath}`, {
28
+ method: "DELETE",
29
+ })
30
+
31
+ if (!resp.ok) {
32
+ const errorText = await resp.text()
33
+ throw new Error(`Failed to delete file: ${errorText}`)
34
+ }
35
+
36
+ return resp.json()
37
+ },
38
+ onSuccess() {
39
+ if (!props.onDataChanged) return
40
+ props.onDataChanged()
41
+ },
42
+ })
43
+
44
+ const archiveFileMutation = useMutation({
45
+ async mutationFn(filePath: string) {
46
+ const endpoint = apiClient.api.files[":path{.+}"]
47
+ const resp = await endpoint.$put({
48
+ param: { path: normalizePath(filePath) },
49
+ json: {
50
+ properties: null,
51
+ content: null,
52
+ title: null,
53
+ description: null,
54
+ isArchived: true,
55
+ },
56
+ })
57
+
58
+ return resp.json()
59
+ },
60
+ onSuccess() {
61
+ if (!props.onDataChanged) return
62
+ props.onDataChanged()
63
+ },
64
+ })
65
+
66
+
67
+ const handleDeleteClick = (filePath: string) => {
68
+ if (deleteConfirmFiles.has(filePath)) {
69
+ deleteFileMutation.mutate(filePath)
70
+ setDeleteConfirmFiles(new Set())
71
+ } else {
72
+ setDeleteConfirmFiles(new Set([filePath]))
73
+ }
74
+ }
75
+
76
+ const handleArchiveClick = (filePath: string) => {
77
+ archiveFileMutation.mutate(filePath)
78
+ }
79
+
80
+ return (
81
+ <div className="flex items-center justify-between">
82
+ <Link
83
+ to="/$"
84
+ params={{ _splat: normalizePath(props.file.path.path) }}
85
+ className="text-blue-600 hover:underline"
86
+ >
87
+ {props.file.path.name}
88
+ </Link>
89
+ <div className="flex items-center gap-2">
90
+ <span className="text-sm uppercase opacity-50">
91
+ {props.file.extension}
92
+ </span>
93
+ <div className="flex gap-1">
94
+ <Button
95
+ size="sm"
96
+ variant="ghost"
97
+ onClick={() => handleArchiveClick(props.file.path.path)}
98
+ disabled={archiveFileMutation.isPending}
99
+ className="h-8 w-8 p-0"
100
+ title="アーカイブする"
101
+ >
102
+ <Archive className="h-4 w-4" />
103
+ </Button>
104
+ <Button
105
+ size="sm"
106
+ variant={
107
+ deleteConfirmFiles.has(props.file.path.path) ? "destructive" : "ghost"
108
+ }
109
+ onClick={() => handleDeleteClick(props.file.path.path)}
110
+ disabled={deleteFileMutation.isPending}
111
+ className="h-8 w-8 p-0"
112
+ title="削除する"
113
+ >
114
+ <Trash2 className="h-4 w-4" />
115
+ </Button>
116
+ </div>
117
+ </div>
118
+ </div>
119
+ )
120
+ }
@@ -0,0 +1,26 @@
1
+ import {
2
+ FileIcon,
3
+ FileJsonIcon,
4
+ FileSpreadsheetIcon,
5
+ FileTextIcon,
6
+ } from "lucide-react"
7
+
8
+ type Props = {
9
+ fileName: string
10
+ }
11
+
12
+ export function FileTreeIcon(props: Props) {
13
+ if (props.fileName.endsWith(".md")) {
14
+ return <FileTextIcon size={16} className="mr-2 text-green-400" />
15
+ }
16
+
17
+ if (props.fileName.endsWith(".csv")) {
18
+ return <FileSpreadsheetIcon size={16} className="mr-2 text-purple-400" />
19
+ }
20
+
21
+ if (props.fileName.endsWith(".json")) {
22
+ return <FileJsonIcon size={16} className="mr-2 text-yellow-400" />
23
+ }
24
+
25
+ return <FileIcon size={16} className="mr-2 text-blue-400" />
26
+ }
@@ -0,0 +1,86 @@
1
+ import { useMutation } from "@tanstack/react-query"
2
+ import { useNavigate } from "@tanstack/react-router"
3
+ import { useState } from "react"
4
+ import { Button } from "@/components/ui/button"
5
+ import { Card } from "@/components/ui/card"
6
+ import { apiClient } from "@/lib/api-client"
7
+ import { normalizePath } from "@/lib/path-utils"
8
+
9
+ type Props = {
10
+ filePath: string
11
+ onRestore?: () => void
12
+ }
13
+
14
+ export function ArchiveRestoreBanner(props: Props) {
15
+ const navigate = useNavigate()
16
+
17
+ const [isRestoring, setIsRestoring] = useState(false)
18
+
19
+ const restoreMutation = useMutation({
20
+ async mutationFn() {
21
+ setIsRestoring(true)
22
+
23
+ // パスから docs/ プレフィックスを削除
24
+ const normalizedPath = normalizePath(props.filePath)
25
+
26
+ // ファイルを復元(アーカイブから移動)
27
+ const response = await apiClient.api.files[":path{.+}"].$put({
28
+ param: {
29
+ path: normalizedPath,
30
+ },
31
+ json: {
32
+ title: null,
33
+ properties: null,
34
+ content: null,
35
+ description: null,
36
+ isArchived: false, // アーカイブを解除
37
+ },
38
+ })
39
+
40
+ return response.json()
41
+ },
42
+ onSuccess: async (_data) => {
43
+ // 復元後のパスを構築(_ディレクトリを除外)
44
+ const pathSegments = props.filePath.split("/")
45
+ const fileName = pathSegments.pop()
46
+ const restoredPath = pathSegments
47
+ .filter((segment) => segment !== "_")
48
+ .concat(fileName || "")
49
+ .join("/")
50
+
51
+ // コールバックを実行
52
+ if (props.onRestore) {
53
+ props.onRestore()
54
+ }
55
+
56
+ // 復元後のファイルパスにリダイレクト
57
+ navigate({ to: `/${restoredPath}` })
58
+ },
59
+ onError: (error) => {
60
+ console.error("復元エラー:", error)
61
+ alert("ファイルの復元に失敗しました")
62
+ },
63
+ onSettled: () => {
64
+ setIsRestoring(false)
65
+ },
66
+ })
67
+
68
+ return (
69
+ <Card className="p-2">
70
+ <div className="flex items-center justify-between gap-2">
71
+ <div className="flex items-center gap-x-2">
72
+ <span className="text-muted-foreground text-sm">
73
+ このファイルはアーカイブされています
74
+ </span>
75
+ </div>
76
+ <Button
77
+ size="sm"
78
+ onClick={() => restoreMutation.mutate()}
79
+ disabled={isRestoring || restoreMutation.isPending}
80
+ >
81
+ {isRestoring ? "復元中..." : "復元する"}
82
+ </Button>
83
+ </div>
84
+ </Card>
85
+ )
86
+ }