@tanstack/cta-ui-base 0.15.4

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 (172) hide show
  1. package/LICENSE +21 -0
  2. package/components.json +21 -0
  3. package/dist/app.d.ts +1 -0
  4. package/dist/app.js +10 -0
  5. package/dist/components/add-on-info-dialog.d.ts +5 -0
  6. package/dist/components/add-on-info-dialog.js +5 -0
  7. package/dist/components/background-animation.d.ts +1 -0
  8. package/dist/components/background-animation.js +144 -0
  9. package/dist/components/cta-provider.d.ts +3 -0
  10. package/dist/components/cta-provider.js +11 -0
  11. package/dist/components/cta-sidebar.d.ts +1 -0
  12. package/dist/components/cta-sidebar.js +15 -0
  13. package/dist/components/custom-add-on-dialog.d.ts +1 -0
  14. package/dist/components/custom-add-on-dialog.js +38 -0
  15. package/dist/components/file-navigator.d.ts +2 -0
  16. package/dist/components/file-navigator.js +86 -0
  17. package/dist/components/file-tree.d.ts +5 -0
  18. package/dist/components/file-tree.js +14 -0
  19. package/dist/components/file-viewer.d.ts +5 -0
  20. package/dist/components/file-viewer.js +40 -0
  21. package/dist/components/header.d.ts +1 -0
  22. package/dist/components/header.js +5 -0
  23. package/dist/components/icons/tailwind.d.ts +3 -0
  24. package/dist/components/icons/tailwind.js +5 -0
  25. package/dist/components/icons/tanstack.d.ts +3 -0
  26. package/dist/components/icons/tanstack.js +5 -0
  27. package/dist/components/icons/typescript.d.ts +3 -0
  28. package/dist/components/icons/typescript.js +5 -0
  29. package/dist/components/query-provider.d.ts +3 -0
  30. package/dist/components/query-provider.js +7 -0
  31. package/dist/components/sidebar-items/add-ons.d.ts +1 -0
  32. package/dist/components/sidebar-items/add-ons.js +27 -0
  33. package/dist/components/sidebar-items/mode-selector.d.ts +1 -0
  34. package/dist/components/sidebar-items/mode-selector.js +19 -0
  35. package/dist/components/sidebar-items/project-name.d.ts +1 -0
  36. package/dist/components/sidebar-items/project-name.js +12 -0
  37. package/dist/components/sidebar-items/run-add-ons.d.ts +1 -0
  38. package/dist/components/sidebar-items/run-add-ons.js +25 -0
  39. package/dist/components/sidebar-items/run-create-app.d.ts +1 -0
  40. package/dist/components/sidebar-items/run-create-app.js +28 -0
  41. package/dist/components/sidebar-items/sidebar-container.d.ts +3 -0
  42. package/dist/components/sidebar-items/sidebar-container.js +4 -0
  43. package/dist/components/sidebar-items/sidebar-group.d.ts +3 -0
  44. package/dist/components/sidebar-items/sidebar-group.js +4 -0
  45. package/dist/components/sidebar-items/starter.d.ts +1 -0
  46. package/dist/components/sidebar-items/starter.js +42 -0
  47. package/dist/components/sidebar-items/typescript-switch.d.ts +1 -0
  48. package/dist/components/sidebar-items/typescript-switch.js +18 -0
  49. package/dist/components/starters-carousel.d.ts +3 -0
  50. package/dist/components/starters-carousel.js +12 -0
  51. package/dist/components/startup-dialog.d.ts +1 -0
  52. package/dist/components/startup-dialog.js +30 -0
  53. package/dist/components/status-list.d.ts +5 -0
  54. package/dist/components/status-list.js +4 -0
  55. package/dist/components/toaster.d.ts +4 -0
  56. package/dist/components/toaster.js +15 -0
  57. package/dist/components/ui/button.d.ts +10 -0
  58. package/dist/components/ui/button.js +32 -0
  59. package/dist/components/ui/carousel.d.ts +20 -0
  60. package/dist/components/ui/carousel.js +90 -0
  61. package/dist/components/ui/checkbox.d.ts +4 -0
  62. package/dist/components/ui/checkbox.js +9 -0
  63. package/dist/components/ui/dialog.d.ts +15 -0
  64. package/dist/components/ui/dialog.js +36 -0
  65. package/dist/components/ui/dropdown-menu.d.ts +25 -0
  66. package/dist/components/ui/dropdown-menu.js +51 -0
  67. package/dist/components/ui/input.d.ts +3 -0
  68. package/dist/components/ui/input.js +7 -0
  69. package/dist/components/ui/label.d.ts +4 -0
  70. package/dist/components/ui/label.js +8 -0
  71. package/dist/components/ui/popover.d.ts +7 -0
  72. package/dist/components/ui/popover.js +17 -0
  73. package/dist/components/ui/separator.d.ts +4 -0
  74. package/dist/components/ui/separator.js +9 -0
  75. package/dist/components/ui/sheet.d.ts +13 -0
  76. package/dist/components/ui/sheet.js +40 -0
  77. package/dist/components/ui/skeleton.d.ts +2 -0
  78. package/dist/components/ui/skeleton.js +6 -0
  79. package/dist/components/ui/sonner.d.ts +3 -0
  80. package/dist/components/ui/sonner.js +12 -0
  81. package/dist/components/ui/switch.d.ts +4 -0
  82. package/dist/components/ui/switch.js +8 -0
  83. package/dist/components/ui/table.d.ts +10 -0
  84. package/dist/components/ui/table.js +28 -0
  85. package/dist/components/ui/tabs.d.ts +7 -0
  86. package/dist/components/ui/tabs.js +17 -0
  87. package/dist/components/ui/toggle-group.d.ts +7 -0
  88. package/dist/components/ui/toggle-group.js +20 -0
  89. package/dist/components/ui/toggle.d.ts +9 -0
  90. package/dist/components/ui/toggle.js +27 -0
  91. package/dist/components/ui/tooltip.d.ts +7 -0
  92. package/dist/components/ui/tooltip.js +18 -0
  93. package/dist/components/ui/tree-view.d.ts +25 -0
  94. package/dist/components/ui/tree-view.js +151 -0
  95. package/dist/file-classes.d.ts +8 -0
  96. package/dist/file-classes.js +41 -0
  97. package/dist/hooks/use-mounted.d.ts +1 -0
  98. package/dist/hooks/use-mounted.js +8 -0
  99. package/dist/hooks/use-preferred-reduced-motion.d.ts +5 -0
  100. package/dist/hooks/use-preferred-reduced-motion.js +20 -0
  101. package/dist/hooks/use-streaming-status.d.ts +6 -0
  102. package/dist/hooks/use-streaming-status.js +55 -0
  103. package/dist/index.d.ts +20 -0
  104. package/dist/index.js +20 -0
  105. package/dist/lib/api.d.ts +14 -0
  106. package/dist/lib/api.js +74 -0
  107. package/dist/lib/utils.d.ts +2 -0
  108. package/dist/lib/utils.js +5 -0
  109. package/dist/store/add-ons.d.ts +7 -0
  110. package/dist/store/add-ons.js +59 -0
  111. package/dist/store/project.d.ts +76 -0
  112. package/dist/store/project.js +269 -0
  113. package/package.json +50 -0
  114. package/src/app.tsx +28 -0
  115. package/src/components/add-on-info-dialog.tsx +39 -0
  116. package/src/components/background-animation.tsx +224 -0
  117. package/src/components/cta-provider.tsx +22 -0
  118. package/src/components/cta-sidebar.tsx +43 -0
  119. package/src/components/custom-add-on-dialog.tsx +79 -0
  120. package/src/components/file-navigator.tsx +207 -0
  121. package/src/components/file-tree.tsx +35 -0
  122. package/src/components/file-viewer.tsx +67 -0
  123. package/src/components/header.tsx +29 -0
  124. package/src/components/icons/tailwind.tsx +26 -0
  125. package/src/components/icons/tanstack.tsx +338 -0
  126. package/src/components/icons/typescript.tsx +23 -0
  127. package/src/components/query-provider.tsx +10 -0
  128. package/src/components/sidebar-items/add-ons.tsx +94 -0
  129. package/src/components/sidebar-items/mode-selector.tsx +56 -0
  130. package/src/components/sidebar-items/project-name.tsx +32 -0
  131. package/src/components/sidebar-items/run-add-ons.tsx +71 -0
  132. package/src/components/sidebar-items/run-create-app.tsx +82 -0
  133. package/src/components/sidebar-items/sidebar-container.tsx +11 -0
  134. package/src/components/sidebar-items/sidebar-group.tsx +11 -0
  135. package/src/components/sidebar-items/starter.tsx +123 -0
  136. package/src/components/sidebar-items/typescript-switch.tsx +58 -0
  137. package/src/components/starters-carousel.tsx +41 -0
  138. package/src/components/startup-dialog.tsx +72 -0
  139. package/src/components/status-list.tsx +22 -0
  140. package/src/components/toaster.tsx +29 -0
  141. package/src/components/ui/button.tsx +61 -0
  142. package/src/components/ui/carousel.tsx +239 -0
  143. package/src/components/ui/checkbox.tsx +30 -0
  144. package/src/components/ui/dialog.tsx +138 -0
  145. package/src/components/ui/dropdown-menu.tsx +255 -0
  146. package/src/components/ui/input.tsx +21 -0
  147. package/src/components/ui/label.tsx +22 -0
  148. package/src/components/ui/popover.tsx +46 -0
  149. package/src/components/ui/separator.tsx +28 -0
  150. package/src/components/ui/sheet.tsx +137 -0
  151. package/src/components/ui/skeleton.tsx +13 -0
  152. package/src/components/ui/sonner.tsx +24 -0
  153. package/src/components/ui/switch.tsx +29 -0
  154. package/src/components/ui/table.tsx +114 -0
  155. package/src/components/ui/tabs.tsx +64 -0
  156. package/src/components/ui/toggle-group.tsx +72 -0
  157. package/src/components/ui/toggle.tsx +49 -0
  158. package/src/components/ui/tooltip.tsx +61 -0
  159. package/src/components/ui/tree-view.tsx +497 -0
  160. package/src/file-classes.ts +54 -0
  161. package/src/hooks/use-mounted.ts +9 -0
  162. package/src/hooks/use-preferred-reduced-motion.ts +27 -0
  163. package/src/hooks/use-streaming-status.ts +70 -0
  164. package/src/index.ts +44 -0
  165. package/src/lib/api.ts +100 -0
  166. package/src/lib/utils.ts +8 -0
  167. package/src/store/add-ons.ts +81 -0
  168. package/src/store/project.ts +345 -0
  169. package/src/types.d.ts +109 -0
  170. package/tests/store/add-ons.test.ts +222 -0
  171. package/tsconfig.json +25 -0
  172. package/vitest.config.ts +6 -0
package/src/app.tsx ADDED
@@ -0,0 +1,28 @@
1
+ import { AppSidebar } from './components/cta-sidebar'
2
+ import { AppHeader } from './components/header'
3
+ import { BackgroundAnimation } from './components/background-animation'
4
+ import FileNavigator from './components/file-navigator'
5
+ import StartupDialog from './components/startup-dialog'
6
+ import { CTAProvider } from './components/cta-provider'
7
+
8
+ export default function RootComponent() {
9
+ return (
10
+ <CTAProvider>
11
+ <main className="min-w-[1280px]">
12
+ <BackgroundAnimation />
13
+ <div className="min-h-dvh p-2 sm:p-4 space-y-2 sm:space-y-4 @container">
14
+ <AppHeader />
15
+ <div className="flex flex-row">
16
+ <div className="w-1/3 @8xl:w-1/4 pr-2">
17
+ <AppSidebar />
18
+ </div>
19
+ <div className="w-2/3 @8xl:w-3/4 pl-2">
20
+ <FileNavigator />
21
+ </div>
22
+ </div>
23
+ </div>
24
+ <StartupDialog />
25
+ </main>
26
+ </CTAProvider>
27
+ )
28
+ }
@@ -0,0 +1,39 @@
1
+ import { Dialog, DialogContent } from './ui/dialog'
2
+
3
+ import type { AddOnInfo } from '../types'
4
+
5
+ export default function CustomAddOnDialog({
6
+ addOn,
7
+ onClose,
8
+ }: {
9
+ addOn?: AddOnInfo
10
+ onClose: () => void
11
+ }) {
12
+ return (
13
+ <Dialog modal open={!!addOn} onOpenChange={onClose}>
14
+ <DialogContent className="sm:min-w-[425px] sm:max-w-fit">
15
+ <div className="flex flex-row">
16
+ {addOn?.smallLogo && (
17
+ <img
18
+ src={`data:image/svg+xml,${encodeURIComponent(addOn.smallLogo)}`}
19
+ alt={addOn.name}
20
+ className="w-15"
21
+ />
22
+ )}
23
+ <div className="flex flex-col ml-4 gap-4">
24
+ <p className="text-lg font-bold">{addOn?.name}</p>
25
+ <p className="text-sm text-gray-500">{addOn?.description}</p>
26
+ <a
27
+ href={addOn?.link}
28
+ target="_blank"
29
+ rel="noopener noreferrer"
30
+ className="text-sm text-blue-500 underline"
31
+ >
32
+ More information on {addOn?.name}
33
+ </a>
34
+ </div>
35
+ </div>
36
+ </DialogContent>
37
+ </Dialog>
38
+ )
39
+ }
@@ -0,0 +1,224 @@
1
+ import * as React from 'react'
2
+ import { twMerge } from 'tailwind-merge'
3
+
4
+ import { useMounted } from '../hooks/use-mounted'
5
+ import { usePrefersReducedMotion } from '../hooks/use-preferred-reduced-motion'
6
+
7
+ export function BackgroundAnimation() {
8
+ const canvasRef = React.useRef<HTMLCanvasElement>(null)
9
+ const prefersReducedMotion = usePrefersReducedMotion()
10
+ const mounted = useMounted()
11
+ const isHomePage = false
12
+
13
+ React.useEffect(() => {
14
+ if (prefersReducedMotion !== false) {
15
+ return
16
+ }
17
+
18
+ const canvas = canvasRef.current
19
+
20
+ let morphDuration = 2000
21
+ const waitDuration = 1000 * 60 * 2
22
+
23
+ const easingFn = cubicBezier(0.645, 0.045, 0.355, 1.0)
24
+
25
+ if (canvas) {
26
+ const ctx = canvas.getContext('2d')!
27
+
28
+ let rafId: ReturnType<typeof requestAnimationFrame> | null = null
29
+ let timeout: ReturnType<typeof setTimeout> | null = null
30
+ let startTime = performance.now()
31
+
32
+ function createBlobs() {
33
+ return shuffle([
34
+ {
35
+ color: { h: 10, s: 100, l: 50 },
36
+ },
37
+ {
38
+ color: { h: 40, s: 100, l: 50 },
39
+ },
40
+ {
41
+ color: { h: 150, s: 100, l: 50 },
42
+ },
43
+ {
44
+ color: { h: 200, s: 100, l: 50 },
45
+ },
46
+ ]).map((blob) => ({
47
+ ...blob,
48
+ x: Math.random() * canvas!.width,
49
+ y: Math.random() * canvas!.height,
50
+ r: Math.random() * 500 + 700,
51
+ colorH: blob.color.h,
52
+ colorS: blob.color.s,
53
+ colorL: blob.color.l,
54
+ }))
55
+ }
56
+
57
+ function shuffle<T>(array: T[]) {
58
+ for (let i = array.length - 1; i > 0; i--) {
59
+ const j = Math.floor(Math.random() * (i + 1))
60
+ ;[array[i], array[j]] = [array[j], array[i]]
61
+ }
62
+ return array
63
+ }
64
+
65
+ let startBlobs = createBlobs()
66
+ let currentBlobs = startBlobs
67
+ let targetBlobs: ReturnType<typeof createBlobs> = []
68
+
69
+ function resizeHandler() {
70
+ // Create an offscreen canvas and copy the current content
71
+ const offscreen = document.createElement('canvas')
72
+ offscreen.width = canvas!.width
73
+ offscreen.height = canvas!.height
74
+ offscreen.getContext('2d')!.drawImage(canvas!, 0, 0)
75
+
76
+ // Resize the main canvas
77
+ canvas!.width = window.innerWidth
78
+ canvas!.height = window.innerHeight
79
+
80
+ // Stretch and redraw the saved content to fill the new size
81
+ ctx.drawImage(offscreen, 0, 0, canvas!.width, canvas!.height)
82
+ }
83
+
84
+ function start() {
85
+ if (timeout) {
86
+ clearTimeout(timeout)
87
+ }
88
+ if (rafId) {
89
+ cancelAnimationFrame(rafId)
90
+ }
91
+
92
+ startBlobs = JSON.parse(JSON.stringify(currentBlobs))
93
+ targetBlobs = createBlobs()
94
+ startTime = performance.now()
95
+ animate()
96
+ }
97
+
98
+ function animate() {
99
+ ctx.clearRect(0, 0, canvas!.width, canvas!.height)
100
+
101
+ const time = performance.now() - startTime
102
+ const progress = time / morphDuration
103
+ const easedProgress = easingFn(progress)
104
+
105
+ // Draw the blobs
106
+ startBlobs.forEach((startBlob, i) => {
107
+ const targetBlob = targetBlobs[i]
108
+
109
+ currentBlobs[i].x = interpolate(
110
+ startBlob.x,
111
+ targetBlob.x,
112
+ easedProgress,
113
+ )
114
+ currentBlobs[i].y = interpolate(
115
+ startBlob.y,
116
+ targetBlob.y,
117
+ easedProgress,
118
+ )
119
+
120
+ const gradient = ctx.createRadialGradient(
121
+ currentBlobs[i].x,
122
+ currentBlobs[i].y,
123
+ 0,
124
+ currentBlobs[i].x,
125
+ currentBlobs[i].y,
126
+ currentBlobs[i].r,
127
+ )
128
+
129
+ currentBlobs[i].colorH = interpolate(
130
+ startBlob.colorH,
131
+ targetBlob.colorH,
132
+ easedProgress,
133
+ )
134
+ currentBlobs[i].colorS = interpolate(
135
+ startBlob.colorS,
136
+ targetBlob.colorS,
137
+ easedProgress,
138
+ )
139
+ currentBlobs[i].colorL = interpolate(
140
+ startBlob.colorL,
141
+ targetBlob.colorL,
142
+ easedProgress,
143
+ )
144
+
145
+ gradient.addColorStop(
146
+ 0,
147
+ `hsla(${currentBlobs[i].colorH}, ${currentBlobs[i].colorS}%, ${currentBlobs[i].colorL}%, 1)`,
148
+ )
149
+ gradient.addColorStop(
150
+ 1,
151
+ `hsla(${currentBlobs[i].colorH}, ${currentBlobs[i].colorS}%, ${currentBlobs[i].colorL}%, 0)`,
152
+ )
153
+
154
+ ctx.fillStyle = gradient
155
+ ctx.beginPath()
156
+ ctx.arc(
157
+ currentBlobs[i].x,
158
+ currentBlobs[i].y,
159
+ currentBlobs[i].r,
160
+ 0,
161
+ Math.PI * 2,
162
+ )
163
+ ctx.fill()
164
+ })
165
+
166
+ if (progress < 1) {
167
+ rafId = requestAnimationFrame(animate)
168
+ } else {
169
+ timeout = setTimeout(() => {
170
+ morphDuration = 4000
171
+ start()
172
+ }, waitDuration)
173
+ }
174
+ }
175
+
176
+ resizeHandler()
177
+ start()
178
+ window.addEventListener('resize', resizeHandler)
179
+
180
+ return () => {
181
+ if (rafId) {
182
+ cancelAnimationFrame(rafId)
183
+ }
184
+ if (timeout) {
185
+ clearTimeout(timeout)
186
+ }
187
+ window.removeEventListener('resize', resizeHandler)
188
+ }
189
+ }
190
+ }, [prefersReducedMotion])
191
+
192
+ return (
193
+ <div
194
+ className={twMerge(
195
+ 'fixed inset-0 z-0 opacity-20 pointer-events-none',
196
+ 'transition-opacity duration-[2s] ease-linear',
197
+ '[&+*]:relative',
198
+ mounted
199
+ ? isHomePage
200
+ ? 'opacity-10 dark:opacity-20'
201
+ : 'opacity-10 dark:opacity-20'
202
+ : 'opacity-0',
203
+ )}
204
+ >
205
+ <canvas ref={canvasRef} />
206
+ </div>
207
+ )
208
+ }
209
+
210
+ function cubicBezier(_p1x: number, p1y: number, _p2x: number, p2y: number) {
211
+ return function (t: number) {
212
+ const cy = 3 * p1y
213
+ const by = 3 * (p2y - p1y) - cy
214
+ const ay = 1 - cy - by
215
+
216
+ const y = ((ay * t + by) * t + cy) * t
217
+
218
+ return y
219
+ }
220
+ }
221
+
222
+ function interpolate(start: number, end: number, progress: number) {
223
+ return start + (end - start) * progress
224
+ }
@@ -0,0 +1,22 @@
1
+ import { useManager } from '../store/project'
2
+ import { Toaster } from './toaster'
3
+
4
+ import { QueryProvider } from './query-provider'
5
+
6
+ function InternalHandler({ children }: { children: React.ReactNode }) {
7
+ useManager()
8
+ return (
9
+ <>
10
+ {children}
11
+ <Toaster />
12
+ </>
13
+ )
14
+ }
15
+
16
+ export function CTAProvider({ children }: { children: React.ReactNode }) {
17
+ return (
18
+ <QueryProvider>
19
+ <InternalHandler>{children}</InternalHandler>
20
+ </QueryProvider>
21
+ )
22
+ }
@@ -0,0 +1,43 @@
1
+ import { useApplicationMode, useReady } from '../store/project'
2
+
3
+ import SelectedAddOns from './sidebar-items/add-ons'
4
+ import RunAddOns from './sidebar-items/run-add-ons'
5
+ import RunCreateApp from './sidebar-items/run-create-app'
6
+ import ProjectName from './sidebar-items/project-name'
7
+ import ModeSelector from './sidebar-items/mode-selector'
8
+ import TypescriptSwitch from './sidebar-items/typescript-switch'
9
+ import StarterDialog from './sidebar-items/starter'
10
+ import SidebarGroup from './sidebar-items/sidebar-group'
11
+
12
+ export function AppSidebar() {
13
+ const ready = useReady()
14
+ const mode = useApplicationMode()
15
+
16
+ return (
17
+ <div className="flex flex-col gap-2">
18
+ {ready && (
19
+ <>
20
+ {mode === 'setup' && (
21
+ <SidebarGroup>
22
+ <ProjectName />
23
+ <ModeSelector />
24
+ <TypescriptSwitch />
25
+ </SidebarGroup>
26
+ )}
27
+ <SidebarGroup>
28
+ <SelectedAddOns />
29
+ </SidebarGroup>
30
+ {mode === 'setup' && (
31
+ <SidebarGroup>
32
+ <StarterDialog />
33
+ </SidebarGroup>
34
+ )}
35
+ </>
36
+ )}
37
+ <div className="mt-5">
38
+ <RunAddOns />
39
+ <RunCreateApp />
40
+ </div>
41
+ </div>
42
+ )
43
+ }
@@ -0,0 +1,79 @@
1
+ import { useState } from 'react'
2
+ import { toast } from 'sonner'
3
+ import { TicketPlusIcon } from 'lucide-react'
4
+
5
+ import { addCustomAddOn, useAddOns, useRouterMode } from '../store/project'
6
+ import { loadRemoteAddOn } from '../lib/api'
7
+
8
+ import { Button } from './ui/button'
9
+ import { Input } from './ui/input'
10
+ import {
11
+ Dialog,
12
+ DialogContent,
13
+ DialogFooter,
14
+ DialogHeader,
15
+ DialogTitle,
16
+ } from './ui/dialog'
17
+
18
+ export default function CustomAddOnDialog() {
19
+ const [url, setUrl] = useState('')
20
+ const [open, setOpen] = useState(false)
21
+
22
+ const mode = useRouterMode()
23
+ const { toggleAddOn } = useAddOns()
24
+
25
+ async function onImport() {
26
+ const data = await loadRemoteAddOn(url)
27
+
28
+ if ('error' in data) {
29
+ toast.error('Failed to load add-on', {
30
+ description: data.error,
31
+ })
32
+ } else {
33
+ addCustomAddOn(data)
34
+ if (data.modes.includes(mode)) {
35
+ toggleAddOn(data.id)
36
+ }
37
+ setOpen(false)
38
+ }
39
+ }
40
+
41
+ return (
42
+ <div>
43
+ <Button
44
+ variant="secondary"
45
+ className="w-full"
46
+ onClick={() => {
47
+ setUrl('')
48
+ setOpen(true)
49
+ }}
50
+ >
51
+ <TicketPlusIcon className="w-4 h-4" />
52
+ Import Custom Add-On
53
+ </Button>
54
+ <Dialog modal open={open} onOpenChange={setOpen}>
55
+ <DialogContent className="sm:min-w-[425px] sm:max-w-fit">
56
+ <DialogHeader>
57
+ <DialogTitle>Import Custom Add-On</DialogTitle>
58
+ </DialogHeader>
59
+ <div>
60
+ <Input
61
+ value={url}
62
+ onChange={(e) => setUrl(e.target.value)}
63
+ placeholder="https://github.com/myorg/myproject/add-on.json"
64
+ className="min-w-lg w-full"
65
+ onKeyDown={(e) => {
66
+ if (e.key === 'Enter') {
67
+ onImport()
68
+ }
69
+ }}
70
+ />
71
+ </div>
72
+ <DialogFooter>
73
+ <Button onClick={onImport}>Import</Button>
74
+ </DialogFooter>
75
+ </DialogContent>
76
+ </Dialog>
77
+ </div>
78
+ )
79
+ }
@@ -0,0 +1,207 @@
1
+ import { useMemo, useState } from 'react'
2
+ import { FileText, Folder } from 'lucide-react'
3
+
4
+ import {
5
+ useApplicationMode,
6
+ useDryRun,
7
+ useFilters,
8
+ useOriginalOutput,
9
+ useProjectLocalFiles,
10
+ useReady,
11
+ } from '../store/project'
12
+
13
+ import { getFileClass, twClasses } from '../file-classes'
14
+
15
+ import FileViewer from './file-viewer'
16
+ import FileTree from './file-tree'
17
+
18
+ import { Label } from './ui/label'
19
+ import { Switch } from './ui/switch'
20
+
21
+ import type { FileTreeItem } from '../types'
22
+
23
+ export function Filters() {
24
+ const { includedFiles, toggleFilter } = useFilters()
25
+
26
+ return (
27
+ <div className="bg-white dark:bg-black/40 shadow-xl p-4 rounded-lg flex flex-row items-center gap-4 mb-2">
28
+ <h3 className="font-medium whitespace-nowrap">File Filters</h3>
29
+ <div className="flex flex-row items-center">
30
+ <Switch
31
+ id="unchanged"
32
+ checked={includedFiles.includes('unchanged')}
33
+ onCheckedChange={() => toggleFilter('unchanged')}
34
+ className="mr-2"
35
+ />
36
+ <Label htmlFor="unchanged" className={twClasses.unchanged}>
37
+ Unchanged
38
+ </Label>
39
+ </div>
40
+ <div className="flex flex-row items-center">
41
+ <Switch
42
+ id="added"
43
+ checked={includedFiles.includes('added')}
44
+ onCheckedChange={() => toggleFilter('added')}
45
+ className="mr-2"
46
+ />
47
+ <Label htmlFor="added" className={twClasses.added}>
48
+ Added
49
+ </Label>
50
+ </div>
51
+ <div className="flex flex-row items-center">
52
+ <Switch
53
+ id="modified"
54
+ checked={includedFiles.includes('modified')}
55
+ onCheckedChange={() => toggleFilter('modified')}
56
+ className="mr-2"
57
+ />
58
+ <Label htmlFor="modified" className={twClasses.modified}>
59
+ Modified
60
+ </Label>
61
+ </div>
62
+ <div className="flex flex-row items-center">
63
+ <Switch
64
+ id="deleted"
65
+ checked={includedFiles.includes('deleted')}
66
+ onCheckedChange={() => toggleFilter('deleted')}
67
+ className="mr-2"
68
+ />
69
+ <Label htmlFor="deleted" className={twClasses.deleted}>
70
+ Deleted
71
+ </Label>
72
+ </div>
73
+ <div className="flex flex-row items-center">
74
+ <Switch
75
+ id="overwritten"
76
+ checked={includedFiles.includes('overwritten')}
77
+ onCheckedChange={() => toggleFilter('overwritten')}
78
+ className="mr-2"
79
+ />
80
+ <Label htmlFor="overwritten" className={twClasses.overwritten}>
81
+ Overwritten
82
+ </Label>
83
+ </div>
84
+ </div>
85
+ )
86
+ }
87
+
88
+ export default function FileNavigator() {
89
+ const [selectedFile, setSelectedFile] = useState<string | null>(
90
+ './package.json',
91
+ )
92
+
93
+ const projectFiles = useOriginalOutput()
94
+ const localTree = useProjectLocalFiles()
95
+ const dryRunOutput = useDryRun()
96
+
97
+ const mode = useApplicationMode()
98
+
99
+ const tree = dryRunOutput.files
100
+ const originalTree: Record<string, string> | undefined =
101
+ mode === 'setup' ? dryRunOutput.files : projectFiles?.files
102
+ const deletedFiles = dryRunOutput.deletedFiles
103
+
104
+ const [originalFileContents, setOriginalFileContents] = useState<string>()
105
+ const [modifiedFileContents, setModifiedFileContents] = useState<string>()
106
+
107
+ const { includedFiles } = useFilters()
108
+
109
+ const fileTree = useMemo(() => {
110
+ const treeData: Array<FileTreeItem> = []
111
+
112
+ if (!originalTree || !localTree) {
113
+ return treeData
114
+ }
115
+
116
+ const allFileSet = Array.from(
117
+ new Set([
118
+ ...Object.keys(tree),
119
+ ...Object.keys(localTree),
120
+ ...Object.keys(originalTree),
121
+ ]),
122
+ )
123
+
124
+ allFileSet.sort().forEach((file) => {
125
+ const strippedFile = file.replace('./', '')
126
+ const parts = strippedFile.split('/')
127
+
128
+ let currentLevel = treeData
129
+ parts.forEach((part, index) => {
130
+ const existingNode = currentLevel.find((node) => node.name === part)
131
+ if (existingNode) {
132
+ currentLevel = existingNode.children || []
133
+ } else {
134
+ const fileInfo = getFileClass(
135
+ file,
136
+ tree,
137
+ originalTree,
138
+ localTree,
139
+ deletedFiles,
140
+ )
141
+
142
+ if (
143
+ index === parts.length - 1 &&
144
+ !includedFiles.includes(fileInfo.fileClass)
145
+ ) {
146
+ return
147
+ }
148
+ if (index === parts.length - 1 && file === selectedFile) {
149
+ setModifiedFileContents(fileInfo.modifiedFile)
150
+ setOriginalFileContents(fileInfo.originalFile)
151
+ }
152
+
153
+ const newNode: FileTreeItem = {
154
+ id: parts.slice(0, index + 1).join('/'),
155
+ name: part,
156
+ fullPath: strippedFile,
157
+ children: index < parts.length - 1 ? [] : undefined,
158
+ icon:
159
+ index < parts.length - 1
160
+ ? () => <Folder className="w-4 h-4 mr-2" />
161
+ : () => <FileText className="w-4 h-4 mr-2" />,
162
+ onClick:
163
+ index === parts.length - 1
164
+ ? () => {
165
+ setSelectedFile(file)
166
+ setModifiedFileContents(fileInfo.modifiedFile)
167
+ setOriginalFileContents(fileInfo.originalFile)
168
+ }
169
+ : undefined,
170
+ className: twClasses[fileInfo.fileClass],
171
+ ...fileInfo,
172
+ contents: tree[file] || localTree[file] || originalTree[file],
173
+ }
174
+ currentLevel.push(newNode)
175
+ currentLevel = newNode.children!
176
+ }
177
+ })
178
+ })
179
+ return treeData
180
+ }, [tree, originalTree, localTree, includedFiles])
181
+
182
+ const ready = useReady()
183
+
184
+ if (!ready) {
185
+ return null
186
+ }
187
+
188
+ return (
189
+ <div className="bg-white dark:bg-black/50 rounded-lg p-2 sm:p-4">
190
+ {mode === 'add' && <Filters />}
191
+ <div className="flex flex-row @container">
192
+ <div className="w-1/3 @6xl:w-1/4 bg-gray-500/10 rounded-l-lg">
193
+ <FileTree selectedFile={selectedFile} tree={fileTree} />
194
+ </div>
195
+ <div className="w-2/3 @6xl:w-3/4">
196
+ {selectedFile && modifiedFileContents ? (
197
+ <FileViewer
198
+ filePath={selectedFile}
199
+ originalFile={originalFileContents}
200
+ modifiedFile={modifiedFileContents}
201
+ />
202
+ ) : null}
203
+ </div>
204
+ </div>
205
+ </div>
206
+ )
207
+ }
@@ -0,0 +1,35 @@
1
+ import { useMemo } from 'react'
2
+ import { FileText, Folder } from 'lucide-react'
3
+
4
+ import { TreeView } from './ui/tree-view'
5
+
6
+ import type { FileTreeItem } from '../types'
7
+
8
+ export default function FileTree({
9
+ selectedFile,
10
+ tree,
11
+ }: {
12
+ selectedFile: string | null
13
+ tree: Array<FileTreeItem>
14
+ }) {
15
+ const initialExpandedItemIds = useMemo(
16
+ () => [
17
+ 'src',
18
+ 'src/routes',
19
+ 'src/components',
20
+ 'src/components/ui',
21
+ 'src/lib',
22
+ ],
23
+ [],
24
+ )
25
+
26
+ return (
27
+ <TreeView
28
+ initialSelectedItemId={selectedFile?.replace('./', '') ?? undefined}
29
+ initialExpandedItemIds={initialExpandedItemIds}
30
+ data={tree}
31
+ defaultNodeIcon={() => <Folder className="w-4 h-4 mr-2" />}
32
+ defaultLeafIcon={() => <FileText className="w-4 h-4 mr-2" />}
33
+ />
34
+ )
35
+ }