@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.
- package/LICENSE +21 -0
- package/components.json +21 -0
- package/dist/app.d.ts +1 -0
- package/dist/app.js +10 -0
- package/dist/components/add-on-info-dialog.d.ts +5 -0
- package/dist/components/add-on-info-dialog.js +5 -0
- package/dist/components/background-animation.d.ts +1 -0
- package/dist/components/background-animation.js +144 -0
- package/dist/components/cta-provider.d.ts +3 -0
- package/dist/components/cta-provider.js +11 -0
- package/dist/components/cta-sidebar.d.ts +1 -0
- package/dist/components/cta-sidebar.js +15 -0
- package/dist/components/custom-add-on-dialog.d.ts +1 -0
- package/dist/components/custom-add-on-dialog.js +38 -0
- package/dist/components/file-navigator.d.ts +2 -0
- package/dist/components/file-navigator.js +86 -0
- package/dist/components/file-tree.d.ts +5 -0
- package/dist/components/file-tree.js +14 -0
- package/dist/components/file-viewer.d.ts +5 -0
- package/dist/components/file-viewer.js +40 -0
- package/dist/components/header.d.ts +1 -0
- package/dist/components/header.js +5 -0
- package/dist/components/icons/tailwind.d.ts +3 -0
- package/dist/components/icons/tailwind.js +5 -0
- package/dist/components/icons/tanstack.d.ts +3 -0
- package/dist/components/icons/tanstack.js +5 -0
- package/dist/components/icons/typescript.d.ts +3 -0
- package/dist/components/icons/typescript.js +5 -0
- package/dist/components/query-provider.d.ts +3 -0
- package/dist/components/query-provider.js +7 -0
- package/dist/components/sidebar-items/add-ons.d.ts +1 -0
- package/dist/components/sidebar-items/add-ons.js +27 -0
- package/dist/components/sidebar-items/mode-selector.d.ts +1 -0
- package/dist/components/sidebar-items/mode-selector.js +19 -0
- package/dist/components/sidebar-items/project-name.d.ts +1 -0
- package/dist/components/sidebar-items/project-name.js +12 -0
- package/dist/components/sidebar-items/run-add-ons.d.ts +1 -0
- package/dist/components/sidebar-items/run-add-ons.js +25 -0
- package/dist/components/sidebar-items/run-create-app.d.ts +1 -0
- package/dist/components/sidebar-items/run-create-app.js +28 -0
- package/dist/components/sidebar-items/sidebar-container.d.ts +3 -0
- package/dist/components/sidebar-items/sidebar-container.js +4 -0
- package/dist/components/sidebar-items/sidebar-group.d.ts +3 -0
- package/dist/components/sidebar-items/sidebar-group.js +4 -0
- package/dist/components/sidebar-items/starter.d.ts +1 -0
- package/dist/components/sidebar-items/starter.js +42 -0
- package/dist/components/sidebar-items/typescript-switch.d.ts +1 -0
- package/dist/components/sidebar-items/typescript-switch.js +18 -0
- package/dist/components/starters-carousel.d.ts +3 -0
- package/dist/components/starters-carousel.js +12 -0
- package/dist/components/startup-dialog.d.ts +1 -0
- package/dist/components/startup-dialog.js +30 -0
- package/dist/components/status-list.d.ts +5 -0
- package/dist/components/status-list.js +4 -0
- package/dist/components/toaster.d.ts +4 -0
- package/dist/components/toaster.js +15 -0
- package/dist/components/ui/button.d.ts +10 -0
- package/dist/components/ui/button.js +32 -0
- package/dist/components/ui/carousel.d.ts +20 -0
- package/dist/components/ui/carousel.js +90 -0
- package/dist/components/ui/checkbox.d.ts +4 -0
- package/dist/components/ui/checkbox.js +9 -0
- package/dist/components/ui/dialog.d.ts +15 -0
- package/dist/components/ui/dialog.js +36 -0
- package/dist/components/ui/dropdown-menu.d.ts +25 -0
- package/dist/components/ui/dropdown-menu.js +51 -0
- package/dist/components/ui/input.d.ts +3 -0
- package/dist/components/ui/input.js +7 -0
- package/dist/components/ui/label.d.ts +4 -0
- package/dist/components/ui/label.js +8 -0
- package/dist/components/ui/popover.d.ts +7 -0
- package/dist/components/ui/popover.js +17 -0
- package/dist/components/ui/separator.d.ts +4 -0
- package/dist/components/ui/separator.js +9 -0
- package/dist/components/ui/sheet.d.ts +13 -0
- package/dist/components/ui/sheet.js +40 -0
- package/dist/components/ui/skeleton.d.ts +2 -0
- package/dist/components/ui/skeleton.js +6 -0
- package/dist/components/ui/sonner.d.ts +3 -0
- package/dist/components/ui/sonner.js +12 -0
- package/dist/components/ui/switch.d.ts +4 -0
- package/dist/components/ui/switch.js +8 -0
- package/dist/components/ui/table.d.ts +10 -0
- package/dist/components/ui/table.js +28 -0
- package/dist/components/ui/tabs.d.ts +7 -0
- package/dist/components/ui/tabs.js +17 -0
- package/dist/components/ui/toggle-group.d.ts +7 -0
- package/dist/components/ui/toggle-group.js +20 -0
- package/dist/components/ui/toggle.d.ts +9 -0
- package/dist/components/ui/toggle.js +27 -0
- package/dist/components/ui/tooltip.d.ts +7 -0
- package/dist/components/ui/tooltip.js +18 -0
- package/dist/components/ui/tree-view.d.ts +25 -0
- package/dist/components/ui/tree-view.js +151 -0
- package/dist/file-classes.d.ts +8 -0
- package/dist/file-classes.js +41 -0
- package/dist/hooks/use-mounted.d.ts +1 -0
- package/dist/hooks/use-mounted.js +8 -0
- package/dist/hooks/use-preferred-reduced-motion.d.ts +5 -0
- package/dist/hooks/use-preferred-reduced-motion.js +20 -0
- package/dist/hooks/use-streaming-status.d.ts +6 -0
- package/dist/hooks/use-streaming-status.js +55 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +20 -0
- package/dist/lib/api.d.ts +14 -0
- package/dist/lib/api.js +74 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +5 -0
- package/dist/store/add-ons.d.ts +7 -0
- package/dist/store/add-ons.js +59 -0
- package/dist/store/project.d.ts +76 -0
- package/dist/store/project.js +269 -0
- package/package.json +50 -0
- package/src/app.tsx +28 -0
- package/src/components/add-on-info-dialog.tsx +39 -0
- package/src/components/background-animation.tsx +224 -0
- package/src/components/cta-provider.tsx +22 -0
- package/src/components/cta-sidebar.tsx +43 -0
- package/src/components/custom-add-on-dialog.tsx +79 -0
- package/src/components/file-navigator.tsx +207 -0
- package/src/components/file-tree.tsx +35 -0
- package/src/components/file-viewer.tsx +67 -0
- package/src/components/header.tsx +29 -0
- package/src/components/icons/tailwind.tsx +26 -0
- package/src/components/icons/tanstack.tsx +338 -0
- package/src/components/icons/typescript.tsx +23 -0
- package/src/components/query-provider.tsx +10 -0
- package/src/components/sidebar-items/add-ons.tsx +94 -0
- package/src/components/sidebar-items/mode-selector.tsx +56 -0
- package/src/components/sidebar-items/project-name.tsx +32 -0
- package/src/components/sidebar-items/run-add-ons.tsx +71 -0
- package/src/components/sidebar-items/run-create-app.tsx +82 -0
- package/src/components/sidebar-items/sidebar-container.tsx +11 -0
- package/src/components/sidebar-items/sidebar-group.tsx +11 -0
- package/src/components/sidebar-items/starter.tsx +123 -0
- package/src/components/sidebar-items/typescript-switch.tsx +58 -0
- package/src/components/starters-carousel.tsx +41 -0
- package/src/components/startup-dialog.tsx +72 -0
- package/src/components/status-list.tsx +22 -0
- package/src/components/toaster.tsx +29 -0
- package/src/components/ui/button.tsx +61 -0
- package/src/components/ui/carousel.tsx +239 -0
- package/src/components/ui/checkbox.tsx +30 -0
- package/src/components/ui/dialog.tsx +138 -0
- package/src/components/ui/dropdown-menu.tsx +255 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +22 -0
- package/src/components/ui/popover.tsx +46 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +137 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/sonner.tsx +24 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/table.tsx +114 -0
- package/src/components/ui/tabs.tsx +64 -0
- package/src/components/ui/toggle-group.tsx +72 -0
- package/src/components/ui/toggle.tsx +49 -0
- package/src/components/ui/tooltip.tsx +61 -0
- package/src/components/ui/tree-view.tsx +497 -0
- package/src/file-classes.ts +54 -0
- package/src/hooks/use-mounted.ts +9 -0
- package/src/hooks/use-preferred-reduced-motion.ts +27 -0
- package/src/hooks/use-streaming-status.ts +70 -0
- package/src/index.ts +44 -0
- package/src/lib/api.ts +100 -0
- package/src/lib/utils.ts +8 -0
- package/src/store/add-ons.ts +81 -0
- package/src/store/project.ts +345 -0
- package/src/types.d.ts +109 -0
- package/tests/store/add-ons.test.ts +222 -0
- package/tsconfig.json +25 -0
- 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
|
+
}
|