@tanstack/cta-ui 0.10.0-alpha.19 → 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.
Files changed (71) hide show
  1. package/lib/index.ts +16 -7
  2. package/lib-dist/index.d.ts +6 -1
  3. package/lib-dist/index.js +6 -3
  4. package/package.json +20 -7
  5. package/public/tailwind.svg +1 -0
  6. package/public/tanstack.png +0 -0
  7. package/public/typescript.svg +1 -0
  8. package/src/components/StatusList.tsx +22 -0
  9. package/src/components/add-on-info-dialog.tsx +39 -0
  10. package/src/components/cta-sidebar.tsx +55 -0
  11. package/src/components/custom-add-on-dialog.tsx +79 -0
  12. package/src/components/file-navigator.tsx +205 -0
  13. package/src/components/file-tree.tsx +18 -60
  14. package/src/components/file-viewer.tsx +11 -3
  15. package/src/components/sidebar-items/add-ons.tsx +91 -0
  16. package/src/components/sidebar-items/mode-selector.tsx +55 -0
  17. package/src/components/sidebar-items/project-name.tsx +29 -0
  18. package/src/components/sidebar-items/run-add-ons.tsx +71 -0
  19. package/src/components/sidebar-items/run-create-app.tsx +82 -0
  20. package/src/components/sidebar-items/starter.tsx +115 -0
  21. package/src/components/sidebar-items/typescript-switch.tsx +52 -0
  22. package/src/components/toaster.tsx +29 -0
  23. package/src/components/ui/button.tsx +21 -19
  24. package/src/components/ui/dialog.tsx +25 -20
  25. package/src/components/ui/dropdown-menu.tsx +255 -0
  26. package/src/components/ui/input.tsx +21 -0
  27. package/src/components/ui/label.tsx +22 -0
  28. package/src/components/ui/popover.tsx +46 -0
  29. package/src/components/ui/separator.tsx +28 -0
  30. package/src/components/ui/sheet.tsx +137 -0
  31. package/src/components/ui/sidebar.tsx +726 -0
  32. package/src/components/ui/skeleton.tsx +13 -0
  33. package/src/components/ui/sonner.tsx +23 -0
  34. package/src/components/ui/switch.tsx +29 -0
  35. package/src/components/ui/toggle-group.tsx +11 -11
  36. package/src/components/ui/toggle.tsx +15 -13
  37. package/src/components/ui/tooltip.tsx +61 -0
  38. package/src/components/ui/tree-view.tsx +17 -12
  39. package/src/engine-handling/add-to-app-wrapper.ts +114 -0
  40. package/src/engine-handling/create-app-wrapper.ts +107 -0
  41. package/src/engine-handling/file-helpers.ts +25 -0
  42. package/src/engine-handling/framework-registration.ts +11 -0
  43. package/src/engine-handling/generate-initial-payload.ts +93 -0
  44. package/src/engine-handling/server-environment.ts +13 -0
  45. package/src/file-classes.ts +54 -0
  46. package/src/hooks/use-mobile.ts +19 -0
  47. package/src/hooks/use-streaming-status.ts +70 -0
  48. package/src/lib/api.ts +90 -0
  49. package/src/routeTree.gen.ts +4 -27
  50. package/src/routes/__root.tsx +36 -7
  51. package/src/routes/api/add-to-app.ts +21 -0
  52. package/src/routes/api/create-app.ts +21 -0
  53. package/src/routes/api/dry-run-add-to-app.ts +16 -0
  54. package/src/routes/api/dry-run-create-app.ts +16 -0
  55. package/src/routes/api/initial-payload.ts +10 -0
  56. package/src/routes/api/load-remote-add-on.ts +42 -0
  57. package/src/routes/api/load-starter.ts +47 -0
  58. package/src/routes/api/shutdown.ts +11 -0
  59. package/src/routes/index.tsx +3 -210
  60. package/src/store/add-ons.ts +81 -0
  61. package/src/store/project.ts +268 -0
  62. package/src/styles.css +47 -0
  63. package/src/types.d.ts +87 -0
  64. package/tests/store/add-ons.test.ts +222 -0
  65. package/vitest.config.ts +6 -0
  66. package/.cursorrules +0 -7
  67. package/src/components/Header.tsx +0 -13
  68. package/src/components/applied-add-on.tsx +0 -149
  69. package/src/lib/server-fns.ts +0 -78
  70. package/src/routes/api.demo-names.ts +0 -11
  71. package/src/routes/demo.tanstack-query.tsx +0 -28
package/lib/index.ts CHANGED
@@ -1,17 +1,26 @@
1
1
  import { dirname, resolve } from 'node:path'
2
2
  import { fileURLToPath } from 'node:url'
3
3
 
4
- export function launchUI() {
5
- const projectPath = process.cwd()
4
+ import type { SerializedOptions } from '@tanstack/cta-engine'
6
5
 
7
- process.env.PROJECT_PATH = projectPath
6
+ export function launchUI({
7
+ mode,
8
+ addOns,
9
+ options,
10
+ }: {
11
+ mode: 'add' | 'setup'
12
+ addOns?: Array<string>
13
+ options?: SerializedOptions
14
+ }) {
15
+ const projectPath = process.cwd()
8
16
 
9
- const configPath = resolve(
10
- dirname(fileURLToPath(import.meta.url)),
11
- '../app.config.js',
12
- )
17
+ process.env.CTA_PROJECT_PATH = projectPath
18
+ process.env.CTA_ADD_ONS = addOns?.join(',') || ''
19
+ process.env.CTA_OPTIONS = options ? JSON.stringify(options) : ''
20
+ process.env.CTA_MODE = mode
13
21
 
14
22
  const developerPath = resolve(dirname(fileURLToPath(import.meta.url)), '..')
23
+ const configPath = resolve(developerPath, './app.config.js')
15
24
 
16
25
  process.chdir(developerPath)
17
26
 
@@ -1 +1,6 @@
1
- export declare function launchUI(): void;
1
+ import type { SerializedOptions } from '@tanstack/cta-engine';
2
+ export declare function launchUI({ mode, addOns, options, }: {
3
+ mode: 'add' | 'setup';
4
+ addOns?: Array<string>;
5
+ options?: SerializedOptions;
6
+ }): void;
package/lib-dist/index.js CHANGED
@@ -1,10 +1,13 @@
1
1
  import { dirname, resolve } from 'node:path';
2
2
  import { fileURLToPath } from 'node:url';
3
- export function launchUI() {
3
+ export function launchUI({ mode, addOns, options, }) {
4
4
  const projectPath = process.cwd();
5
- process.env.PROJECT_PATH = projectPath;
6
- const configPath = resolve(dirname(fileURLToPath(import.meta.url)), '../app.config.js');
5
+ process.env.CTA_PROJECT_PATH = projectPath;
6
+ process.env.CTA_ADD_ONS = addOns?.join(',') || '';
7
+ process.env.CTA_OPTIONS = options ? JSON.stringify(options) : '';
8
+ process.env.CTA_MODE = mode;
7
9
  const developerPath = resolve(dirname(fileURLToPath(import.meta.url)), '..');
10
+ const configPath = resolve(developerPath, './app.config.js');
8
11
  process.chdir(developerPath);
9
12
  import(configPath).then(async (config) => {
10
13
  const out = await config.default;
package/package.json CHANGED
@@ -10,11 +10,17 @@
10
10
  "@codemirror/lang-json": "^6.0.1",
11
11
  "@radix-ui/react-accordion": "^1.2.3",
12
12
  "@radix-ui/react-checkbox": "^1.1.4",
13
- "@radix-ui/react-dialog": "^1.1.6",
14
- "@radix-ui/react-slot": "^1.1.2",
13
+ "@radix-ui/react-dialog": "^1.1.10",
14
+ "@radix-ui/react-dropdown-menu": "^2.1.11",
15
+ "@radix-ui/react-label": "^2.1.4",
16
+ "@radix-ui/react-popover": "^1.1.10",
17
+ "@radix-ui/react-separator": "^1.1.4",
18
+ "@radix-ui/react-slot": "^1.2.0",
19
+ "@radix-ui/react-switch": "^1.2.2",
15
20
  "@radix-ui/react-tabs": "^1.1.3",
16
21
  "@radix-ui/react-toggle": "^1.1.2",
17
22
  "@radix-ui/react-toggle-group": "^1.1.2",
23
+ "@radix-ui/react-tooltip": "^1.2.3",
18
24
  "@tailwindcss/vite": "^4.0.6",
19
25
  "@tanstack/react-query": "^5.66.5",
20
26
  "@tanstack/react-query-devtools": "^5.66.5",
@@ -22,37 +28,44 @@
22
28
  "@tanstack/react-router-devtools": "^1.114.3",
23
29
  "@tanstack/react-router-with-query": "^1.114.3",
24
30
  "@tanstack/react-start": "^1.114.3",
31
+ "@tanstack/react-store": "^0.7.0",
25
32
  "@tanstack/router-plugin": "^1.114.3",
26
- "@uiw/codemirror-theme-okaidia": "^4.23.10",
33
+ "@uiw/codemirror-theme-github": "^4.23.10",
27
34
  "@uiw/react-codemirror": "^4.23.10",
28
35
  "class-variance-authority": "^0.7.1",
29
36
  "clsx": "^2.1.1",
30
37
  "execa": "^9.5.2",
38
+ "jotai-tanstack-query": "^0.9.0",
31
39
  "lucide-react": "^0.476.0",
40
+ "next-themes": "^0.4.6",
32
41
  "react": "^19.0.0",
33
42
  "react-codemirror-merge": "^4.23.10",
34
43
  "react-dom": "^19.0.0",
44
+ "sonner": "^2.0.3",
35
45
  "tailwind-merge": "^3.0.2",
36
46
  "tailwindcss": "^4.0.6",
37
47
  "tailwindcss-animate": "^1.0.7",
38
48
  "vinxi": "^0.5.3",
39
49
  "vite-tsconfig-paths": "^5.1.4",
40
- "@tanstack/cta-engine": "0.10.0-alpha.19",
41
- "@tanstack/cta-custom-add-on": "0.10.0-alpha.19"
50
+ "zustand": "^5.0.3",
51
+ "@tanstack/cta-framework-react-cra": "0.10.0-alpha.20",
52
+ "@tanstack/cta-engine": "0.10.0-alpha.20",
53
+ "@tanstack/cta-framework-solid": "0.10.0-alpha.20"
42
54
  },
43
55
  "devDependencies": {
44
56
  "@testing-library/dom": "^10.4.0",
45
57
  "@testing-library/react": "^16.2.0",
46
- "@types/node": "^22.13.14",
58
+ "@types/node": "^22.14.1",
47
59
  "@types/react": "^19.0.8",
48
60
  "@types/react-dom": "^19.0.3",
49
61
  "@vitejs/plugin-react": "^4.3.4",
62
+ "@vitest/coverage-v8": "3.1.1",
50
63
  "jsdom": "^26.0.0",
51
64
  "typescript": "^5.7.2",
52
65
  "vite": "^6.1.0",
53
66
  "vitest": "^3.0.5",
54
67
  "web-vitals": "^4.2.4"
55
68
  },
56
- "version": "0.10.0-alpha.19",
69
+ "version": "0.10.0-alpha.20",
57
70
  "scripts": {}
58
71
  }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 54 33"><g clip-path="url(#a)"><path fill="#38bdf8" fill-rule="evenodd" d="M27 0c-7.2 0-11.7 3.6-13.5 10.8 2.7-3.6 5.85-4.95 9.45-4.05 2.054.513 3.522 2.004 5.147 3.653C30.744 13.09 33.808 16.2 40.5 16.2c7.2 0 11.7-3.6 13.5-10.8-2.7 3.6-5.85 4.95-9.45 4.05-2.054-.513-3.522-2.004-5.147-3.653C36.756 3.11 33.692 0 27 0zM13.5 16.2C6.3 16.2 1.8 19.8 0 27c2.7-3.6 5.85-4.95 9.45-4.05 2.054.514 3.522 2.004 5.147 3.653C17.244 29.29 20.308 32.4 27 32.4c7.2 0 11.7-3.6 13.5-10.8-2.7 3.6-5.85 4.95-9.45 4.05-2.054-.513-3.522-2.004-5.147-3.653C23.256 19.31 20.192 16.2 13.5 16.2z" clip-rule="evenodd"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h54v32.4H0z"/></clipPath></defs></svg>
Binary file
@@ -0,0 +1 @@
1
+ <svg viewBox="0 0 256 256" width="256" height="256" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid"><path d="M20 0h216c11.046 0 20 8.954 20 20v216c0 11.046-8.954 20-20 20H20c-11.046 0-20-8.954-20-20V20C0 8.954 8.954 0 20 0Z" fill="#3178C6"/><path d="M150.518 200.475v27.62c4.492 2.302 9.805 4.028 15.938 5.179 6.133 1.151 12.597 1.726 19.393 1.726 6.622 0 12.914-.633 18.874-1.899 5.96-1.266 11.187-3.352 15.678-6.257 4.492-2.906 8.048-6.704 10.669-11.394 2.62-4.689 3.93-10.486 3.93-17.391 0-5.006-.749-9.394-2.246-13.163a30.748 30.748 0 0 0-6.479-10.055c-2.821-2.935-6.205-5.567-10.149-7.898-3.945-2.33-8.394-4.531-13.347-6.602-3.628-1.497-6.881-2.949-9.761-4.359-2.879-1.41-5.327-2.848-7.342-4.316-2.016-1.467-3.571-3.021-4.665-4.661-1.094-1.64-1.641-3.495-1.641-5.567 0-1.899.489-3.61 1.468-5.135s2.362-2.834 4.147-3.927c1.785-1.094 3.973-1.942 6.565-2.547 2.591-.604 5.471-.906 8.638-.906 2.304 0 4.737.173 7.299.518 2.563.345 5.14.877 7.732 1.597a53.669 53.669 0 0 1 7.558 2.719 41.7 41.7 0 0 1 6.781 3.797v-25.807c-4.204-1.611-8.797-2.805-13.778-3.582-4.981-.777-10.697-1.165-17.147-1.165-6.565 0-12.784.705-18.658 2.115-5.874 1.409-11.043 3.61-15.506 6.602-4.463 2.993-7.99 6.805-10.582 11.437-2.591 4.632-3.887 10.17-3.887 16.615 0 8.228 2.375 15.248 7.127 21.06 4.751 5.811 11.963 10.731 21.638 14.759a291.458 291.458 0 0 1 10.625 4.575c3.283 1.496 6.119 3.049 8.509 4.66 2.39 1.611 4.276 3.366 5.658 5.265 1.382 1.899 2.073 4.057 2.073 6.474a9.901 9.901 0 0 1-1.296 4.963c-.863 1.524-2.174 2.848-3.93 3.97-1.756 1.122-3.945 1.999-6.565 2.632-2.62.633-5.687.95-9.2.95-5.989 0-11.92-1.05-17.794-3.151-5.875-2.1-11.317-5.25-16.327-9.451Zm-46.036-68.733H140V109H41v22.742h35.345V233h28.137V131.742Z" fill="#FFF"/></svg>
@@ -0,0 +1,22 @@
1
+ import type { StreamItem } from '@/types'
2
+
3
+ export default function StatusList({
4
+ streamItems,
5
+ finished,
6
+ }: {
7
+ streamItems: Array<StreamItem>
8
+ finished: boolean
9
+ }) {
10
+ return (
11
+ <div className="flex flex-col gap-2">
12
+ {streamItems.map((item, index) => (
13
+ <div key={item.id} className="flex items-center gap-2">
14
+ <item.icon
15
+ className={`w-4 h-4 ${index === streamItems.length - 1 && !finished ? 'text-green-500 animate-spin' : ''}`}
16
+ />
17
+ {item.message}
18
+ </div>
19
+ ))}
20
+ </div>
21
+ )
22
+ }
@@ -0,0 +1,39 @@
1
+ import type { AddOnInfo } from '@/types'
2
+
3
+ import { Dialog, DialogContent } from '@/components/ui/dialog'
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,55 @@
1
+ import {
2
+ Sidebar,
3
+ SidebarContent,
4
+ SidebarFooter,
5
+ SidebarGroup,
6
+ SidebarHeader,
7
+ } from '@/components/ui/sidebar'
8
+
9
+ import SelectedAddOns from '@/components/sidebar-items/add-ons'
10
+ import RunAddOns from '@/components/sidebar-items/run-add-ons'
11
+ import RunCreateApp from '@/components/sidebar-items/run-create-app'
12
+ import ProjectName from '@/components/sidebar-items/project-name'
13
+ import ModeSelector from '@/components/sidebar-items/mode-selector'
14
+ import TypescriptSwitch from '@/components/sidebar-items/typescript-switch'
15
+ import StarterDialog from '@/components/sidebar-items/starter'
16
+
17
+ import { useApplicationMode, useReady } from '@/store/project'
18
+
19
+ export function AppSidebar() {
20
+ const ready = useReady()
21
+ const mode = useApplicationMode()
22
+
23
+ return (
24
+ <Sidebar>
25
+ <SidebarHeader className="flex justify-center items-center">
26
+ <img src="/tanstack.png" className="w-3/5" />
27
+ </SidebarHeader>
28
+ <SidebarContent>
29
+ {ready && (
30
+ <>
31
+ {mode === 'setup' && (
32
+ <SidebarGroup>
33
+ <ProjectName />
34
+ <ModeSelector />
35
+ <TypescriptSwitch />
36
+ </SidebarGroup>
37
+ )}
38
+ <SidebarGroup>
39
+ <SelectedAddOns />
40
+ </SidebarGroup>
41
+ {mode === 'setup' && (
42
+ <SidebarGroup>
43
+ <StarterDialog />
44
+ </SidebarGroup>
45
+ )}
46
+ </>
47
+ )}
48
+ </SidebarContent>
49
+ <SidebarFooter className="mb-5">
50
+ <RunAddOns />
51
+ <RunCreateApp />
52
+ </SidebarFooter>
53
+ </Sidebar>
54
+ )
55
+ }
@@ -0,0 +1,79 @@
1
+ import { useState } from 'react'
2
+ import { toast } from 'sonner'
3
+ import { TicketPlusIcon } from 'lucide-react'
4
+
5
+ import { Button } from '@/components/ui/button'
6
+ import { Input } from '@/components/ui/input'
7
+ import {
8
+ Dialog,
9
+ DialogContent,
10
+ DialogFooter,
11
+ DialogHeader,
12
+ DialogTitle,
13
+ } from '@/components/ui/dialog'
14
+
15
+ import { addCustomAddOn, useAddOns, useRouterMode } from '@/store/project'
16
+ import { loadRemoteAddOn } from '@/lib/api'
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}>
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,205 @@
1
+ import { useMemo, useState } from 'react'
2
+ import { FileText, Folder } from 'lucide-react'
3
+
4
+ import FileViewer from './file-viewer'
5
+ import FileTree from './file-tree'
6
+
7
+ import type { FileTreeItem } from '@/types'
8
+
9
+ import { Label } from '@/components/ui/label'
10
+ import { Checkbox } from '@/components/ui/checkbox'
11
+
12
+ import {
13
+ useApplicationMode,
14
+ useDryRun,
15
+ useFilters,
16
+ useOriginalOutput,
17
+ useProjectLocalFiles,
18
+ useReady,
19
+ } from '@/store/project'
20
+
21
+ import { getFileClass, twClasses } from '@/file-classes'
22
+
23
+ export function Filters() {
24
+ const { includedFiles, toggleFilter } = useFilters()
25
+
26
+ return (
27
+ <div className="p-2 rounded-md bg-gray-900 file-filters">
28
+ <div className="text-center text-sm mb-2">File Filters</div>
29
+ <div className="flex flex-row flex-wrap gap-y-2">
30
+ <div className="flex flex-row items-center gap-2 w-1/3">
31
+ <Checkbox
32
+ id="unchanged"
33
+ checked={includedFiles.includes('unchanged')}
34
+ className="w-4 h-4"
35
+ onCheckedChange={() => toggleFilter('unchanged')}
36
+ />
37
+ <Label htmlFor="unchanged" className={twClasses.unchanged}>
38
+ Unchanged
39
+ </Label>
40
+ </div>
41
+ <div className="flex flex-row items-center gap-2 w-1/3">
42
+ <Checkbox
43
+ id="added"
44
+ checked={includedFiles.includes('added')}
45
+ className="w-4 h-4"
46
+ onCheckedChange={() => toggleFilter('added')}
47
+ />
48
+ <Label htmlFor="added" className={twClasses.added}>
49
+ Added
50
+ </Label>
51
+ </div>
52
+ <div className="flex flex-row items-center gap-2 w-1/3">
53
+ <Checkbox
54
+ id="modified"
55
+ checked={includedFiles.includes('modified')}
56
+ className="w-4 h-4"
57
+ onCheckedChange={() => toggleFilter('modified')}
58
+ />
59
+ <Label htmlFor="modified" className={twClasses.modified}>
60
+ Modified
61
+ </Label>
62
+ </div>
63
+ <div className="flex flex-row items-center gap-2 w-1/3">
64
+ <Checkbox
65
+ id="deleted"
66
+ checked={includedFiles.includes('deleted')}
67
+ className="w-4 h-4"
68
+ onCheckedChange={() => toggleFilter('deleted')}
69
+ />
70
+ <Label htmlFor="deleted" className={twClasses.deleted}>
71
+ Deleted
72
+ </Label>
73
+ </div>
74
+ <div className="flex flex-row items-center gap-2 w-1/3">
75
+ <Checkbox
76
+ id="overwritten"
77
+ checked={includedFiles.includes('overwritten')}
78
+ className="w-4 h-4"
79
+ onCheckedChange={() => toggleFilter('overwritten')}
80
+ />
81
+ <Label htmlFor="overwritten" className={twClasses.overwritten}>
82
+ Overwritten
83
+ </Label>
84
+ </div>
85
+ </div>
86
+ </div>
87
+ )
88
+ }
89
+
90
+ export default function FileNavigator() {
91
+ const [selectedFile, setSelectedFile] = useState<string | null>(
92
+ './package.json',
93
+ )
94
+
95
+ const projectFiles = useOriginalOutput()
96
+ const localTree = useProjectLocalFiles()
97
+ const dryRunOutput = useDryRun()
98
+
99
+ const mode = useApplicationMode()
100
+
101
+ const tree = dryRunOutput.files
102
+ const originalTree =
103
+ mode === 'setup' ? dryRunOutput.files : projectFiles.files
104
+ const deletedFiles = dryRunOutput.deletedFiles
105
+
106
+ const [originalFileContents, setOriginalFileContents] = useState<string>()
107
+ const [modifiedFileContents, setModifiedFileContents] = useState<string>()
108
+
109
+ const { includedFiles } = useFilters()
110
+
111
+ const fileTree = useMemo(() => {
112
+ const treeData: Array<FileTreeItem> = []
113
+
114
+ const allFileSet = Array.from(
115
+ new Set([
116
+ ...Object.keys(tree),
117
+ ...Object.keys(localTree),
118
+ ...Object.keys(originalTree),
119
+ ]),
120
+ )
121
+
122
+ allFileSet.sort().forEach((file) => {
123
+ const strippedFile = file.replace('./', '')
124
+ const parts = strippedFile.split('/')
125
+
126
+ let currentLevel = treeData
127
+ parts.forEach((part, index) => {
128
+ const existingNode = currentLevel.find((node) => node.name === part)
129
+ if (existingNode) {
130
+ currentLevel = existingNode.children || []
131
+ } else {
132
+ const fileInfo = getFileClass(
133
+ file,
134
+ tree,
135
+ originalTree,
136
+ localTree,
137
+ deletedFiles,
138
+ )
139
+
140
+ if (
141
+ index === parts.length - 1 &&
142
+ !includedFiles.includes(fileInfo.fileClass)
143
+ ) {
144
+ return
145
+ }
146
+ if (index === parts.length - 1 && file === selectedFile) {
147
+ setModifiedFileContents(fileInfo.modifiedFile)
148
+ setOriginalFileContents(fileInfo.originalFile)
149
+ }
150
+
151
+ const newNode: FileTreeItem = {
152
+ id: parts.slice(0, index + 1).join('/'),
153
+ name: part,
154
+ fullPath: strippedFile,
155
+ children: index < parts.length - 1 ? [] : undefined,
156
+ icon:
157
+ index < parts.length - 1
158
+ ? () => <Folder className="w-4 h-4 mr-2" />
159
+ : () => <FileText className="w-4 h-4 mr-2" />,
160
+ onClick:
161
+ index === parts.length - 1
162
+ ? () => {
163
+ setSelectedFile(file)
164
+ setModifiedFileContents(fileInfo.modifiedFile)
165
+ setOriginalFileContents(fileInfo.originalFile)
166
+ }
167
+ : undefined,
168
+ className: twClasses[fileInfo.fileClass],
169
+ ...fileInfo,
170
+ contents: tree[file] || localTree[file] || originalTree[file],
171
+ }
172
+ currentLevel.push(newNode)
173
+ currentLevel = newNode.children!
174
+ }
175
+ })
176
+ })
177
+ return treeData
178
+ }, [tree, originalTree, localTree, includedFiles])
179
+
180
+ const ready = useReady()
181
+
182
+ if (!ready) {
183
+ return null
184
+ }
185
+
186
+ return (
187
+ <div
188
+ className={`flex flex-row border-1 rounded-md mr-10 p-2 inset-shadow-gray-600 inset-shadow-sm`}
189
+ >
190
+ <div className="w-1/4 max-w-1/4 pr-2">
191
+ {mode === 'add' && <Filters />}
192
+ <FileTree selectedFile={selectedFile} tree={fileTree} />
193
+ </div>
194
+ <div className="max-w-3/4 w-3/4 pl-2">
195
+ {selectedFile && modifiedFileContents ? (
196
+ <FileViewer
197
+ filePath={selectedFile}
198
+ originalFile={originalFileContents}
199
+ modifiedFile={modifiedFileContents}
200
+ />
201
+ ) : null}
202
+ </div>
203
+ </div>
204
+ )
205
+ }
@@ -1,77 +1,35 @@
1
1
  import { useMemo } from 'react'
2
2
  import { FileText, Folder } from 'lucide-react'
3
3
 
4
- import { TreeView } from '@/components/ui/tree-view'
4
+ import type { FileTreeItem } from '@/types.js'
5
5
 
6
- import type { TreeDataItem } from '@/components/ui/tree-view'
6
+ import { TreeView } from '@/components/ui/tree-view'
7
7
 
8
8
  export default function FileTree({
9
- prefix,
9
+ selectedFile,
10
10
  tree,
11
- originalTree,
12
- onFileSelected,
13
- extraTreeItems = [],
14
11
  }: {
15
- prefix: string
16
- tree: Record<string, string>
17
- originalTree: Record<string, string>
18
- onFileSelected: (file: string) => void
19
- extraTreeItems?: Array<TreeDataItem>
12
+ selectedFile: string | null
13
+ tree: Array<FileTreeItem>
20
14
  }) {
21
- const computedTree = useMemo(() => {
22
- const treeData: Array<TreeDataItem> = []
23
-
24
- function changed(file: string) {
25
- if (!originalTree[file]) {
26
- return true
27
- }
28
- return tree[file] !== originalTree[file]
29
- }
30
-
31
- Object.keys(tree)
32
- .sort()
33
- .forEach((file) => {
34
- const parts = file.replace(`${prefix}/`, '').split('/')
35
-
36
- let currentLevel = treeData
37
- parts.forEach((part, index) => {
38
- const existingNode = currentLevel.find((node) => node.name === part)
39
- if (existingNode) {
40
- currentLevel = existingNode.children || []
41
- } else {
42
- const newNode: TreeDataItem = {
43
- id: index === parts.length - 1 ? file : `${file}-${index}`,
44
- name: part,
45
- children: index < parts.length - 1 ? [] : undefined,
46
- icon:
47
- index < parts.length - 1
48
- ? () => <Folder className="w-4 h-4 mr-2" />
49
- : () => <FileText className="w-4 h-4 mr-2" />,
50
- onClick:
51
- index === parts.length - 1
52
- ? () => {
53
- onFileSelected(file)
54
- }
55
- : undefined,
56
- className:
57
- index === parts.length - 1 && changed(file)
58
- ? 'text-green-300'
59
- : '',
60
- }
61
- currentLevel.push(newNode)
62
- currentLevel = newNode.children!
63
- }
64
- })
65
- })
66
- return [...extraTreeItems, ...treeData]
67
- }, [prefix, tree, originalTree, extraTreeItems])
15
+ const initialExpandedItemIds = useMemo(
16
+ () => [
17
+ 'src',
18
+ 'src/routes',
19
+ 'src/components',
20
+ 'src/components/ui',
21
+ 'src/lib',
22
+ ],
23
+ [],
24
+ )
68
25
 
69
26
  return (
70
27
  <TreeView
71
- data={computedTree}
28
+ initialSelectedItemId={selectedFile?.replace('./', '') ?? undefined}
29
+ initialExpandedItemIds={initialExpandedItemIds}
30
+ data={tree}
72
31
  defaultNodeIcon={() => <Folder className="w-4 h-4 mr-2" />}
73
32
  defaultLeafIcon={() => <FileText className="w-4 h-4 mr-2" />}
74
- className="max-w-1/4 w-1/4 pr-2"
75
33
  />
76
34
  )
77
35
  }
@@ -6,7 +6,15 @@ import { json } from '@codemirror/lang-json'
6
6
  import { css } from '@codemirror/lang-css'
7
7
  import { html } from '@codemirror/lang-html'
8
8
 
9
- import { okaidia } from '@uiw/codemirror-theme-okaidia'
9
+ import { githubDarkInit } from '@uiw/codemirror-theme-github'
10
+
11
+ const theme = githubDarkInit({
12
+ settings: {
13
+ background: 'oklch(0.07 0.005 285.823)',
14
+ foreground: '#c9d1d9',
15
+ gutterBackground: 'oklch(0.22 0.005 285.823)',
16
+ },
17
+ })
10
18
 
11
19
  export default function FileViewer({
12
20
  originalFile,
@@ -41,7 +49,7 @@ export default function FileViewer({
41
49
  return (
42
50
  <CodeMirror
43
51
  value={modifiedFile}
44
- theme={okaidia}
52
+ theme={theme}
45
53
  height="100vh"
46
54
  width="100%"
47
55
  readOnly
@@ -51,7 +59,7 @@ export default function FileViewer({
51
59
  )
52
60
  }
53
61
  return (
54
- <CodeMirrorMerge orientation="a-b" theme={okaidia} className="text-lg">
62
+ <CodeMirrorMerge orientation="a-b" theme={theme} className="text-lg">
55
63
  <CodeMirrorMerge.Original value={originalFile} extensions={[language]} />
56
64
  <CodeMirrorMerge.Modified value={modifiedFile} extensions={[language]} />
57
65
  </CodeMirrorMerge>