@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,230 @@
1
+ import { useSuspenseQuery } from "@tanstack/react-query"
2
+ import { useState } from "react"
3
+ import { Button } from "@/components/ui/button"
4
+ import { useFilePropertiesMutation } from "@/hooks/use-file-properties-mutation"
5
+ import { apiClient } from "@/lib/api-client"
6
+ import type { DocDirectory, DocFile, DocFileMd, DocRelation, DocRelationFile } from "@/lib/types"
7
+ import { ProjectPageGroup } from "./project-page-group"
8
+ import { UnlinkedFeaturesSection } from "./unlinked-features-section"
9
+
10
+ type Props = {
11
+ project: string
12
+ }
13
+
14
+ type PageGroup = {
15
+ page: DocFile
16
+ features: DocFile[]
17
+ }
18
+
19
+ export function ProjectView(props: Props) {
20
+ const pagesQuery = useSuspenseQuery({
21
+ queryKey: ["apps/client", props.project],
22
+ queryFn: async () => {
23
+ const response = await apiClient.api.directories[":path{.+}"].$get({
24
+ param: { path: `${props.project}/pages` },
25
+ })
26
+ return response.json()
27
+ },
28
+ })
29
+
30
+ const featuresQuery = useSuspenseQuery({
31
+ queryKey: ["apps/client",`products/${props.project}/features`],
32
+ queryFn: async () => {
33
+ const response = await apiClient.api.directories[":path{.+}"].$get({
34
+ param: { path: `${props.project}/features` },
35
+ })
36
+ return response.json()
37
+ },
38
+ })
39
+
40
+ const pages: DocFile[] = pagesQuery.data.files || []
41
+
42
+ const features: DocFile[] = featuresQuery.data.files || []
43
+
44
+ const milestoneRelation = featuresQuery.data.relations?.find(
45
+ (rel: DocRelation) => {return rel.path === `${props.project}/milestones`},
46
+ )
47
+
48
+ const milestoneOptions = milestoneRelation?.files || []
49
+
50
+ // マイルストーンフィルター状態
51
+ const [selectedMilestone, setSelectedMilestone] = useState<string | null>(
52
+ null,
53
+ )
54
+
55
+ const updatePropertiesMutation = useFilePropertiesMutation()
56
+
57
+ const handleMilestoneUpdate = async (
58
+ featurePath: string,
59
+ milestone: string,
60
+ ) => {
61
+ try {
62
+ await updatePropertiesMutation.mutateAsync({
63
+ path: featurePath,
64
+ field: "milestone",
65
+ value: milestone,
66
+ })
67
+ await featuresQuery.refetch()
68
+ } catch (error) {
69
+ console.error("Failed to update milestone:", error)
70
+ }
71
+ }
72
+
73
+ const handlePropertyUpdate = async (
74
+ featurePath: string,
75
+ field: string,
76
+ value: unknown,
77
+ ) => {
78
+ try {
79
+ await updatePropertiesMutation.mutateAsync({
80
+ path: featurePath,
81
+ field: field,
82
+ value: value,
83
+ })
84
+ await featuresQuery.refetch()
85
+ } catch (error) {
86
+ console.error("Failed to update property:", error)
87
+ }
88
+ }
89
+
90
+ const handleFeatureAdd = async (pagePath: string, featurePath: string) => {
91
+ // ページのfeaturesフィールドに新しい機能を追加
92
+ const page = pages.find((p: DocFile) => p.path.path === pagePath)
93
+ if (!page) return
94
+
95
+ const currentFeatures =
96
+ (page.type === 'markdown'
97
+ ? (page.content.frontMatter as Record<string, unknown>)?.features as string[]
98
+ : []) || []
99
+ const updatedFeatures = [...currentFeatures, featurePath]
100
+
101
+ await updatePropertiesMutation.mutateAsync({
102
+ path: pagePath,
103
+ field: "features",
104
+ value: updatedFeatures,
105
+ })
106
+
107
+ // 両方のクエリを再取得
108
+ await Promise.all([pagesQuery.refetch(), featuresQuery.refetch()])
109
+ }
110
+
111
+ const handleFeatureRemove = async (pagePath: string, featurePath: string) => {
112
+ try {
113
+ const page = pages.find((p: DocFile) => p.path.path === pagePath)
114
+ if (!page) return
115
+
116
+ const currentFeatures =
117
+ (page.type === 'markdown'
118
+ ? (page.content.frontMatter as Record<string, unknown>)?.features as string[]
119
+ : []) || []
120
+
121
+ // フィーチャーを削除
122
+ const updatedFeatures = currentFeatures.filter((f) => f !== featurePath)
123
+
124
+ await updatePropertiesMutation.mutateAsync({
125
+ path: pagePath,
126
+ field: "features",
127
+ value: updatedFeatures,
128
+ })
129
+
130
+ await Promise.all([pagesQuery.refetch(), featuresQuery.refetch()])
131
+ } catch (error) {
132
+ console.error("Failed to remove feature from page:", error)
133
+ }
134
+ }
135
+
136
+ // フィルタリング関数
137
+ const filterFeaturesByMilestone = (features: DocFile[]) => {
138
+ if (!selectedMilestone) return features
139
+ return features.filter((feature) => {
140
+ if (feature.type !== 'markdown') return false
141
+ const milestone = (feature.content.frontMatter as Record<string, unknown>)
142
+ ?.milestone as string
143
+ return milestone === selectedMilestone
144
+ })
145
+ }
146
+
147
+ function isDocFileMdWithFeatures(file: DocFile): file is DocFileMd {
148
+ return file.type === 'markdown'
149
+ }
150
+
151
+ // ページと関連機能をグループ化
152
+ const pageGroups: PageGroup[] = pages
153
+ .filter(isDocFileMdWithFeatures)
154
+ .map((page: DocFileMd) => {
155
+ const pageFeatures = isDocFileMdWithFeatures(page)
156
+ ? ((page.content.frontMatter as Record<string, unknown>)?.features as string[]) || []
157
+ : []
158
+ const relatedFeatures = features.filter((feature: DocFile) => {
159
+ return pageFeatures.includes(feature.path.path)
160
+ })
161
+
162
+ // フィルタリング適用
163
+ const filteredFeatures = filterFeaturesByMilestone(relatedFeatures).filter((feature): feature is DocFileMd => feature.type === 'markdown')
164
+
165
+ return {
166
+ page,
167
+ features: filteredFeatures,
168
+ }
169
+ })
170
+
171
+ // 関連付けられていない機能
172
+ const allLinkedFeatures = pageGroups.flatMap((group) =>
173
+ group.features.map((f) => f.path.path),
174
+ )
175
+
176
+ const unlinkedFeatures = filterFeaturesByMilestone(
177
+ features.filter(
178
+ (feature: DocFile) => !allLinkedFeatures.includes(feature.path.path),
179
+ ),
180
+ )
181
+
182
+ return (
183
+ <main className="w-full space-y-2 p-2">
184
+ {/* マイルストーンフィルターボタン */}
185
+ <div className="flex flex-wrap gap-2">
186
+ <Button
187
+ variant={selectedMilestone === null ? "default" : "secondary"}
188
+ onClick={() => setSelectedMilestone(null)}
189
+ size="sm"
190
+ >
191
+ すべて
192
+ </Button>
193
+ {milestoneOptions.map((milestone: DocRelationFile) => (
194
+ <Button
195
+ key={milestone.name}
196
+ variant={
197
+ selectedMilestone === milestone.name ? "default" : "secondary"
198
+ }
199
+ onClick={() => setSelectedMilestone(milestone.name)}
200
+ size="sm"
201
+ >
202
+ {milestone.name}
203
+ </Button>
204
+ ))}
205
+ </div>
206
+
207
+ {/* ページグループ */}
208
+ <div className="space-y-2">
209
+ {pageGroups.map((group) => (
210
+ <ProjectPageGroup
211
+ key={group.page.path.name}
212
+ group={group}
213
+ milestoneOptions={milestoneOptions}
214
+ onMilestoneUpdate={handleMilestoneUpdate}
215
+ onPropertyUpdate={handlePropertyUpdate}
216
+ allFeatures={features}
217
+ onFeatureAdd={handleFeatureAdd}
218
+ onFeatureRemove={handleFeatureRemove}
219
+ />
220
+ ))}
221
+ <UnlinkedFeaturesSection
222
+ unlinkedFeatures={unlinkedFeatures.filter((feature): feature is DocFileMd => 'frontMatter' in feature && 'title' in feature)}
223
+ milestoneOptions={milestoneOptions}
224
+ onMilestoneUpdate={handleMilestoneUpdate}
225
+ onPropertyUpdate={handlePropertyUpdate}
226
+ />
227
+ </div>
228
+ </main>
229
+ )
230
+ }
@@ -0,0 +1,73 @@
1
+ import { Badge } from "@/components/ui/badge"
2
+ import { Card } from "@/components/ui/card"
3
+ import type { DocFile, DocFileMd, DocRelationFile } from "@/lib/types"
4
+ import { FeatureItem } from "./feature-item"
5
+
6
+ function isDocFileMd(file: DocFile): file is DocFileMd {
7
+ return file.type === 'markdown'
8
+ }
9
+
10
+ type Priority = "high" | "medium" | "low"
11
+
12
+ type Props = {
13
+ unlinkedFeatures: DocFile[]
14
+ milestoneOptions?: DocRelationFile[]
15
+ onMilestoneUpdate?: (featurePath: string, milestone: string) => void
16
+ onPropertyUpdate?: (
17
+ featurePath: string,
18
+ field: string,
19
+ value: unknown,
20
+ ) => void
21
+ }
22
+
23
+ export function UnlinkedFeaturesSection(props: Props) {
24
+ if (props.unlinkedFeatures.length === 0) {
25
+ return null
26
+ }
27
+
28
+ const sortedFeatures = props.unlinkedFeatures
29
+ .filter(isDocFileMd)
30
+ .sort((a, b) => {
31
+ const priorityOrder: Record<Priority, number> = {
32
+ high: 0,
33
+ medium: 1,
34
+ low: 2,
35
+ }
36
+ const aPriority =
37
+ ((a.content.frontMatter as Record<string, unknown>)?.priority as Priority) ||
38
+ "low"
39
+ const bPriority =
40
+ ((b.content.frontMatter as Record<string, unknown>)?.priority as Priority) ||
41
+ "low"
42
+ return (priorityOrder[aPriority] || 2) - (priorityOrder[bPriority] || 2)
43
+ })
44
+
45
+ return (
46
+ <Card className="w-full p-2">
47
+ <div className="flex gap-2">
48
+ <div className="w-1/3 border-r pr-4">
49
+ <h2 className="font-semibold text-lg">未関連付け機能</h2>
50
+ <p className="text-gray-600 text-sm">
51
+ ページに関連付けられていない機能
52
+ </p>
53
+ <Badge variant="outline" className="mt-2">
54
+ {props.unlinkedFeatures.length}機能
55
+ </Badge>
56
+ </div>
57
+ <div className="flex-1">
58
+ <div className="space-y-3">
59
+ {sortedFeatures.map((feature) => (
60
+ <FeatureItem
61
+ key={feature.path.name}
62
+ feature={feature}
63
+ milestoneOptions={props.milestoneOptions}
64
+ onMilestoneUpdate={props.onMilestoneUpdate}
65
+ onPropertyUpdate={props.onPropertyUpdate}
66
+ />
67
+ ))}
68
+ </div>
69
+ </div>
70
+ </div>
71
+ </Card>
72
+ )
73
+ }
@@ -0,0 +1,21 @@
1
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
2
+ import { useState } from "react"
3
+
4
+ export function Providers(props: { children: React.ReactNode }) {
5
+ const [queryClient] = useState(() => {
6
+ return new QueryClient({
7
+ defaultOptions: {
8
+ queries: {
9
+ staleTime: 60 * 1000, // 1分
10
+ retry: 1,
11
+ },
12
+ },
13
+ })
14
+ })
15
+
16
+ return (
17
+ <QueryClientProvider client={queryClient}>
18
+ {props.children}
19
+ </QueryClientProvider>
20
+ )
21
+ }
@@ -0,0 +1,25 @@
1
+ import { RefreshCw } from "lucide-react"
2
+ import { Button } from "./ui/button"
3
+
4
+ type Props = {
5
+ onReload: () => void
6
+ size?: "sm" | "default" | "lg" | "icon"
7
+ variant?: "default" | "secondary" | "ghost" | "outline"
8
+ disabled?: boolean
9
+ }
10
+
11
+ /**
12
+ * リロードボタンコンポーネント
13
+ */
14
+ export function ReloadButton(props: Props) {
15
+ return (
16
+ <Button
17
+ size={props.size || "sm"}
18
+ variant={props.variant || "secondary"}
19
+ onClick={props.onReload}
20
+ disabled={props.disabled}
21
+ >
22
+ <RefreshCw />
23
+ </Button>
24
+ )
25
+ }
@@ -0,0 +1,13 @@
1
+ import { PanelLeftIcon } from "lucide-react"
2
+ import { Button } from "@/components/ui/button"
3
+ import { useSidebar } from "@/components/ui/sidebar"
4
+
5
+ export function SidebarButton() {
6
+ const sidebar = useSidebar()
7
+
8
+ return (
9
+ <Button variant={"outline"} onClick={sidebar.toggleSidebar} size={"icon"}>
10
+ <PanelLeftIcon />
11
+ </Button>
12
+ )
13
+ }
@@ -0,0 +1,66 @@
1
+ import { cva, type VariantProps } from "class-variance-authority"
2
+ import type * as React from "react"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const alertVariants = cva(
7
+ "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-card text-card-foreground",
12
+ destructive:
13
+ "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ },
20
+ )
21
+
22
+ function Alert({
23
+ className,
24
+ variant,
25
+ ...props
26
+ }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
27
+ return (
28
+ <div
29
+ data-slot="alert"
30
+ role="alert"
31
+ className={cn(alertVariants({ variant }), className)}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
38
+ return (
39
+ <div
40
+ data-slot="alert-title"
41
+ className={cn(
42
+ "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
43
+ className,
44
+ )}
45
+ {...props}
46
+ />
47
+ )
48
+ }
49
+
50
+ function AlertDescription({
51
+ className,
52
+ ...props
53
+ }: React.ComponentProps<"div">) {
54
+ return (
55
+ <div
56
+ data-slot="alert-description"
57
+ className={cn(
58
+ "col-start-2 grid justify-items-start gap-1 text-muted-foreground text-sm [&_p]:leading-relaxed",
59
+ className,
60
+ )}
61
+ {...props}
62
+ />
63
+ )
64
+ }
65
+
66
+ export { Alert, AlertTitle, AlertDescription }
@@ -0,0 +1,46 @@
1
+ import { cva, type VariantProps } from "class-variance-authority"
2
+ import { Slot as SlotPrimitive } from "radix-ui"
3
+ import type * as React from "react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const badgeVariants = cva(
8
+ "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14
+ secondary:
15
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16
+ destructive:
17
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18
+ outline:
19
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ },
25
+ },
26
+ )
27
+
28
+ function Badge({
29
+ className,
30
+ variant,
31
+ asChild = false,
32
+ ...props
33
+ }: React.ComponentProps<"span"> &
34
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
35
+ const Comp = asChild ? SlotPrimitive.Slot : "span"
36
+
37
+ return (
38
+ <Comp
39
+ data-slot="badge"
40
+ className={cn(badgeVariants({ variant }), className)}
41
+ {...props}
42
+ />
43
+ )
44
+ }
45
+
46
+ export { Badge, badgeVariants }
@@ -0,0 +1,59 @@
1
+ import { cva, type VariantProps } from "class-variance-authority"
2
+ import { Slot as SlotPrimitive } from "radix-ui"
3
+ import type * as React from "react"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const buttonVariants = cva(
8
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14
+ destructive:
15
+ "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16
+ outline:
17
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18
+ secondary:
19
+ "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20
+ ghost:
21
+ "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22
+ link: "text-primary underline-offset-4 hover:underline",
23
+ },
24
+ size: {
25
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
26
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28
+ icon: "size-9",
29
+ },
30
+ },
31
+ defaultVariants: {
32
+ variant: "default",
33
+ size: "default",
34
+ },
35
+ },
36
+ )
37
+
38
+ function Button({
39
+ className,
40
+ variant,
41
+ size,
42
+ asChild = false,
43
+ ...props
44
+ }: React.ComponentProps<"button"> &
45
+ VariantProps<typeof buttonVariants> & {
46
+ asChild?: boolean
47
+ }) {
48
+ const Comp = asChild ? SlotPrimitive.Slot : "button"
49
+
50
+ return (
51
+ <Comp
52
+ data-slot="button"
53
+ className={cn(buttonVariants({ variant, size, className }))}
54
+ {...props}
55
+ />
56
+ )
57
+ }
58
+
59
+ export { Button, buttonVariants }
@@ -0,0 +1,92 @@
1
+ import type * as React from "react"
2
+
3
+ import { cn } from "@/lib/utils"
4
+
5
+ function Card({ className, ...props }: React.ComponentProps<"div">) {
6
+ return (
7
+ <div
8
+ data-slot="card"
9
+ className={cn(
10
+ "flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm",
11
+ className,
12
+ )}
13
+ {...props}
14
+ />
15
+ )
16
+ }
17
+
18
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
19
+ return (
20
+ <div
21
+ data-slot="card-header"
22
+ className={cn(
23
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
24
+ className,
25
+ )}
26
+ {...props}
27
+ />
28
+ )
29
+ }
30
+
31
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
32
+ return (
33
+ <div
34
+ data-slot="card-title"
35
+ className={cn("font-semibold leading-none", className)}
36
+ {...props}
37
+ />
38
+ )
39
+ }
40
+
41
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
42
+ return (
43
+ <div
44
+ data-slot="card-description"
45
+ className={cn("text-muted-foreground text-sm", className)}
46
+ {...props}
47
+ />
48
+ )
49
+ }
50
+
51
+ function CardAction({ className, ...props }: React.ComponentProps<"div">) {
52
+ return (
53
+ <div
54
+ data-slot="card-action"
55
+ className={cn(
56
+ "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
57
+ className,
58
+ )}
59
+ {...props}
60
+ />
61
+ )
62
+ }
63
+
64
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
65
+ return (
66
+ <div
67
+ data-slot="card-content"
68
+ className={cn("px-6", className)}
69
+ {...props}
70
+ />
71
+ )
72
+ }
73
+
74
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
75
+ return (
76
+ <div
77
+ data-slot="card-footer"
78
+ className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
79
+ {...props}
80
+ />
81
+ )
82
+ }
83
+
84
+ export {
85
+ Card,
86
+ CardHeader,
87
+ CardFooter,
88
+ CardTitle,
89
+ CardAction,
90
+ CardDescription,
91
+ CardContent,
92
+ }