@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,57 @@
1
+ import { useSortable } from "@dnd-kit/sortable"
2
+ import { CSS } from "@dnd-kit/utilities"
3
+ import { GripVertical } from "lucide-react"
4
+ import { TableCell, TableRow } from "@/components/ui/table"
5
+ import { EditableCsvCell } from "./editable-csv-cell"
6
+
7
+ type Props = {
8
+ id: string
9
+ row: string[]
10
+ rowIndex: number
11
+ onCellUpdate: (rowIndex: number, columnIndex: number, value: string) => void
12
+ }
13
+
14
+ export function SortableCsvRow(props: Props) {
15
+ const sortable = useSortable({
16
+ id: props.id,
17
+ attributes: {
18
+ role: "button",
19
+ tabIndex: 0,
20
+ },
21
+ })
22
+
23
+ const style = {
24
+ transform: CSS.Transform.toString(sortable.transform),
25
+ opacity: sortable.isDragging ? 0.5 : 1,
26
+ touchAction: "none",
27
+ }
28
+
29
+ function handleCellChange(
30
+ rowIndex: number,
31
+ columnIndex: number,
32
+ value: string,
33
+ ) {
34
+ props.onCellUpdate(rowIndex, columnIndex, value)
35
+ }
36
+
37
+ return (
38
+ <TableRow
39
+ ref={sortable.setNodeRef}
40
+ style={style}
41
+ className="hover:opacity-60"
42
+ >
43
+ <TableCell {...sortable.attributes} {...sortable.listeners}>
44
+ <GripVertical className="size-4" />
45
+ </TableCell>
46
+ {props.row.map((cell: string, cellIndex: number) => (
47
+ <EditableCsvCell
48
+ key={`cell-${props.rowIndex}-${cellIndex}-${cell}`}
49
+ content={cell}
50
+ rowIndex={props.rowIndex}
51
+ columnIndex={cellIndex}
52
+ onCellChange={handleCellChange}
53
+ />
54
+ ))}
55
+ </TableRow>
56
+ )
57
+ }
@@ -0,0 +1,30 @@
1
+ import { useSortable } from "@dnd-kit/sortable"
2
+ import { CSS } from "@dnd-kit/utilities"
3
+
4
+ type Props = {
5
+ id: string
6
+ index: number
7
+ children: React.ReactNode
8
+ }
9
+
10
+ export function CsvSortableHeader({ id, index, children }: Props) {
11
+ const { attributes, listeners, setNodeRef, transform, transition } =
12
+ useSortable({ id })
13
+
14
+ const style = {
15
+ transform: CSS.Transform.toString(transform),
16
+ transition,
17
+ }
18
+
19
+ return (
20
+ <th
21
+ ref={setNodeRef}
22
+ style={style}
23
+ {...attributes}
24
+ {...listeners}
25
+ className="cursor-grab border border-gray-600 p-2 text-sm"
26
+ >
27
+ {children}
28
+ </th>
29
+ )
30
+ }
@@ -0,0 +1,8 @@
1
+ export function generateUniqueId(
2
+ prefix: string,
3
+ value: string,
4
+ index: number,
5
+ ): string {
6
+ // 単純な値とインデックスの組み合わせ
7
+ return `${prefix}-${value.slice(0, 10).replace(/\s+/g, "-")}-${index}`
8
+ }
@@ -0,0 +1,95 @@
1
+ import { useSuspenseQuery } from "@tanstack/react-query"
2
+ import { Card } from "@/components/ui/card"
3
+ import { apiClient } from "@/lib/api-client"
4
+ import type { DocFile, DocFileMd } from "@/lib/types"
5
+
6
+ type Props = {
7
+ project: string
8
+ }
9
+
10
+ export function MilestonesEditor(props: Props) {
11
+ const { data: milestonesData } = useSuspenseQuery({
12
+ queryKey: ["directories", `products/${props.project}/milestones`],
13
+ queryFn: async () => {
14
+ const response = await apiClient.api.directories[":path{.+}"].$get({
15
+ param: { path: `products/${props.project}/milestones` },
16
+ })
17
+ if (!response.ok) {
18
+ throw new Error("Failed to fetch milestones")
19
+ }
20
+ return response.json()
21
+ },
22
+ })
23
+
24
+ const { data: featuresData } = useSuspenseQuery({
25
+ queryKey: ["directories", `products/${props.project}/features`],
26
+ queryFn: async () => {
27
+ const response = await apiClient.api.directories[":path{.+}"].$get({
28
+ param: { path: `products/${props.project}/features` },
29
+ })
30
+ if (!response.ok) {
31
+ throw new Error("Failed to fetch features")
32
+ }
33
+ return response.json()
34
+ },
35
+ })
36
+
37
+ function isDocFileMd(file: DocFile): file is DocFileMd {
38
+ return file.type === 'markdown'
39
+ }
40
+
41
+ function hasMarkdownContent(file: { content: unknown }): file is { content: { title: string; description: string; frontMatter: Record<string, unknown> } } {
42
+ return typeof file.content === 'object' && file.content !== null && 'frontMatter' in file.content
43
+ }
44
+
45
+ const milestones = (milestonesData.files || []).filter(isDocFileMd)
46
+ const features = (featuresData.files || []).filter(isDocFileMd)
47
+
48
+ function getFeaturesByMilestone(milestoneId: string) {
49
+ return features.filter(
50
+ (feature) => {
51
+ if (!hasMarkdownContent(feature)) return false
52
+ const frontMatter = feature.content.frontMatter
53
+ return typeof frontMatter === 'object' && frontMatter !== null && 'milestone' in frontMatter && frontMatter.milestone === milestoneId
54
+ }
55
+ )
56
+ }
57
+
58
+ return (
59
+ <div className="grid gap-2">
60
+ {milestones.map((milestone) => {
61
+ const milestoneFeatures = getFeaturesByMilestone(milestone.path.name)
62
+ return (
63
+ <Card key={milestone.path.name} className="gap-2 overflow-hidden p-2">
64
+ <div className="space-y-1">
65
+ <h2 className="font-semibold text-xl">
66
+ {hasMarkdownContent(milestone) ? milestone.content.title || milestone.path.name : milestone.path.name}
67
+ </h2>
68
+ <p className="text-muted-foreground text-sm">
69
+ {hasMarkdownContent(milestone) ? milestone.content.description : ''}
70
+ </p>
71
+ </div>
72
+ {milestoneFeatures.length > 0 ? (
73
+ <div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
74
+ {milestoneFeatures.map((feature) => (
75
+ <div key={feature.path.name} className="rounded border p-2">
76
+ <h3 className="font-medium">
77
+ {hasMarkdownContent(feature) ? feature.content.title || feature.path.name : feature.path.name}
78
+ </h3>
79
+ <p className="text-muted-foreground text-sm">
80
+ {hasMarkdownContent(feature) ? feature.content.description : ''}
81
+ </p>
82
+ </div>
83
+ ))}
84
+ </div>
85
+ ) : (
86
+ <p className="py-4 text-center text-muted-foreground">
87
+ このマイルストーンに関連する機能はありません
88
+ </p>
89
+ )}
90
+ </Card>
91
+ )
92
+ })}
93
+ </div>
94
+ )
95
+ }
@@ -0,0 +1,20 @@
1
+ import type { ReactNode } from "react"
2
+ import { SidebarButton } from "@/components/sidebar-button"
3
+ import { Input } from "@/components/ui/input"
4
+
5
+ type Props = {
6
+ filePath: string
7
+ }
8
+
9
+ export function PageHeader(props: Props): ReactNode {
10
+ const paths = props.filePath.split("/")
11
+
12
+ const fileName = paths[paths.length - 1]
13
+
14
+ return (
15
+ <div className="flex flex-1 items-center gap-2">
16
+ <SidebarButton />
17
+ <Input value={fileName} readOnly />
18
+ </div>
19
+ )
20
+ }
@@ -0,0 +1,139 @@
1
+ import { CheckCircle, X } from "lucide-react"
2
+ import { SingleRelationSelect } from "@/components/file-view/single-relation-select"
3
+ import { Button } from "@/components/ui/button"
4
+ import type { DocFile, DocFileMd, DocRelationFile } from "@/lib/types"
5
+ import { cn } from "@/lib/utils"
6
+
7
+ function isDocFileMd(file: DocFile): file is DocFileMd {
8
+ return file.type === 'markdown'
9
+ }
10
+
11
+ type Priority = "high" | "medium" | "low"
12
+
13
+ type Props = {
14
+ feature: DocFile
15
+ milestoneOptions?: DocRelationFile[]
16
+ onMilestoneUpdate?: (featurePath: string, milestone: string) => void
17
+ onPropertyUpdate?: (
18
+ featurePath: string,
19
+ field: string,
20
+ value: unknown,
21
+ ) => void
22
+ onFeatureRemove?: (featurePath: string) => void
23
+ }
24
+
25
+ export function FeatureItem(props: Props) {
26
+ if (!isDocFileMd(props.feature)) {
27
+ return null
28
+ }
29
+
30
+ const frontMatter = props.feature.content.frontMatter as Record<string, unknown>
31
+
32
+ const isDone = (frontMatter?.["is-done"] as boolean) === true
33
+
34
+ const priorityNumber = (frontMatter?.priority as number) || 0
35
+
36
+ const priority =
37
+ priorityNumber === 2 ? "high" : priorityNumber === 1 ? "medium" : "low"
38
+
39
+ const milestone = frontMatter?.milestone as string
40
+
41
+ const getCurrentValue = () => {
42
+ const matchingOption = props.milestoneOptions?.find(
43
+ (option) => option.label === milestone,
44
+ )
45
+ return matchingOption?.name || ""
46
+ }
47
+
48
+ const handleMilestoneChange = (value: string) => {
49
+ if (!value) {
50
+ props.onMilestoneUpdate?.(props.feature.path.path, "")
51
+ return
52
+ }
53
+
54
+ const selectedOption = props.milestoneOptions?.find(
55
+ (option) => option.name === value,
56
+ )
57
+ const labelToSave = selectedOption?.label || value
58
+ props.onMilestoneUpdate?.(props.feature.path.path, labelToSave)
59
+ }
60
+
61
+ const handleDoneToggle = () => {
62
+ props.onPropertyUpdate?.(props.feature.path.path, "is-done", !isDone)
63
+ }
64
+
65
+ const handlePriorityClick = () => {
66
+ const currentValue = (frontMatter?.priority as number) || 0
67
+ const nextValue = (currentValue + 1) % 3
68
+ props.onPropertyUpdate?.(props.feature.path.path, "priority", nextValue)
69
+ }
70
+
71
+ const getPriorityColor = (priority: Priority): string => {
72
+ switch (priority) {
73
+ case "high":
74
+ return "border-red-500"
75
+ case "medium":
76
+ return "border-yellow-500"
77
+ case "low":
78
+ return "border-green-500"
79
+ default:
80
+ return "border-gray-500"
81
+ }
82
+ }
83
+
84
+ const getPriorityLabel = (priority: Priority): string => {
85
+ if (priority === "high") return "高"
86
+ if (priority === "medium") return "中"
87
+ return "低"
88
+ }
89
+
90
+ return (
91
+ <div
92
+ className={cn("space-y-2 rounded-md border p-2", isDone && "opacity-75")}
93
+ >
94
+ <div className="flex items-center gap-3">
95
+ <Button
96
+ variant={isDone ? "default" : "secondary"}
97
+ size="sm"
98
+ onClick={handleDoneToggle}
99
+ >
100
+ <CheckCircle className="size-4" />
101
+ </Button>
102
+ <h3 className={`font-bold ${isDone ? "line-through opacity-80" : ""}`}>
103
+ {props.feature.content.title || props.feature.path.name}
104
+ </h3>
105
+ </div>
106
+ {props.feature.content.description && (
107
+ <p className="text-sm opacity-80">{props.feature.content.description}</p>
108
+ )}
109
+ <div className="flex flex-col items-end justify-between gap-2 lg:flex-row">
110
+ <div className="text-xs opacity-50">{props.feature.path.name}</div>
111
+ <div className="flex gap-2">
112
+ <SingleRelationSelect
113
+ value={getCurrentValue()}
114
+ relationOptions={props.milestoneOptions}
115
+ onValueChange={handleMilestoneChange}
116
+ />
117
+ <Button
118
+ variant="outline"
119
+ className={cn("w-8", getPriorityColor(priority))}
120
+ onClick={handlePriorityClick}
121
+ >
122
+ {getPriorityLabel(priority)}
123
+ </Button>
124
+ {props.onFeatureRemove && (
125
+ <Button
126
+ variant="outline"
127
+ className="w-8"
128
+ onClick={() => {
129
+ props.onFeatureRemove?.(props.feature.path.path)
130
+ }}
131
+ >
132
+ <X className="size-3" />
133
+ </Button>
134
+ )}
135
+ </div>
136
+ </div>
137
+ </div>
138
+ )
139
+ }
@@ -0,0 +1,125 @@
1
+ import { Card } from "@/components/ui/card"
2
+ import {
3
+ Select,
4
+ SelectContent,
5
+ SelectItem,
6
+ SelectTrigger,
7
+ SelectValue,
8
+ } from "@/components/ui/select"
9
+ import type { DocFile, DocFileMd, DocRelationFile } from "@/lib/types"
10
+ import { FeatureItem } from "./feature-item"
11
+
12
+ function isDocFileMd(file: DocFile): file is DocFileMd {
13
+ return file.type === 'markdown'
14
+ }
15
+
16
+ type Priority = "high" | "medium" | "low"
17
+
18
+ type PageGroup = {
19
+ page: DocFile
20
+ features: DocFile[]
21
+ }
22
+
23
+ type Props = {
24
+ group: PageGroup
25
+ milestoneOptions?: DocRelationFile[]
26
+ onMilestoneUpdate?: (featurePath: string, milestone: string) => void
27
+ onPropertyUpdate?: (
28
+ featurePath: string,
29
+ field: string,
30
+ value: unknown,
31
+ ) => void
32
+ allFeatures?: DocFile[]
33
+ onFeatureAdd?: (pagePath: string, featurePath: string) => void
34
+ onFeatureRemove?: (pagePath: string, featurePath: string) => void
35
+ }
36
+
37
+ export function ProjectPageGroup(props: Props) {
38
+ const sortedFeatures = props.group.features
39
+ .filter(isDocFileMd)
40
+ .sort((a, b) => {
41
+ const priorityOrder: Record<Priority, number> = {
42
+ high: 0,
43
+ medium: 1,
44
+ low: 2,
45
+ }
46
+ const aPriority =
47
+ ((a.content.frontMatter as Record<string, unknown>)?.priority as Priority) ||
48
+ "low"
49
+ const bPriority =
50
+ ((b.content.frontMatter as Record<string, unknown>)?.priority as Priority) ||
51
+ "low"
52
+ return (priorityOrder[aPriority] || 2) - (priorityOrder[bPriority] || 2)
53
+ })
54
+
55
+ // このページに関連付けられていない機能を取得
56
+ const linkedFeaturePaths = props.group.features.map((f) => f.path.path)
57
+ const availableFeatures = (props.allFeatures || [])
58
+ .filter(isDocFileMd)
59
+ .filter((feature) => !linkedFeaturePaths.includes(feature.path.path))
60
+
61
+ const handleFeatureAdd = (featureValue: string) => {
62
+ if (featureValue && props.onFeatureAdd) {
63
+ props.onFeatureAdd(props.group.page.path.path, featureValue)
64
+ }
65
+ }
66
+
67
+ return (
68
+ <Card className="w-full rounded-md p-2">
69
+ <div className="flex flex-col gap-2 lg:flex-row">
70
+ {/* 左側: ページ情報 */}
71
+ <div className="space-y-2 lg:w-1/3">
72
+ <h2 className="font-semibold text-lg">
73
+ {isDocFileMd(props.group.page) ? props.group.page.content.title || props.group.page.path.name : props.group.page.path.name}
74
+ </h2>
75
+ <div className="text-xs opacity-50">{props.group.page.path.name}</div>
76
+ {isDocFileMd(props.group.page) && props.group.page.content.description && (
77
+ <p className="text-sm opacity-50">{props.group.page.content.description}</p>
78
+ )}
79
+ </div>
80
+ {/* 右側: 関連機能一覧 */}
81
+ <div className="flex-1">
82
+ <div className="space-y-2">
83
+ {sortedFeatures.length > 0 ? (
84
+ sortedFeatures.map((feature) => (
85
+ <FeatureItem
86
+ key={feature.path.name}
87
+ feature={feature}
88
+ milestoneOptions={props.milestoneOptions}
89
+ onMilestoneUpdate={props.onMilestoneUpdate}
90
+ onPropertyUpdate={props.onPropertyUpdate}
91
+ onFeatureRemove={(featurePath) =>
92
+ props.onFeatureRemove?.(
93
+ props.group.page.path.path,
94
+ featurePath,
95
+ )
96
+ }
97
+ />
98
+ ))
99
+ ) : (
100
+ <p className="text-sm italic opacity-50">
101
+ このページに関連する機能はありません
102
+ </p>
103
+ )}
104
+
105
+ {/* 機能追加用Select */}
106
+ {availableFeatures.length > 0 && (
107
+ <Select value="" onValueChange={handleFeatureAdd}>
108
+ <SelectTrigger className="w-full">
109
+ <SelectValue placeholder="機能を追加..." />
110
+ </SelectTrigger>
111
+ <SelectContent>
112
+ {availableFeatures.map((feature) => (
113
+ <SelectItem key={feature.path.path} value={feature.path.path || ""}>
114
+ {feature.content.title || feature.path.name}
115
+ </SelectItem>
116
+ ))}
117
+ </SelectContent>
118
+ </Select>
119
+ )}
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </Card>
124
+ )
125
+ }