@tanstack/cta-ui 0.10.0-alpha.18 → 0.10.0-alpha.20
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/lib/index.ts +16 -7
- package/lib-dist/index.d.ts +6 -1
- package/lib-dist/index.js +6 -3
- package/package.json +20 -7
- package/public/tailwind.svg +1 -0
- package/public/tanstack.png +0 -0
- package/public/typescript.svg +1 -0
- package/src/components/StatusList.tsx +22 -0
- package/src/components/add-on-info-dialog.tsx +39 -0
- package/src/components/cta-sidebar.tsx +55 -0
- package/src/components/custom-add-on-dialog.tsx +79 -0
- package/src/components/file-navigator.tsx +205 -0
- package/src/components/file-tree.tsx +18 -60
- package/src/components/file-viewer.tsx +11 -3
- package/src/components/sidebar-items/add-ons.tsx +91 -0
- package/src/components/sidebar-items/mode-selector.tsx +55 -0
- package/src/components/sidebar-items/project-name.tsx +29 -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/starter.tsx +115 -0
- package/src/components/sidebar-items/typescript-switch.tsx +52 -0
- package/src/components/toaster.tsx +29 -0
- package/src/components/ui/button.tsx +21 -19
- package/src/components/ui/dialog.tsx +25 -20
- 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/sidebar.tsx +726 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/sonner.tsx +23 -0
- package/src/components/ui/switch.tsx +29 -0
- package/src/components/ui/toggle-group.tsx +11 -11
- package/src/components/ui/toggle.tsx +15 -13
- package/src/components/ui/tooltip.tsx +61 -0
- package/src/components/ui/tree-view.tsx +17 -12
- package/src/engine-handling/add-to-app-wrapper.ts +114 -0
- package/src/engine-handling/create-app-wrapper.ts +107 -0
- package/src/engine-handling/file-helpers.ts +25 -0
- package/src/engine-handling/framework-registration.ts +11 -0
- package/src/engine-handling/generate-initial-payload.ts +93 -0
- package/src/engine-handling/server-environment.ts +13 -0
- package/src/file-classes.ts +54 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-streaming-status.ts +70 -0
- package/src/lib/api.ts +90 -0
- package/src/routeTree.gen.ts +4 -27
- package/src/routes/__root.tsx +36 -7
- package/src/routes/api/add-to-app.ts +21 -0
- package/src/routes/api/create-app.ts +21 -0
- package/src/routes/api/dry-run-add-to-app.ts +16 -0
- package/src/routes/api/dry-run-create-app.ts +16 -0
- package/src/routes/api/initial-payload.ts +10 -0
- package/src/routes/api/load-remote-add-on.ts +42 -0
- package/src/routes/api/load-starter.ts +47 -0
- package/src/routes/api/shutdown.ts +11 -0
- package/src/routes/index.tsx +3 -210
- package/src/store/add-ons.ts +81 -0
- package/src/store/project.ts +268 -0
- package/src/styles.css +47 -0
- package/src/types.d.ts +87 -0
- package/tests/store/add-ons.test.ts +222 -0
- package/vitest.config.ts +6 -0
- package/.cursorrules +0 -7
- package/src/components/Header.tsx +0 -13
- package/src/components/applied-add-on.tsx +0 -149
- package/src/lib/server-fns.ts +0 -78
- package/src/routes/api.demo-names.ts +0 -11
- package/src/routes/demo.tanstack-query.tsx +0 -28
package/src/routes/index.tsx
CHANGED
|
@@ -1,222 +1,15 @@
|
|
|
1
|
-
import { useState, Fragment } from 'react'
|
|
2
1
|
import { createFileRoute } from '@tanstack/react-router'
|
|
3
|
-
import { Info } from 'lucide-react'
|
|
4
2
|
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
Table,
|
|
8
|
-
TableBody,
|
|
9
|
-
TableCell,
|
|
10
|
-
TableHead,
|
|
11
|
-
TableHeader,
|
|
12
|
-
TableRow,
|
|
13
|
-
} from '@/components/ui/table'
|
|
14
|
-
|
|
15
|
-
import AppliedAddOn from '@/components/applied-add-on'
|
|
16
|
-
import FileTree from '@/components/file-tree'
|
|
17
|
-
import FileViewer from '@/components/file-viewer'
|
|
18
|
-
|
|
19
|
-
import {
|
|
20
|
-
getAddons,
|
|
21
|
-
getAddonInfo,
|
|
22
|
-
getOriginalOptions,
|
|
23
|
-
runCreateApp,
|
|
24
|
-
} from '@/lib/server-fns'
|
|
25
|
-
import type { AddOn } from '@tanstack/cta-engine'
|
|
3
|
+
import FileNavigator from '@/components/file-navigator'
|
|
26
4
|
|
|
27
5
|
export const Route = createFileRoute('/')({
|
|
28
6
|
component: App,
|
|
29
|
-
loader: async () => {
|
|
30
|
-
const originalOptions = await getOriginalOptions()
|
|
31
|
-
const [codeRouterAddons, fileRouterAddons] = await Promise.all([
|
|
32
|
-
getAddons({
|
|
33
|
-
data: { platform: 'react', mode: 'code-router' },
|
|
34
|
-
}),
|
|
35
|
-
getAddons({
|
|
36
|
-
data: { platform: 'react', mode: 'file-router' },
|
|
37
|
-
}),
|
|
38
|
-
])
|
|
39
|
-
return {
|
|
40
|
-
addOns: {
|
|
41
|
-
'code-router': codeRouterAddons,
|
|
42
|
-
'file-router': fileRouterAddons,
|
|
43
|
-
},
|
|
44
|
-
projectPath: process.env.PROJECT_PATH!,
|
|
45
|
-
output: await runCreateApp({
|
|
46
|
-
data: { withAddOn: true, options: originalOptions },
|
|
47
|
-
}),
|
|
48
|
-
outputWithoutAddon: await runCreateApp({
|
|
49
|
-
data: { withAddOn: false, options: originalOptions },
|
|
50
|
-
}),
|
|
51
|
-
addOnInfo: (await getAddonInfo()) as AddOn,
|
|
52
|
-
originalOptions,
|
|
53
|
-
}
|
|
54
|
-
},
|
|
55
7
|
})
|
|
56
8
|
|
|
57
|
-
const CAPTIONS = {
|
|
58
|
-
dependencies: 'Dependencies',
|
|
59
|
-
devDependencies: 'Dev Dependencies',
|
|
60
|
-
scripts: 'Scripts',
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function InfoViewer({ info }: { info: AddOn }) {
|
|
64
|
-
return (
|
|
65
|
-
<div className="text-lg">
|
|
66
|
-
<div className="grid grid-cols-[200px_1fr] gap-4 mb-4">
|
|
67
|
-
<div className="font-medium">Name</div>
|
|
68
|
-
<div className="font-bold">{info.name}</div>
|
|
69
|
-
<div className="font-medium">Description</div>
|
|
70
|
-
<div className="font-bold">{info.description}</div>
|
|
71
|
-
<div className="font-medium">Version</div>
|
|
72
|
-
<div className="font-bold">{info.version}</div>
|
|
73
|
-
<div className="font-medium">Author</div>
|
|
74
|
-
<div className="font-bold">{info.author}</div>
|
|
75
|
-
<div className="font-medium">License</div>
|
|
76
|
-
<div className="font-bold">{info.license}</div>
|
|
77
|
-
{(
|
|
78
|
-
[
|
|
79
|
-
'dependencies',
|
|
80
|
-
'devDependencies',
|
|
81
|
-
'scripts',
|
|
82
|
-
] as (keyof AddOn['packageAdditions'])[]
|
|
83
|
-
)
|
|
84
|
-
.filter(
|
|
85
|
-
(key) =>
|
|
86
|
-
info.packageAdditions[key] &&
|
|
87
|
-
Object.entries(info.packageAdditions[key]).length > 0,
|
|
88
|
-
)
|
|
89
|
-
.map((key) => (
|
|
90
|
-
<Fragment key={key}>
|
|
91
|
-
<div className="font-medium">{CAPTIONS[key]}</div>
|
|
92
|
-
<Table>
|
|
93
|
-
{key !== 'scripts' && (
|
|
94
|
-
<TableHeader>
|
|
95
|
-
<TableRow>
|
|
96
|
-
<TableHead className="w-1/2">Package</TableHead>
|
|
97
|
-
<TableHead className="w-1/2">Version</TableHead>
|
|
98
|
-
</TableRow>
|
|
99
|
-
</TableHeader>
|
|
100
|
-
)}
|
|
101
|
-
<TableBody>
|
|
102
|
-
{Object.entries(info.packageAdditions[key]).map(
|
|
103
|
-
([pkg, version]) => (
|
|
104
|
-
<TableRow key={`${key}-${pkg}`}>
|
|
105
|
-
<TableCell className="w-1/2">{pkg}</TableCell>
|
|
106
|
-
<TableCell className="w-1/2">{version}</TableCell>
|
|
107
|
-
</TableRow>
|
|
108
|
-
),
|
|
109
|
-
)}
|
|
110
|
-
</TableBody>
|
|
111
|
-
</Table>
|
|
112
|
-
</Fragment>
|
|
113
|
-
))}
|
|
114
|
-
{info.routes && (
|
|
115
|
-
<>
|
|
116
|
-
<div className="font-medium">Routes</div>
|
|
117
|
-
<div>
|
|
118
|
-
<Table>
|
|
119
|
-
<TableHeader>
|
|
120
|
-
<TableRow>
|
|
121
|
-
<TableHead className="w-1/2">URL</TableHead>
|
|
122
|
-
<TableHead className="w-1/2">Name</TableHead>
|
|
123
|
-
</TableRow>
|
|
124
|
-
</TableHeader>
|
|
125
|
-
<TableBody>
|
|
126
|
-
{info.routes.map(({ url, name }) => (
|
|
127
|
-
<TableRow key={`${url}-${name}`}>
|
|
128
|
-
<TableCell className="w-1/2">{url}</TableCell>
|
|
129
|
-
<TableCell className="w-1/2">{name}</TableCell>
|
|
130
|
-
</TableRow>
|
|
131
|
-
))}
|
|
132
|
-
</TableBody>
|
|
133
|
-
</Table>
|
|
134
|
-
</div>
|
|
135
|
-
</>
|
|
136
|
-
)}
|
|
137
|
-
{info.commmand && (
|
|
138
|
-
<>
|
|
139
|
-
<div className="font-medium">Setup Command</div>
|
|
140
|
-
<div className="font-bold font-mono">{`${info.command.command} ${info.command.args.join(' ')}`}</div>
|
|
141
|
-
</>
|
|
142
|
-
)}
|
|
143
|
-
</div>
|
|
144
|
-
</div>
|
|
145
|
-
)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function AddOnViewer({ addOnInfo }: { addOnInfo: AddOn }) {
|
|
149
|
-
const [selectedFile, setSelectedFile] = useState<string | null>(null)
|
|
150
|
-
const [showInfo, setShowInfo] = useState(false)
|
|
151
|
-
|
|
152
|
-
return (
|
|
153
|
-
<div className="flex flex-row">
|
|
154
|
-
<FileTree
|
|
155
|
-
prefix="./"
|
|
156
|
-
tree={addOnInfo.files || {}}
|
|
157
|
-
originalTree={{}}
|
|
158
|
-
onFileSelected={(file) => {
|
|
159
|
-
setSelectedFile(file)
|
|
160
|
-
setShowInfo(false)
|
|
161
|
-
}}
|
|
162
|
-
extraTreeItems={[
|
|
163
|
-
{
|
|
164
|
-
id: 'info',
|
|
165
|
-
name: 'Add-On Info',
|
|
166
|
-
icon: () => <Info className="w-4 h-4 mr-2" />,
|
|
167
|
-
onClick: () => {
|
|
168
|
-
setSelectedFile(null)
|
|
169
|
-
setShowInfo(true)
|
|
170
|
-
},
|
|
171
|
-
},
|
|
172
|
-
]}
|
|
173
|
-
/>
|
|
174
|
-
<div className="max-w-3/4 w-3/4 pl-2">
|
|
175
|
-
{showInfo && <InfoViewer info={addOnInfo as AddOn} />}
|
|
176
|
-
{selectedFile ? (
|
|
177
|
-
<FileViewer
|
|
178
|
-
filePath={selectedFile}
|
|
179
|
-
modifiedFile={addOnInfo?.files?.[selectedFile] || ''}
|
|
180
|
-
/>
|
|
181
|
-
) : null}
|
|
182
|
-
</div>
|
|
183
|
-
</div>
|
|
184
|
-
)
|
|
185
|
-
}
|
|
186
|
-
|
|
187
9
|
function App() {
|
|
188
|
-
const {
|
|
189
|
-
projectPath,
|
|
190
|
-
output,
|
|
191
|
-
addOnInfo,
|
|
192
|
-
outputWithoutAddon,
|
|
193
|
-
originalOptions,
|
|
194
|
-
addOns,
|
|
195
|
-
} = Route.useLoaderData()
|
|
196
|
-
|
|
197
10
|
return (
|
|
198
|
-
<div className="
|
|
199
|
-
<
|
|
200
|
-
<TabsList className="grid grid-cols-2 w-[400px]">
|
|
201
|
-
<TabsTrigger value="add-on">Add-On</TabsTrigger>
|
|
202
|
-
<TabsTrigger value="applied">Applied</TabsTrigger>
|
|
203
|
-
</TabsList>
|
|
204
|
-
<TabsContent value="add-on">
|
|
205
|
-
<AddOnViewer addOnInfo={addOnInfo} />
|
|
206
|
-
</TabsContent>
|
|
207
|
-
<TabsContent value="applied">
|
|
208
|
-
<AppliedAddOn
|
|
209
|
-
projectPath={projectPath}
|
|
210
|
-
output={output}
|
|
211
|
-
addOnInfo={addOnInfo}
|
|
212
|
-
outputWithoutAddon={outputWithoutAddon}
|
|
213
|
-
originalOptions={
|
|
214
|
-
originalOptions as Required<typeof originalOptions>
|
|
215
|
-
}
|
|
216
|
-
addOns={addOns}
|
|
217
|
-
/>
|
|
218
|
-
</TabsContent>
|
|
219
|
-
</Tabs>
|
|
11
|
+
<div className="pl-3">
|
|
12
|
+
<FileNavigator />
|
|
220
13
|
</div>
|
|
221
14
|
)
|
|
222
15
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { AddOnInfo } from '@/types'
|
|
2
|
+
|
|
3
|
+
export function getAddOnStatus(
|
|
4
|
+
availableAddOns: Array<AddOnInfo>,
|
|
5
|
+
chosenAddOns: Array<string>,
|
|
6
|
+
originalAddOns: Array<string>,
|
|
7
|
+
) {
|
|
8
|
+
const addOnMap = new Map<
|
|
9
|
+
string,
|
|
10
|
+
{
|
|
11
|
+
enabled: boolean
|
|
12
|
+
selected: boolean
|
|
13
|
+
dependedUpon: boolean
|
|
14
|
+
}
|
|
15
|
+
>()
|
|
16
|
+
|
|
17
|
+
for (const addOn of availableAddOns) {
|
|
18
|
+
addOnMap.set(addOn.id, {
|
|
19
|
+
selected: false,
|
|
20
|
+
enabled: true,
|
|
21
|
+
dependedUpon: false,
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Guard against cycles in the dependency graph. The results won't be great. But it won't crash.
|
|
26
|
+
function cycleGuardedSelectAndDisableDependsOn(startingAddOnId: string) {
|
|
27
|
+
const visited = new Set<string>()
|
|
28
|
+
function selectAndDisableDependsOn(addOnId: string) {
|
|
29
|
+
if (visited.has(addOnId)) {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
visited.add(addOnId)
|
|
33
|
+
const selectedAddOn = availableAddOns.find(
|
|
34
|
+
(addOn) => addOn.id === addOnId,
|
|
35
|
+
)
|
|
36
|
+
if (selectedAddOn) {
|
|
37
|
+
for (const dependsOnId of selectedAddOn.dependsOn || []) {
|
|
38
|
+
const dependsOnAddOn = addOnMap.get(dependsOnId)
|
|
39
|
+
if (dependsOnAddOn) {
|
|
40
|
+
dependsOnAddOn.selected = true
|
|
41
|
+
dependsOnAddOn.enabled = false
|
|
42
|
+
dependsOnAddOn.dependedUpon = true
|
|
43
|
+
selectAndDisableDependsOn(dependsOnId)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const addOn = addOnMap.get(addOnId)
|
|
47
|
+
if (addOn) {
|
|
48
|
+
addOn.selected = true
|
|
49
|
+
if (!addOn.dependedUpon) {
|
|
50
|
+
addOn.enabled = true
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
selectAndDisableDependsOn(startingAddOnId)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const addOn of originalAddOns) {
|
|
59
|
+
const addOnInfo = addOnMap.get(addOn)
|
|
60
|
+
if (addOnInfo) {
|
|
61
|
+
addOnInfo.selected = true
|
|
62
|
+
addOnInfo.enabled = false
|
|
63
|
+
addOnInfo.dependedUpon = true
|
|
64
|
+
}
|
|
65
|
+
cycleGuardedSelectAndDisableDependsOn(addOn)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const addOnId of chosenAddOns) {
|
|
69
|
+
cycleGuardedSelectAndDisableDependsOn(addOnId)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return Object.fromEntries(
|
|
73
|
+
Array.from(addOnMap.entries()).map(([v, addOn]) => [
|
|
74
|
+
v,
|
|
75
|
+
{
|
|
76
|
+
enabled: addOn.enabled,
|
|
77
|
+
selected: addOn.selected,
|
|
78
|
+
},
|
|
79
|
+
]),
|
|
80
|
+
)
|
|
81
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { useCallback, useMemo } from 'react'
|
|
2
|
+
import { create } from 'zustand'
|
|
3
|
+
import { useQuery } from '@tanstack/react-query'
|
|
4
|
+
|
|
5
|
+
import { getAddOnStatus } from './add-ons'
|
|
6
|
+
|
|
7
|
+
import type { Mode, SerializedOptions } from '@tanstack/cta-engine'
|
|
8
|
+
|
|
9
|
+
import type { AddOnInfo, DryRunOutput, StarterInfo } from '@/types.js'
|
|
10
|
+
import { dryRunAddToApp, dryRunCreateApp, loadInitialData } from '@/lib/api'
|
|
11
|
+
|
|
12
|
+
const useInitialData = () =>
|
|
13
|
+
useQuery({
|
|
14
|
+
queryKey: ['initial-data'],
|
|
15
|
+
queryFn: async () => loadInitialData(),
|
|
16
|
+
initialData: {
|
|
17
|
+
options: {
|
|
18
|
+
framework: 'react-cra',
|
|
19
|
+
mode: 'file-router',
|
|
20
|
+
projectName: 'my-application',
|
|
21
|
+
targetDir: 'my-application',
|
|
22
|
+
typescript: true,
|
|
23
|
+
tailwind: true,
|
|
24
|
+
git: true,
|
|
25
|
+
chosenAddOns: [],
|
|
26
|
+
packageManager: 'pnpm',
|
|
27
|
+
},
|
|
28
|
+
localFiles: {},
|
|
29
|
+
output: {
|
|
30
|
+
files: {},
|
|
31
|
+
commands: [],
|
|
32
|
+
deletedFiles: [],
|
|
33
|
+
},
|
|
34
|
+
addOns: {
|
|
35
|
+
'code-router': [],
|
|
36
|
+
'file-router': [],
|
|
37
|
+
},
|
|
38
|
+
applicationMode: 'none',
|
|
39
|
+
},
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
export const useProjectLocalFiles = () => useInitialData().data.localFiles
|
|
43
|
+
export const useOriginalOutput = () => useInitialData().data.output
|
|
44
|
+
export const useOriginalSelectedAddOns = () =>
|
|
45
|
+
useInitialData().data.options.chosenAddOns
|
|
46
|
+
export const useApplicationMode = () => useInitialData().data.applicationMode
|
|
47
|
+
export const useReady = () => useInitialData().isFetched
|
|
48
|
+
export const useCodeRouterAddOns = () =>
|
|
49
|
+
useInitialData().data.addOns['code-router']
|
|
50
|
+
export const useFileRouterAddOns = () =>
|
|
51
|
+
useInitialData().data.addOns['file-router']
|
|
52
|
+
|
|
53
|
+
export const useProjectOptions = create<SerializedOptions>(() => ({
|
|
54
|
+
framework: 'react-cra',
|
|
55
|
+
mode: 'file-router',
|
|
56
|
+
projectName: 'my-app',
|
|
57
|
+
targetDir: 'my-app',
|
|
58
|
+
typescript: true,
|
|
59
|
+
tailwind: true,
|
|
60
|
+
git: true,
|
|
61
|
+
chosenAddOns: [],
|
|
62
|
+
packageManager: 'pnpm',
|
|
63
|
+
}))
|
|
64
|
+
|
|
65
|
+
const useApplicationSettings = create<{
|
|
66
|
+
includeFiles: Array<string>
|
|
67
|
+
}>(() => ({
|
|
68
|
+
includeFiles: ['unchanged', 'added', 'modified', 'deleted', 'overwritten'],
|
|
69
|
+
}))
|
|
70
|
+
|
|
71
|
+
const useMutableAddOns = create<{
|
|
72
|
+
userSelectedAddOns: Array<string>
|
|
73
|
+
customAddOns: Array<AddOnInfo>
|
|
74
|
+
}>(() => ({
|
|
75
|
+
userSelectedAddOns: [],
|
|
76
|
+
customAddOns: [],
|
|
77
|
+
}))
|
|
78
|
+
|
|
79
|
+
export const useProjectStarter = create<{
|
|
80
|
+
projectStarter: StarterInfo | undefined
|
|
81
|
+
}>(() => ({
|
|
82
|
+
projectStarter: undefined,
|
|
83
|
+
}))
|
|
84
|
+
|
|
85
|
+
export function addCustomAddOn(addOn: AddOnInfo) {
|
|
86
|
+
useMutableAddOns.setState((state) => ({
|
|
87
|
+
customAddOns: [...state.customAddOns, addOn],
|
|
88
|
+
}))
|
|
89
|
+
if (addOn.modes.includes(useProjectOptions.getState().mode)) {
|
|
90
|
+
useMutableAddOns.setState((state) => ({
|
|
91
|
+
userSelectedAddOns: [...state.userSelectedAddOns, addOn.id],
|
|
92
|
+
}))
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function useAddOns() {
|
|
97
|
+
const routerMode = useRouterMode()
|
|
98
|
+
const originalSelectedAddOns = useOriginalSelectedAddOns()
|
|
99
|
+
const codeRouterAddOns = useCodeRouterAddOns()
|
|
100
|
+
const fileRouterAddOns = useFileRouterAddOns()
|
|
101
|
+
const { userSelectedAddOns, customAddOns } = useMutableAddOns()
|
|
102
|
+
const projectStarter = useProjectStarter().projectStarter
|
|
103
|
+
|
|
104
|
+
const availableAddOns = useMemo(() => {
|
|
105
|
+
const baseAddOns =
|
|
106
|
+
routerMode === 'code-router' ? codeRouterAddOns : fileRouterAddOns
|
|
107
|
+
return [
|
|
108
|
+
...baseAddOns,
|
|
109
|
+
...customAddOns.filter((addOn) => addOn.modes.includes(routerMode)),
|
|
110
|
+
]
|
|
111
|
+
}, [routerMode, codeRouterAddOns, fileRouterAddOns, customAddOns])
|
|
112
|
+
|
|
113
|
+
const addOnState = useMemo(() => {
|
|
114
|
+
const originalAddOns: Set<string> = new Set()
|
|
115
|
+
for (const addOn of projectStarter?.dependsOn || []) {
|
|
116
|
+
originalAddOns.add(addOn)
|
|
117
|
+
}
|
|
118
|
+
for (const addOn of originalSelectedAddOns) {
|
|
119
|
+
originalAddOns.add(addOn)
|
|
120
|
+
}
|
|
121
|
+
return getAddOnStatus(
|
|
122
|
+
availableAddOns,
|
|
123
|
+
userSelectedAddOns,
|
|
124
|
+
Array.from(originalAddOns),
|
|
125
|
+
)
|
|
126
|
+
}, [
|
|
127
|
+
availableAddOns,
|
|
128
|
+
userSelectedAddOns,
|
|
129
|
+
originalSelectedAddOns,
|
|
130
|
+
projectStarter?.dependsOn,
|
|
131
|
+
])
|
|
132
|
+
|
|
133
|
+
const chosenAddOns = useMemo(() => {
|
|
134
|
+
return Object.keys(addOnState).filter((addOn) => addOnState[addOn].selected)
|
|
135
|
+
}, [addOnState])
|
|
136
|
+
|
|
137
|
+
const toggleAddOn = useCallback(
|
|
138
|
+
(addOnId: string) => {
|
|
139
|
+
if (addOnState[addOnId] && addOnState[addOnId].enabled) {
|
|
140
|
+
if (addOnState[addOnId].selected) {
|
|
141
|
+
useMutableAddOns.setState((state) => ({
|
|
142
|
+
userSelectedAddOns: state.userSelectedAddOns.filter(
|
|
143
|
+
(addOn) => addOn !== addOnId,
|
|
144
|
+
),
|
|
145
|
+
}))
|
|
146
|
+
} else {
|
|
147
|
+
useMutableAddOns.setState((state) => ({
|
|
148
|
+
userSelectedAddOns: [...state.userSelectedAddOns, addOnId],
|
|
149
|
+
}))
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
[addOnState],
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
toggleAddOn,
|
|
158
|
+
chosenAddOns,
|
|
159
|
+
availableAddOns,
|
|
160
|
+
userSelectedAddOns,
|
|
161
|
+
originalSelectedAddOns,
|
|
162
|
+
addOnState,
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const useHasProjectStarter = () =>
|
|
167
|
+
useProjectStarter((state) => state.projectStarter === undefined)
|
|
168
|
+
|
|
169
|
+
export const useModeEditable = () => useHasProjectStarter()
|
|
170
|
+
|
|
171
|
+
export const useTypeScriptEditable = () => {
|
|
172
|
+
const hasProjectStarter = useHasProjectStarter()
|
|
173
|
+
const routerMode = useRouterMode()
|
|
174
|
+
return hasProjectStarter && routerMode === 'code-router'
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export const useTailwindEditable = () => {
|
|
178
|
+
const hasProjectStarter = useHasProjectStarter()
|
|
179
|
+
const routerMode = useRouterMode()
|
|
180
|
+
return hasProjectStarter && routerMode === 'code-router'
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export const useProjectName = () =>
|
|
184
|
+
useProjectOptions((state) => state.projectName)
|
|
185
|
+
|
|
186
|
+
export const useRouterMode = () => useProjectOptions((state) => state.mode)
|
|
187
|
+
|
|
188
|
+
export function useFilters() {
|
|
189
|
+
const includedFiles = useApplicationSettings((state) => state.includeFiles)
|
|
190
|
+
|
|
191
|
+
const toggleFilter = useCallback((filter: string) => {
|
|
192
|
+
useApplicationSettings.setState((state) => ({
|
|
193
|
+
includeFiles: state.includeFiles.includes(filter)
|
|
194
|
+
? state.includeFiles.filter((f) => f !== filter)
|
|
195
|
+
: [...state.includeFiles, filter],
|
|
196
|
+
}))
|
|
197
|
+
}, [])
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
includedFiles,
|
|
201
|
+
toggleFilter,
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function useDryRun() {
|
|
206
|
+
const applicationMode = useApplicationMode()
|
|
207
|
+
const projectOptions = useProjectOptions()
|
|
208
|
+
const { userSelectedAddOns, chosenAddOns } = useAddOns()
|
|
209
|
+
const projectStarter = useProjectStarter().projectStarter
|
|
210
|
+
|
|
211
|
+
const { data: dryRunOutput } = useQuery<DryRunOutput>({
|
|
212
|
+
queryKey: [
|
|
213
|
+
'dry-run',
|
|
214
|
+
applicationMode,
|
|
215
|
+
JSON.stringify(projectOptions),
|
|
216
|
+
JSON.stringify(userSelectedAddOns),
|
|
217
|
+
projectStarter?.url,
|
|
218
|
+
],
|
|
219
|
+
queryFn: async () => {
|
|
220
|
+
if (applicationMode === 'none') {
|
|
221
|
+
return {
|
|
222
|
+
files: {},
|
|
223
|
+
commands: [],
|
|
224
|
+
deletedFiles: [],
|
|
225
|
+
}
|
|
226
|
+
} else if (applicationMode === 'setup') {
|
|
227
|
+
return dryRunCreateApp(projectOptions, chosenAddOns, projectStarter)
|
|
228
|
+
} else {
|
|
229
|
+
return dryRunAddToApp(userSelectedAddOns)
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
initialData: {
|
|
233
|
+
files: {},
|
|
234
|
+
commands: [],
|
|
235
|
+
deletedFiles: [],
|
|
236
|
+
},
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
return dryRunOutput
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export const setProjectName = (projectName: string) =>
|
|
243
|
+
useProjectOptions.setState({
|
|
244
|
+
projectName,
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
export const setRouterMode = (mode: Mode) =>
|
|
248
|
+
useProjectOptions.setState({
|
|
249
|
+
mode,
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
export function setTypeScript(typescript: boolean) {
|
|
253
|
+
useProjectOptions.setState({
|
|
254
|
+
typescript,
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function setTailwind(tailwind: boolean) {
|
|
259
|
+
useProjectOptions.setState({
|
|
260
|
+
tailwind,
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function setProjectStarter(starter: StarterInfo | undefined) {
|
|
265
|
+
useProjectStarter.setState(() => ({
|
|
266
|
+
projectStarter: starter,
|
|
267
|
+
}))
|
|
268
|
+
}
|
package/src/styles.css
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
@plugin "tailwindcss-animate";
|
|
4
4
|
|
|
5
|
+
@source inline("bg-green-500");
|
|
6
|
+
|
|
5
7
|
@custom-variant dark (&:is(.dark *));
|
|
6
8
|
|
|
7
9
|
body {
|
|
@@ -128,6 +130,30 @@ code {
|
|
|
128
130
|
--color-sidebar-ring: var(--sidebar-ring);
|
|
129
131
|
}
|
|
130
132
|
|
|
133
|
+
@layer base {
|
|
134
|
+
:root {
|
|
135
|
+
--sidebar-background: 0 0% 98%;
|
|
136
|
+
--sidebar-foreground: 240 5.3% 26.1%;
|
|
137
|
+
--sidebar-primary: 240 5.9% 10%;
|
|
138
|
+
--sidebar-primary-foreground: 0 0% 98%;
|
|
139
|
+
--sidebar-accent: 240 4.8% 95.9%;
|
|
140
|
+
--sidebar-accent-foreground: 240 5.9% 10%;
|
|
141
|
+
--sidebar-border: 220 13% 91%;
|
|
142
|
+
--sidebar-ring: 217.2 91.2% 59.8%;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.dark {
|
|
146
|
+
--sidebar-background: 240 5.9% 10%;
|
|
147
|
+
--sidebar-foreground: 240 4.8% 95.9%;
|
|
148
|
+
--sidebar-primary: 224.3 76.3% 48%;
|
|
149
|
+
--sidebar-primary-foreground: 0 0% 100%;
|
|
150
|
+
--sidebar-accent: 240 3.7% 15.9%;
|
|
151
|
+
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
|
152
|
+
--sidebar-border: 240 3.7% 15.9%;
|
|
153
|
+
--sidebar-ring: 217.2 91.2% 59.8%;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
131
157
|
@layer base {
|
|
132
158
|
* {
|
|
133
159
|
@apply border-border outline-ring/50;
|
|
@@ -136,3 +162,24 @@ code {
|
|
|
136
162
|
@apply bg-background text-foreground;
|
|
137
163
|
}
|
|
138
164
|
}
|
|
165
|
+
|
|
166
|
+
[data-sidebar='group'] {
|
|
167
|
+
background-color: hsl(240 5.9% 6%) !important;
|
|
168
|
+
@apply mb-2 rounded-lg inset-shadow-gray-600 inset-shadow-sm;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
[data-sidebar='content'] {
|
|
172
|
+
@apply p-2 pt-5;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
[data-sidebar='header'] {
|
|
176
|
+
@apply shadow-gray-700 shadow-md z-50;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.cm-changedLine {
|
|
180
|
+
background-color: oklch(0.27 0.005 285.823) !important;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.file-filters {
|
|
184
|
+
background-color: oklch(0.2 0.005 285.823);
|
|
185
|
+
}
|