@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,165 @@
1
+ import { useState } from "react"
2
+ import { CsvFileView } from "@/components/file-view/csv-file-view"
3
+ import { DefaultFileViewer } from "@/components/file-view/default-file-view"
4
+ import { JsonFileEditor } from "@/components/file-view/json-file-editor"
5
+ import { MarkdownFileView } from "@/components/file-view/markdown-file-view"
6
+ import { useDirectoryQuery } from "@/hooks/use-directory-query"
7
+ import { useFilePropertiesMutation } from "@/hooks/use-file-properties-mutation"
8
+ import { useFileQuery } from "@/hooks/use-file-query"
9
+ import { apiClient } from "@/lib/api-client"
10
+ import { normalizePath } from "@/lib/path-utils"
11
+
12
+ type Props = {
13
+ filePath: string
14
+ }
15
+
16
+ export function FilePageView(props: Props) {
17
+ const fileQuery = useFileQuery(props.filePath)
18
+
19
+ const directoryQuery = useDirectoryQuery(props.filePath)
20
+
21
+ const fileData = fileQuery.data
22
+
23
+ const directorySchema =
24
+ directoryQuery.data?.indexFile?.content?.frontMatter?.schema || {}
25
+
26
+ const relations = directoryQuery.data?.relations || []
27
+
28
+ // contentがオブジェクトの場合はbodyプロパティを使用
29
+ const initialContent = typeof fileData?.content === 'string'
30
+ ? fileData.content
31
+ : fileData?.content?.body || ""
32
+ const [currentContent, setCurrentContent] = useState(initialContent)
33
+
34
+ const updateProperties = useFilePropertiesMutation()
35
+
36
+ const onChange = async (newContent: string) => {
37
+ const normalizedPath = normalizePath(props.filePath)
38
+
39
+ const result = await apiClient.api.files[":path{.+}"].$put({
40
+ param: { path: normalizedPath },
41
+ json: {
42
+ title: null,
43
+ properties: null,
44
+ content: newContent,
45
+ description: null,
46
+ isArchived: null,
47
+ },
48
+ })
49
+
50
+ const data = await result.json()
51
+ if ("content" in data && typeof data.content === "string") {
52
+ setCurrentContent(data.content)
53
+ }
54
+ }
55
+
56
+ const handleReload = async () => {
57
+ const result = await fileQuery.refetch()
58
+ if (result.data === undefined) return
59
+ const content = typeof result.data.content === 'string'
60
+ ? result.data.content
61
+ : result.data.content?.body || ""
62
+ setCurrentContent(content)
63
+ }
64
+
65
+ const handleFrontMatterUpdate = async (key: string, value: unknown) => {
66
+ await updateProperties.mutateAsync({
67
+ path: props.filePath,
68
+ field: key,
69
+ value: value,
70
+ })
71
+ // フロントマター更新後にファイル内容を再取得
72
+ handleReload()
73
+ }
74
+
75
+ if (!fileData) {
76
+ return <div>ファイルが見つかりません</div>
77
+ }
78
+
79
+ // pathオブジェクトから実際のパス文字列を取得
80
+ const filePath = typeof fileData.path === 'string' ? fileData.path : fileData.path?.path
81
+
82
+ if (filePath?.endsWith(".md")) {
83
+ return (
84
+ <main className="p-2">
85
+ <MarkdownFileView
86
+ filePath={props.filePath}
87
+ fileData={{ path: filePath, title: fileData.content.title || null }}
88
+ cwd={directoryQuery.data.cwd}
89
+ content={currentContent}
90
+ onChange={onChange}
91
+ frontMatter={(() => {
92
+ // frontMatterはcontent内にある
93
+ const fm = fileData.content.frontMatter || {}
94
+ // スキーマやアイコンなどの複雑なオブジェクトを除外し、プリミティブ値のみを返す
95
+ const filtered: Record<
96
+ string,
97
+ string | number | boolean | string[] | number[] | boolean[] | null
98
+ > = {}
99
+ for (const [key, value] of Object.entries(fm)) {
100
+ if (key === "schema" || key === "icon") continue
101
+ if (
102
+ typeof value === "string" ||
103
+ typeof value === "number" ||
104
+ typeof value === "boolean" ||
105
+ value === null ||
106
+ (Array.isArray(value) &&
107
+ value.every(
108
+ (v) =>
109
+ typeof v === "string" ||
110
+ typeof v === "number" ||
111
+ typeof v === "boolean",
112
+ ))
113
+ ) {
114
+ filtered[key] = value as
115
+ | string
116
+ | number
117
+ | boolean
118
+ | string[]
119
+ | number[]
120
+ | boolean[]
121
+ | null
122
+ }
123
+ }
124
+ return filtered
125
+ })()}
126
+ onFrontMatterUpdate={handleFrontMatterUpdate}
127
+ onReload={handleReload}
128
+ isLoading={fileQuery.isLoading}
129
+ schema={directorySchema}
130
+ relations={relations}
131
+ />
132
+ </main>
133
+ )
134
+ }
135
+
136
+ if (filePath?.endsWith(".csv")) {
137
+ return (
138
+ <main className="p-2">
139
+ <CsvFileView
140
+ filePath={props.filePath}
141
+ fileData={{ path: filePath, title: fileData.content.title || null }}
142
+ cwd={directoryQuery.data.cwd}
143
+ content={currentContent}
144
+ onChange={onChange}
145
+ onReload={handleReload}
146
+ isLoading={fileQuery.isLoading}
147
+ />
148
+ </main>
149
+ )
150
+ }
151
+
152
+ if (filePath?.endsWith(".json")) {
153
+ return (
154
+ <main className="p-2">
155
+ <JsonFileEditor content={currentContent} />
156
+ </main>
157
+ )
158
+ }
159
+
160
+ return (
161
+ <main className="p-2">
162
+ <DefaultFileViewer content={currentContent} />
163
+ </main>
164
+ )
165
+ }
@@ -0,0 +1,157 @@
1
+ import { useEffect, useState } from "react"
2
+ import { Input } from "@/components/ui/input"
3
+ import {
4
+ Select,
5
+ SelectContent,
6
+ SelectItem,
7
+ SelectTrigger,
8
+ SelectValue,
9
+ } from "@/components/ui/select"
10
+ import type { DocRelationFile, DocSchemaField } from "@/lib/types"
11
+ import { MultiRelationSelect } from "./multi-relation-select"
12
+ import { SingleRelationSelect } from "./single-relation-select"
13
+
14
+ type Props = {
15
+ fieldKey: string
16
+ value: unknown
17
+ originalValue: unknown
18
+ onValueChange: (key: string, value: string) => void
19
+ onBlur: (key: string, value: string) => void
20
+ schemaField?: DocSchemaField
21
+ relationOptions?: DocRelationFile[]
22
+ }
23
+
24
+ /**
25
+ * Front Matter用の入力フィールドを表示するコンポーネント
26
+ */
27
+ export function FrontMatterInputField(props: Props) {
28
+ const [localValue, setLocalValue] = useState(() => {
29
+ if (Array.isArray(props.value)) {
30
+ return props.value.join(", ")
31
+ }
32
+ return String(props.value)
33
+ })
34
+
35
+ useEffect(() => {
36
+ if (Array.isArray(props.value)) {
37
+ setLocalValue(props.value.join(", "))
38
+ } else {
39
+ setLocalValue(String(props.value))
40
+ }
41
+ }, [props.value])
42
+
43
+ const _getDisplayValue = (value: unknown): string => {
44
+ if (Array.isArray(value)) {
45
+ return value.join(", ")
46
+ }
47
+ return String(value)
48
+ }
49
+
50
+ // リレーション型の場合
51
+ if (
52
+ props.schemaField &&
53
+ props.schemaField.type === "relation" &&
54
+ props.schemaField.path
55
+ ) {
56
+ return (
57
+ <SingleRelationSelect
58
+ value={(props.value as string) || ""}
59
+ relationOptions={props.relationOptions}
60
+ onValueChange={(value) => props.onValueChange(props.fieldKey, value)}
61
+ />
62
+ )
63
+ }
64
+
65
+ // 配列リレーション型の場合
66
+ if (
67
+ props.schemaField &&
68
+ props.schemaField.type === "multi-relation" &&
69
+ props.schemaField.path
70
+ ) {
71
+ // 値を配列に正規化
72
+ let normalizedValue: string[] = []
73
+ if (Array.isArray(props.value)) {
74
+ normalizedValue = props.value.filter(
75
+ (v) => typeof v === "string" && v.trim() !== "",
76
+ )
77
+ } else if (typeof props.value === "string" && props.value.trim() !== "") {
78
+ normalizedValue = props.value
79
+ .split(",")
80
+ .map((v) => v.trim())
81
+ .filter((v) => v !== "")
82
+ }
83
+
84
+ return (
85
+ <MultiRelationSelect
86
+ value={normalizedValue}
87
+ relationOptions={props.relationOptions}
88
+ onValueChange={(value) =>
89
+ props.onValueChange(props.fieldKey, value.join(", "))
90
+ }
91
+ wrap={true}
92
+ />
93
+ )
94
+ }
95
+
96
+ if (typeof props.originalValue === "boolean") {
97
+ return (
98
+ <Select
99
+ value={String(props.value)}
100
+ onValueChange={(value) => props.onValueChange(props.fieldKey, value)}
101
+ >
102
+ <SelectTrigger className="w-full">
103
+ <SelectValue />
104
+ </SelectTrigger>
105
+ <SelectContent>
106
+ <SelectItem value="true">true</SelectItem>
107
+ <SelectItem value="false">false</SelectItem>
108
+ </SelectContent>
109
+ </Select>
110
+ )
111
+ }
112
+
113
+ if (typeof props.originalValue === "number") {
114
+ return (
115
+ <Input
116
+ type="number"
117
+ value={localValue}
118
+ onChange={(e) => setLocalValue(e.target.value)}
119
+ onBlur={(e) => props.onBlur(props.fieldKey, e.target.value)}
120
+ className="flex-1"
121
+ placeholder={`${props.fieldKey}の値を入力`}
122
+ />
123
+ )
124
+ }
125
+
126
+ if (Array.isArray(props.originalValue)) {
127
+ const placeholder =
128
+ props.originalValue.length > 0 &&
129
+ typeof props.originalValue[0] === "number"
130
+ ? "カンマ区切りで数値を入力 (例: 1, 2, 3)"
131
+ : props.originalValue.length > 0 &&
132
+ typeof props.originalValue[0] === "boolean"
133
+ ? "カンマ区切りでtrue/falseを入力 (例: true, false, true)"
134
+ : "カンマ区切りで入力 (例: item1, item2, item3)"
135
+
136
+ return (
137
+ <Input
138
+ value={localValue}
139
+ onChange={(e) => setLocalValue(e.target.value)}
140
+ onBlur={(e) => props.onBlur(props.fieldKey, e.target.value)}
141
+ className="flex-1"
142
+ placeholder={placeholder}
143
+ />
144
+ )
145
+ }
146
+
147
+ // string型の場合
148
+ return (
149
+ <Input
150
+ value={localValue}
151
+ onChange={(e) => setLocalValue(e.target.value)}
152
+ onBlur={(e) => props.onBlur(props.fieldKey, e.target.value)}
153
+ className="flex-1"
154
+ placeholder={`${props.fieldKey}の値を入力`}
155
+ />
156
+ )
157
+ }
@@ -0,0 +1,30 @@
1
+ import { Card } from "@/components/ui/card"
2
+ import { RenderFrontMatterValue } from "./render-front-matter-value"
3
+
4
+ type Props = {
5
+ frontMatter: Record<string, string | string[]> | null
6
+ }
7
+
8
+ /**
9
+ * Front-matterをより見やすく表示するコンポーネント(表示のみ)
10
+ */
11
+ export function FrontMatterView(props: Props) {
12
+ const frontMatter = props.frontMatter
13
+ // front-matterが空の場合は表示しない
14
+ if (!frontMatter || Object.keys(frontMatter).length === 0) {
15
+ return null
16
+ }
17
+
18
+ return (
19
+ <Card className="gap-0 p-2">
20
+ {Object.entries(frontMatter).map(([key, value]) => (
21
+ <div key={key}>
22
+ <div className="flex gap-x-2">
23
+ <span className="font-bold">{key}</span>
24
+ <RenderFrontMatterValue value={value} />
25
+ </div>
26
+ </div>
27
+ ))}
28
+ </Card>
29
+ )
30
+ }
@@ -0,0 +1,38 @@
1
+ import { useEffect, useState } from "react"
2
+
3
+ type Props = {
4
+ content: string
5
+ }
6
+
7
+ export function JsonFileEditor(props: Props) {
8
+ const [formattedJson, setFormattedJson] = useState<string>("")
9
+ const [error, setError] = useState<string | null>(null)
10
+
11
+ useEffect(() => {
12
+ try {
13
+ const jsonData = JSON.parse(props.content)
14
+ setFormattedJson(JSON.stringify(jsonData, null, 2))
15
+ setError(null)
16
+ } catch (_e) {
17
+ setError(
18
+ "JSONのパースに失敗しました。有効なJSONではない可能性があります。",
19
+ )
20
+ setFormattedJson(props.content) // JSONのパースに失敗した場合はそのまま表示
21
+ }
22
+ }, [props.content])
23
+
24
+ return (
25
+ <div>
26
+ {error && (
27
+ <div className="mb-4 rounded border border-red-700 bg-red-900/30 p-2 text-red-300 text-sm">
28
+ {error}
29
+ </div>
30
+ )}
31
+ <div className="overflow-x-auto rounded bg-gray-800 p-4 text-white">
32
+ <pre className="whitespace-pre-wrap font-mono text-green-300">
33
+ {formattedJson}
34
+ </pre>
35
+ </div>
36
+ </div>
37
+ )
38
+ }
@@ -0,0 +1,71 @@
1
+ import { marked } from "marked"
2
+ import type { ReactNode } from "react"
3
+ import { ArchiveRestoreBanner } from "@/components/file-view/archive-restore-banner"
4
+ import { EditableFrontMatterView } from "@/components/file-view/editable-front-matter-view"
5
+ import { FileHeader } from "@/components/file-view/file-header"
6
+ import { Card } from "@/components/ui/card"
7
+ import type { DocRelation, DocSchemaRecord, DocFileMdFrontMatter } from "@/lib/types"
8
+
9
+ import "github-markdown-css"
10
+
11
+ type Props = {
12
+ filePath: string
13
+ fileData: {
14
+ path: string
15
+ title: string | null
16
+ }
17
+ cwd: string
18
+ content: string
19
+ onChange(content: string): void
20
+ frontMatter: DocFileMdFrontMatter
21
+ onFrontMatterUpdate: (key: string, value: unknown) => void
22
+ onReload: () => void
23
+ isLoading: boolean
24
+ schema?: DocSchemaRecord
25
+ relations?: DocRelation[]
26
+ }
27
+
28
+ export function MarkdownFileView(props: Props): ReactNode {
29
+ const frontMatter = props.frontMatter
30
+
31
+ const hasFrontMatter =
32
+ frontMatter !== null && Object.keys(frontMatter || {}).length > 0
33
+
34
+ const html = marked.parse(props.content)
35
+
36
+ // アーカイブファイルかどうかを判定
37
+ const isArchived = props.filePath.includes("/_/")
38
+
39
+ return (
40
+ <div className="h-full space-y-2">
41
+ {isArchived && (
42
+ <ArchiveRestoreBanner
43
+ filePath={props.filePath}
44
+ onRestore={props.onReload}
45
+ />
46
+ )}
47
+ <FileHeader
48
+ filePath={props.filePath}
49
+ fileData={props.fileData}
50
+ cwd={props.cwd}
51
+ onReload={props.onReload}
52
+ isLoading={props.isLoading}
53
+ />
54
+ {hasFrontMatter && (
55
+ <EditableFrontMatterView
56
+ frontMatter={frontMatter}
57
+ onUpdate={props.onFrontMatterUpdate}
58
+ schema={props.schema}
59
+ relations={props.relations}
60
+ />
61
+ )}
62
+ <Card className="overflow-hidden rounded-md p-0">
63
+ <div
64
+ className="markdown-body p-4"
65
+ // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>
66
+ dangerouslySetInnerHTML={{ __html: html }}
67
+ />
68
+ </Card>
69
+ </div>
70
+ )
71
+ }
@@ -0,0 +1,129 @@
1
+ import { X } from "lucide-react"
2
+ import { Button } from "@/components/ui/button"
3
+ import {
4
+ Select,
5
+ SelectContent,
6
+ SelectItem,
7
+ SelectTrigger,
8
+ SelectValue,
9
+ } from "@/components/ui/select"
10
+ import type { DocRelationFile } from "@/lib/types"
11
+
12
+ type Props = {
13
+ value: string[]
14
+ onValueChange: (value: string[]) => void
15
+ relationOptions?: DocRelationFile[]
16
+ wrap?: boolean
17
+ }
18
+
19
+ /**
20
+ * 複数リレーション用のSelectコンポーネント
21
+ */
22
+ export function MultiRelationSelect(props: Props) {
23
+ const options = props.relationOptions || []
24
+ const selectedValues = Array.isArray(props.value) ? props.value : []
25
+
26
+ // 選択済みの値を除外した利用可能なオプション
27
+ const availableOptions = options.filter(
28
+ (option) => !selectedValues.includes(option.name),
29
+ )
30
+
31
+ const addValue = (newValue: string) => {
32
+ if (newValue && !selectedValues.includes(newValue)) {
33
+ const updatedValues = [...selectedValues, newValue]
34
+ props.onValueChange(updatedValues)
35
+ }
36
+ }
37
+
38
+ const removeValue = (valueToRemove: string) => {
39
+ const updatedValues = selectedValues.filter((val) => val !== valueToRemove)
40
+ props.onValueChange(updatedValues)
41
+ }
42
+
43
+ // wrapありの場合(ファイル詳細用)
44
+ if (props.wrap) {
45
+ return (
46
+ <div className="space-y-2">
47
+ {/* 新しい値を追加するSelect */}
48
+ {availableOptions.length > 0 && (
49
+ <Select onValueChange={addValue}>
50
+ <SelectTrigger className="w-full">
51
+ <SelectValue placeholder="項目を追加" />
52
+ </SelectTrigger>
53
+ <SelectContent>
54
+ {availableOptions.map((option) => (
55
+ <SelectItem key={option.name} value={option.name}>
56
+ {option.label}
57
+ </SelectItem>
58
+ ))}
59
+ </SelectContent>
60
+ </Select>
61
+ )}
62
+ {/* 選択済みの値を表示(wrap対応) */}
63
+ <div className="flex flex-wrap gap-2">
64
+ {selectedValues.toReversed().map((value) => {
65
+ const option = options.find((opt) => opt.name === value)
66
+ const label = option?.label || value
67
+ return (
68
+ <Button
69
+ key={value}
70
+ variant={"outline"}
71
+ onClick={() => removeValue(value)}
72
+ className="h-auto break-words text-left"
73
+ >
74
+ <span className="flex-1">{label}</span>
75
+ <X className="ml-2 w-4 flex-shrink-0 text-muted-foreground" />
76
+ </Button>
77
+ )
78
+ })}
79
+ </div>
80
+ {selectedValues.length === 0 && availableOptions.length === 0 && (
81
+ <div className="text-muted-foreground text-sm">
82
+ 利用可能なオプションがありません
83
+ </div>
84
+ )}
85
+ </div>
86
+ )
87
+ }
88
+
89
+ // wrapなしの場合(テーブル用)
90
+ return (
91
+ <div className="flex gap-2">
92
+ {/* 新しい値を追加するSelect */}
93
+ {availableOptions.length > 0 && (
94
+ <Select onValueChange={addValue}>
95
+ <SelectTrigger className="min-w-24">
96
+ <SelectValue placeholder="項目を追加" />
97
+ </SelectTrigger>
98
+ <SelectContent>
99
+ {availableOptions.map((option) => (
100
+ <SelectItem key={option.name} value={option.name}>
101
+ {option.label}
102
+ </SelectItem>
103
+ ))}
104
+ </SelectContent>
105
+ </Select>
106
+ )}
107
+ {/* 選択済みの値を表示(横並び) */}
108
+ {selectedValues.toReversed().map((value) => {
109
+ const option = options.find((opt) => opt.name === value)
110
+ const label = option?.label || value
111
+ return (
112
+ <Button
113
+ key={value}
114
+ variant={"outline"}
115
+ onClick={() => removeValue(value)}
116
+ >
117
+ <span>{label}</span>
118
+ <X className="w-4 text-muted-foreground" />
119
+ </Button>
120
+ )
121
+ })}
122
+ {selectedValues.length === 0 && availableOptions.length === 0 && (
123
+ <div className="text-muted-foreground text-sm">
124
+ 利用可能なオプションがありません
125
+ </div>
126
+ )}
127
+ </div>
128
+ )
129
+ }
@@ -0,0 +1,57 @@
1
+ type Props = {
2
+ value: unknown
3
+ }
4
+
5
+ /**
6
+ * フロントマターの値を再帰的にレンダリングする
7
+ */
8
+ export function RenderFrontMatterValue(props: Props) {
9
+ const value = props.value
10
+
11
+ if (value === null || value === undefined) {
12
+ return <span>null</span>
13
+ }
14
+
15
+ if (typeof value === "boolean") {
16
+ return <span>{value.toString()}</span>
17
+ }
18
+
19
+ if (typeof value === "number") {
20
+ return <span>{value}</span>
21
+ }
22
+
23
+ if (typeof value === "string") {
24
+ return <span>"{value}"</span>
25
+ }
26
+
27
+ if (Array.isArray(value)) {
28
+ return (
29
+ <div>
30
+ {value.map((item, index) => (
31
+ <div
32
+ key={`item-${index}-${String(item).substring(0, 8)}`}
33
+ className="flex"
34
+ >
35
+ <span className="mr-2">-</span>
36
+ <RenderFrontMatterValue value={item} />
37
+ </div>
38
+ ))}
39
+ </div>
40
+ )
41
+ }
42
+
43
+ if (typeof value === "object") {
44
+ return (
45
+ <div>
46
+ {Object.entries(value as Record<string, unknown>).map(([key, val]) => (
47
+ <div key={key} className="flex">
48
+ <span className="mr-2">{key}:</span>
49
+ <RenderFrontMatterValue value={val} />
50
+ </div>
51
+ ))}
52
+ </div>
53
+ )
54
+ }
55
+
56
+ return <span>{String(value)}</span>
57
+ }
@@ -0,0 +1,41 @@
1
+ import {
2
+ Select,
3
+ SelectContent,
4
+ SelectItem,
5
+ SelectTrigger,
6
+ SelectValue,
7
+ } from "@/components/ui/select"
8
+
9
+ import type { DocRelationFile } from "@/lib/types"
10
+
11
+ type Props = {
12
+ value: string
13
+ onValueChange: (value: string) => void
14
+ relationOptions?: DocRelationFile[]
15
+ }
16
+
17
+ /**
18
+ * 単一リレーション用のSelectコンポーネント
19
+ */
20
+ export function SingleRelationSelect(props: Props) {
21
+ const options = props.relationOptions || []
22
+
23
+ const handleValueChange = (value: string) => {
24
+ props.onValueChange(value)
25
+ }
26
+
27
+ return (
28
+ <Select value={props.value || ""} onValueChange={handleValueChange}>
29
+ <SelectTrigger className="w-full bg-background">
30
+ <SelectValue placeholder="リレーションを選択" />
31
+ </SelectTrigger>
32
+ <SelectContent>
33
+ {options.map((option) => (
34
+ <SelectItem key={option.name} value={option.name}>
35
+ {option.label}
36
+ </SelectItem>
37
+ ))}
38
+ </SelectContent>
39
+ </Select>
40
+ )
41
+ }