@tscircuit/fake-snippets 0.0.73 → 0.0.75

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 (34) hide show
  1. package/dist/bundle.js +250 -202
  2. package/dist/index.d.ts +72 -2
  3. package/dist/index.js +24 -7
  4. package/dist/schema.d.ts +112 -0
  5. package/dist/schema.js +18 -1
  6. package/fake-snippets-api/lib/db/db-client.ts +11 -7
  7. package/fake-snippets-api/lib/db/schema.ts +25 -0
  8. package/fake-snippets-api/routes/api/package_releases/rebuild.ts +32 -0
  9. package/fake-snippets-api/routes/api/package_releases/update.ts +4 -1
  10. package/package.json +1 -1
  11. package/src/components/DownloadButtonAndMenu.tsx +26 -8
  12. package/src/components/Header.tsx +7 -7
  13. package/src/components/PackageBuildsPage/ErrorObject.ts +12 -0
  14. package/src/components/PackageBuildsPage/LogContent.tsx +32 -0
  15. package/src/components/PackageBuildsPage/PackageBuildDetailsPage.tsx +114 -0
  16. package/src/components/PackageBuildsPage/build-preview-content.tsx +21 -5
  17. package/src/components/PackageBuildsPage/capitalCase.ts +4 -0
  18. package/src/components/PackageBuildsPage/collapsible-section.tsx +34 -9
  19. package/src/components/PackageBuildsPage/getColorForDisplayStatus.ts +17 -0
  20. package/src/components/PackageBuildsPage/package-build-details-panel.tsx +147 -0
  21. package/src/components/PackageBuildsPage/{deployment-header.tsx → package-build-header.tsx} +16 -31
  22. package/src/components/ViewPackagePage/components/main-content-header.tsx +1 -1
  23. package/src/components/ViewPackagePage/components/package-header.tsx +0 -1
  24. package/src/components/dialogs/pcb-download-dialog.tsx +113 -0
  25. package/src/components/package-port/CodeAndPreview.tsx +3 -2
  26. package/src/components/package-port/CodeEditor.tsx +4 -2
  27. package/src/hooks/use-create-package-mutation.ts +0 -7
  28. package/src/hooks/use-current-package-release.ts +28 -0
  29. package/src/hooks/useFileManagement.ts +26 -21
  30. package/src/lib/download-fns/download-pcb-svg.ts +35 -0
  31. package/src/lib/utils/timeAgo.ts +14 -3
  32. package/src/pages/package-builds.tsx +2 -2
  33. package/src/components/PackageBuildsPage/DeploymentDetailsPage.tsx +0 -56
  34. package/src/components/PackageBuildsPage/deployment-details-panel.tsx +0 -84
@@ -2,14 +2,9 @@ import { HeaderLogin } from "@/components/HeaderLogin"
2
2
  import { Button } from "@/components/ui/button"
3
3
  import { useGlobalStore } from "@/hooks/use-global-store"
4
4
  import { cn } from "@/lib/utils"
5
- import {
6
- GitHubLogoIcon,
7
- OpenInNewWindowIcon,
8
- ChatBubbleIcon,
9
- DiscordLogoIcon,
10
- } from "@radix-ui/react-icons"
5
+ import { GitHubLogoIcon, DiscordLogoIcon } from "@radix-ui/react-icons"
11
6
  import { Menu, X } from "lucide-react"
12
- import React, { useState } from "react"
7
+ import React, { useEffect, useState } from "react"
13
8
  import { useLocation } from "wouter"
14
9
  import { PrefetchPageLink } from "./PrefetchPageLink"
15
10
  import CmdKMenu from "./CmdKMenu"
@@ -55,6 +50,11 @@ const HeaderButton = ({
55
50
  export default function Header() {
56
51
  const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
57
52
  const isLoggedIn = useGlobalStore((s) => Boolean(s.session))
53
+ const sessionToken = useGlobalStore((s) => s.session?.token)
54
+
55
+ useEffect(() => {
56
+ window.TSCIRCUIT_REGISTRY_TOKEN = sessionToken ?? ""
57
+ }, [sessionToken])
58
58
 
59
59
  return (
60
60
  <header className="px-4 py-3">
@@ -0,0 +1,12 @@
1
+ export type ErrorObjectOrString =
2
+ | {
3
+ message: string
4
+ }
5
+ | string
6
+
7
+ export const getErrorText = (error: ErrorObjectOrString) => {
8
+ if (typeof error === "string") {
9
+ return error
10
+ }
11
+ return error.message
12
+ }
@@ -0,0 +1,32 @@
1
+ type ErrorObject =
2
+ | {
3
+ message: string
4
+ }
5
+ | string
6
+
7
+ const getErrorText = (error: ErrorObject | string) => {
8
+ if (typeof error === "string") {
9
+ return error
10
+ }
11
+ return error.message
12
+ }
13
+
14
+ export const LogContent = ({
15
+ logs,
16
+ error,
17
+ }: { logs: any[]; error?: ErrorObject | string | null }) => {
18
+ return (
19
+ <div className="whitespace-pre-wrap font-mono text-xs">
20
+ {logs.map((log) =>
21
+ log.msg || log.message ? (
22
+ <div>{log.msg ?? log.message}</div>
23
+ ) : (
24
+ <div>
25
+ <pre>{log.message}</pre>
26
+ </div>
27
+ ),
28
+ )}
29
+ {error && <div className="text-red-600">{getErrorText(error)}</div>}
30
+ </div>
31
+ )
32
+ }
@@ -0,0 +1,114 @@
1
+ "use client"
2
+
3
+ import { useState } from "react"
4
+ import { BuildPreviewContent } from "./build-preview-content"
5
+ import { PackageBuildDetailsPanel } from "./package-build-details-panel"
6
+ import { PackageBuildHeader } from "./package-build-header"
7
+ import { CollapsibleSection } from "./collapsible-section"
8
+ import { useCurrentPackageRelease } from "@/hooks/use-current-package-release"
9
+ import { LogContent } from "./LogContent"
10
+ import { PackageRelease } from "fake-snippets-api/lib/db/schema"
11
+
12
+ function computeDuration(
13
+ startedAt: string | null | undefined,
14
+ completedAt: string | null | undefined,
15
+ ) {
16
+ if (!startedAt || !completedAt) return ""
17
+ return `${Math.floor((new Date(completedAt).getTime() - new Date(startedAt).getTime()) / 1000)}s`
18
+ }
19
+
20
+ export const PackageBuildDetailsPage = () => {
21
+ const { packageRelease } = useCurrentPackageRelease()
22
+ const [openSections, setOpenSections] = useState<Record<string, boolean>>({})
23
+
24
+ const {
25
+ circuit_json_build_logs,
26
+ circuit_json_build_completed_at,
27
+ circuit_json_build_in_progress,
28
+ circuit_json_build_is_stale,
29
+ circuit_json_build_started_at,
30
+ circuit_json_build_error,
31
+ circuit_json_build_error_last_updated_at,
32
+ transpilation_completed_at,
33
+ transpilation_in_progress,
34
+ transpilation_is_stale,
35
+ transpilation_logs,
36
+ transpilation_started_at,
37
+ circuit_json_build_display_status,
38
+ transpilation_display_status,
39
+ transpilation_error,
40
+ } = packageRelease ?? ({} as Partial<PackageRelease>)
41
+
42
+ const toggleSection = (section: string) => {
43
+ setOpenSections((prev) => ({
44
+ ...prev,
45
+ [section]: !prev[section],
46
+ }))
47
+ }
48
+
49
+ return (
50
+ <div className="min-h-screen bg-gray-50 text-gray-900">
51
+ <PackageBuildHeader />
52
+
53
+ <div className="px-6 py-6 container mx-auto">
54
+ {/* Main Content */}
55
+ <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8 items-start">
56
+ {/* Preview Section */}
57
+ <div className="lg:col-span-2">
58
+ <div className="bg-white border border-gray-200 rounded-lg p-4 flex items-center justify-center max-h-[420px]">
59
+ <BuildPreviewContent />
60
+ </div>
61
+ </div>
62
+
63
+ {/* Details Panel */}
64
+ <PackageBuildDetailsPanel />
65
+ </div>
66
+
67
+ {/* Collapsible Sections */}
68
+ <div className="space-y-4 mb-8">
69
+ <CollapsibleSection
70
+ title="Transpilation Logs"
71
+ duration={computeDuration(
72
+ transpilation_started_at,
73
+ transpilation_completed_at,
74
+ )}
75
+ displayStatus={transpilation_display_status}
76
+ error={transpilation_error}
77
+ isOpen={openSections.summary}
78
+ onToggle={() => toggleSection("summary")}
79
+ >
80
+ <LogContent
81
+ logs={
82
+ packageRelease?.transpilation_logs ?? [
83
+ { msg: "No transpilation logs available" },
84
+ ]
85
+ }
86
+ error={transpilation_error}
87
+ />
88
+ </CollapsibleSection>
89
+
90
+ <CollapsibleSection
91
+ title="Circuit JSON Build Logs"
92
+ duration={computeDuration(
93
+ circuit_json_build_started_at,
94
+ circuit_json_build_completed_at,
95
+ )}
96
+ displayStatus={circuit_json_build_display_status}
97
+ error={circuit_json_build_error}
98
+ isOpen={openSections.logs}
99
+ onToggle={() => toggleSection("logs")}
100
+ >
101
+ <LogContent
102
+ logs={
103
+ packageRelease?.circuit_json_build_logs ?? [
104
+ { msg: "No Circuit JSON logs available" },
105
+ ]
106
+ }
107
+ error={circuit_json_build_error!}
108
+ />
109
+ </CollapsibleSection>
110
+ </div>
111
+ </div>
112
+ </div>
113
+ )
114
+ }
@@ -1,11 +1,27 @@
1
+ import { useCurrentPackageInfo } from "@/hooks/use-current-package-info"
2
+ import { useCurrentPackageRelease } from "@/hooks/use-current-package-release"
3
+
1
4
  export function BuildPreviewContent() {
5
+ const { packageRelease } = useCurrentPackageRelease()
6
+ const { packageInfo } = useCurrentPackageInfo()
7
+
8
+ if (!packageRelease) {
9
+ return (
10
+ <div className="flex items-center justify-center">
11
+ <div className="w-48 h-48 bg-gray-200 rounded animate-pulse-slow"></div>
12
+ </div>
13
+ )
14
+ }
15
+
2
16
  return (
3
17
  <div className="flex items-center justify-center">
4
- <img
5
- src="https://placehold.co/600x400/000/31343C"
6
- alt="Deployment preview"
7
- className="object-contain rounded p-2 max-h-[400px]"
8
- />
18
+ <div className="rounded overflow-hidden">
19
+ <img
20
+ src={`https://api.tscircuit.com/packages/images/${packageInfo?.name}/pcb.png`}
21
+ alt="Package build preview"
22
+ className="object-contain rounded max-h-[360px]"
23
+ />
24
+ </div>
9
25
  </div>
10
26
  )
11
27
  }
@@ -0,0 +1,4 @@
1
+ export const capitalCase = (str: string | null | undefined) => {
2
+ if (!str) return ""
3
+ return str.charAt(0).toUpperCase() + str.slice(1)
4
+ }
@@ -6,24 +6,34 @@ import {
6
6
  CollapsibleContent,
7
7
  CollapsibleTrigger,
8
8
  } from "@/components/ui/collapsible"
9
+ import { getColorForDisplayStatus } from "./getColorForDisplayStatus"
10
+ import { PackageRelease } from "fake-snippets-api/lib/db/schema"
11
+ import { ErrorObjectOrString, getErrorText } from "./ErrorObject"
12
+ import { capitalCase } from "./capitalCase"
13
+
14
+ type BadgeInfo = {
15
+ text: string
16
+ variant?: "default" | "secondary" | "destructive"
17
+ className?: string
18
+ icon?: React.ReactNode
19
+ }
9
20
 
10
21
  interface CollapsibleSectionProps {
11
22
  title: string
12
23
  duration?: string
24
+ error?: ErrorObjectOrString | null
25
+ displayStatus?: PackageRelease["display_status"]
13
26
  isOpen: boolean
14
27
  onToggle: () => void
15
- badges?: Array<{
16
- text: string
17
- icon?: React.ReactNode
18
- variant?: "default" | "secondary"
19
- className?: string
20
- }>
28
+ badges?: Array<BadgeInfo>
21
29
  children?: React.ReactNode
22
30
  }
23
31
 
24
32
  export function CollapsibleSection({
25
33
  title,
26
34
  duration,
35
+ error,
36
+ displayStatus,
27
37
  isOpen,
28
38
  onToggle,
29
39
  badges = [],
@@ -40,7 +50,17 @@ export function CollapsibleSection({
40
50
  <span className="font-medium">{title}</span>
41
51
  </div>
42
52
  <div className="flex items-center gap-2">
43
- {badges.map((badge, index) => (
53
+ {[
54
+ ...badges,
55
+ ...(error
56
+ ? [
57
+ {
58
+ text: getErrorText(error),
59
+ variant: "destructive",
60
+ } as BadgeInfo,
61
+ ]
62
+ : []),
63
+ ].map((badge, index) => (
44
64
  <Badge
45
65
  key={index}
46
66
  variant={badge.variant || "secondary"}
@@ -56,13 +76,18 @@ export function CollapsibleSection({
56
76
  {duration && (
57
77
  <span className="text-sm text-gray-600">{duration}</span>
58
78
  )}
59
- <CheckCircle2 className="w-4 h-4 text-green-500" />
79
+ <div
80
+ className={`w-2 h-2 rounded-lg ${getColorForDisplayStatus(displayStatus)}`}
81
+ />
82
+ <div className="text-gray-600 text-xs font-medium">
83
+ {capitalCase(displayStatus) || "???"}
84
+ </div>
60
85
  </div>
61
86
  </div>
62
87
  </CollapsibleTrigger>
63
88
  <CollapsibleContent>
64
89
  <div className="p-4 bg-white border-x border-b border-gray-200 rounded-b-lg">
65
- {children || `${title} details would go here...`}
90
+ {children}
66
91
  </div>
67
92
  </CollapsibleContent>
68
93
  </Collapsible>
@@ -0,0 +1,17 @@
1
+ import { PackageRelease } from "fake-snippets-api/lib/db/schema"
2
+
3
+ export const getColorForDisplayStatus = (
4
+ display_status?: PackageRelease["display_status"] | null,
5
+ ) => {
6
+ switch (display_status) {
7
+ case "pending":
8
+ return "bg-yellow-500"
9
+ case "building":
10
+ return "bg-blue-500"
11
+ case "successful":
12
+ return "bg-green-500"
13
+ case "failed":
14
+ return "bg-red-500"
15
+ }
16
+ return "bg-gray-500"
17
+ }
@@ -0,0 +1,147 @@
1
+ import { Globe, GitBranch, GitCommit, Clock } from "lucide-react"
2
+ import { Badge } from "@/components/ui/badge"
3
+ import { useCurrentPackageRelease } from "@/hooks/use-current-package-release"
4
+ import { useParams } from "wouter"
5
+ import { timeAgo } from "@/lib/utils/timeAgo"
6
+ import { PackageRelease } from "fake-snippets-api/lib/db/schema"
7
+
8
+ const capitalCase = (str: string) => {
9
+ return str.charAt(0).toUpperCase() + str.slice(1)
10
+ }
11
+
12
+ function getColorFromDisplayStatus(
13
+ display_status: PackageRelease["display_status"],
14
+ ) {
15
+ switch (display_status) {
16
+ case "pending":
17
+ return "bg-yellow-500"
18
+ case "building":
19
+ return "bg-blue-500"
20
+ case "successful":
21
+ return "bg-green-500"
22
+ case "failed":
23
+ return "bg-red-500"
24
+ }
25
+ }
26
+
27
+ export function PackageBuildDetailsPanel() {
28
+ const { packageRelease } = useCurrentPackageRelease()
29
+ const { author } = useParams() // TODO use packageRelease.author_account_id when it's added by backed
30
+
31
+ if (!packageRelease) {
32
+ // TODO show skeleton instead
33
+ return null
34
+ }
35
+
36
+ const {
37
+ circuit_json_build_display_status,
38
+ circuit_json_build_in_progress,
39
+ circuit_json_build_is_stale,
40
+ circuit_json_build_logs,
41
+ transpilation_display_status,
42
+ transpilation_in_progress,
43
+ transpilation_logs,
44
+ circuit_json_build_completed_at,
45
+ transpilation_is_stale,
46
+ display_status,
47
+ created_at,
48
+ has_transpiled,
49
+ circuit_json_build_started_at,
50
+ circuit_json_build_error,
51
+ circuit_json_build_error_last_updated_at,
52
+ total_build_duration_ms,
53
+ transpilation_completed_at,
54
+ transpilation_error,
55
+ transpilation_started_at,
56
+ commit_sha,
57
+ } = packageRelease
58
+
59
+ return (
60
+ <div className="space-y-6 bg-white p-4 border border-gray-200 rounded-lg">
61
+ {/* Created */}
62
+ <div>
63
+ <h3 className="text-sm font-medium text-gray-600 mb-2">Created</h3>
64
+ <div className="flex items-center gap-2">
65
+ <div className="w-6 h-6 bg-orange-500 rounded-full flex items-center justify-center text-xs font-bold">
66
+ I
67
+ </div>
68
+ <span className="text-sm">{author}</span>
69
+ <span className="text-sm text-gray-500">
70
+ {timeAgo(packageRelease?.created_at, "")}
71
+ </span>
72
+ </div>
73
+ </div>
74
+
75
+ {/* Status */}
76
+ <div>
77
+ <h3 className="text-sm font-medium text-gray-600 mb-2">Status</h3>
78
+ <div className="flex items-center gap-2">
79
+ <div
80
+ className={`w-2 h-2 ${getColorFromDisplayStatus(display_status)} rounded-full`}
81
+ ></div>
82
+ <span className="text-sm">{capitalCase(display_status)}</span>
83
+ {/* <Badge
84
+ variant="secondary"
85
+ className="bg-gray-200 text-gray-700 text-xs"
86
+ >
87
+ Latest
88
+ </Badge> */}
89
+ </div>
90
+ </div>
91
+
92
+ <div>
93
+ <h3 className="text-sm font-medium text-gray-600 mb-2">Build Time</h3>
94
+ <div className="flex items-center gap-2">
95
+ <Clock className="w-4 h-4 text-gray-500" />
96
+ {circuit_json_build_completed_at && (
97
+ <span className="text-sm">
98
+ {total_build_duration_ms
99
+ ? `${Math.floor(total_build_duration_ms / 1000)}s`
100
+ : ""}
101
+ </span>
102
+ )}
103
+ <span className="text-sm text-gray-500">
104
+ {timeAgo(circuit_json_build_completed_at, "waiting...")}
105
+ </span>
106
+ </div>
107
+ </div>
108
+
109
+ {/* Version */}
110
+ <div>
111
+ <h3 className="text-sm font-medium text-gray-600 mb-2">Version</h3>
112
+ <div className="flex items-center gap-2">
113
+ <Globe className="w-4 h-4 text-gray-500" />
114
+ <span className="text-sm">{packageRelease.version}</span>
115
+ {/* <Badge variant="default" className="bg-blue-600 text-white text-xs">
116
+ Current
117
+ </Badge> */}
118
+ </div>
119
+ </div>
120
+
121
+ {/* Outputs */}
122
+ <div>
123
+ <h3 className="text-sm font-medium text-gray-600 mb-2">Outputs</h3>
124
+ <div>
125
+ <span className="text-sm text-gray-400">None</span>
126
+ </div>
127
+ </div>
128
+
129
+ {/* Source */}
130
+ {/* <div>
131
+ <h3 className="text-sm font-medium text-gray-600 mb-2">Source</h3>
132
+ <div className="space-y-2">
133
+ <div className="flex items-center gap-2">
134
+ <GitBranch className="w-4 h-4 text-gray-500" />
135
+ <span className="text-sm">main</span>
136
+ </div>
137
+ <div className="flex items-center gap-2">
138
+ <GitCommit className="w-4 h-4 text-gray-500" />
139
+ <span className="text-sm text-gray-500">
140
+ edfdc67 support empty file creation (#356)
141
+ </span>
142
+ </div>
143
+ </div>
144
+ </div> */}
145
+ </div>
146
+ )
147
+ }
@@ -1,4 +1,4 @@
1
- import { ChevronDown, Download, Github, Link } from "lucide-react"
1
+ import { ChevronDown, Download, Github, Link, RefreshCw } from "lucide-react"
2
2
  import { Button } from "@/components/ui/button"
3
3
  import {
4
4
  DropdownMenu,
@@ -7,9 +7,12 @@ import {
7
7
  DropdownMenuTrigger,
8
8
  } from "@/components/ui/dropdown-menu"
9
9
  import { useParams } from "wouter"
10
+ import { useCurrentPackageRelease } from "@/hooks/use-current-package-release"
11
+ import { DownloadButtonAndMenu } from "../DownloadButtonAndMenu"
10
12
 
11
- export function DeploymentHeader() {
13
+ export function PackageBuildHeader() {
12
14
  const { author, packageName } = useParams()
15
+ const { packageRelease } = useCurrentPackageRelease()
13
16
 
14
17
  return (
15
18
  <div className="border-b border-gray-200 bg-white px-6 py-4">
@@ -39,35 +42,17 @@ export function DeploymentHeader() {
39
42
  Report Issue
40
43
  </a>
41
44
  </Button>
42
- <DropdownMenu>
43
- <DropdownMenuTrigger asChild>
44
- <Button
45
- size="sm"
46
- className="bg-gray-900 text-white hover:bg-gray-800"
47
- >
48
- <Download className="w-4 h-4 mr-2" />
49
- Download
50
- <ChevronDown className="w-4 h-4 ml-2" />
51
- </Button>
52
- </DropdownMenuTrigger>
53
- <DropdownMenuContent
54
- align="end"
55
- className="bg-white border-gray-200"
56
- >
57
- <DropdownMenuItem className="text-gray-900 hover:bg-gray-100">
58
- Circuit JSON
59
- </DropdownMenuItem>
60
- <DropdownMenuItem className="text-gray-900 hover:bg-gray-100">
61
- PCB SVG
62
- </DropdownMenuItem>
63
- <DropdownMenuItem className="text-gray-900 hover:bg-gray-100">
64
- Schematic SVG
65
- </DropdownMenuItem>
66
- <DropdownMenuItem className="text-gray-900 hover:bg-gray-100">
67
- 3D Model (stl)
68
- </DropdownMenuItem>
69
- </DropdownMenuContent>
70
- </DropdownMenu>
45
+ <Button
46
+ variant="outline"
47
+ size="sm"
48
+ className="border-gray-300 bg-white hover:bg-gray-50 text-xs sm:text-sm"
49
+ >
50
+ <RefreshCw className="w-3 h-3 sm:w-4 sm:h-4 mr-1 sm:mr-2" />
51
+ Rebuild
52
+ </Button>
53
+ <DownloadButtonAndMenu
54
+ snippetUnscopedName={`${author}/${packageName}`}
55
+ />
71
56
  </div>
72
57
  </div>
73
58
  </div>
@@ -79,7 +79,7 @@ export default function MainContentHeader({
79
79
  <DropdownMenuTrigger asChild>
80
80
  <Button
81
81
  size="sm"
82
- className="h-9 bg-green-600 hover:bg-green-700 dark:bg-[#238636] dark:hover:bg-[#2ea043] text-white"
82
+ className="bg-green-600 hover:bg-green-700 dark:bg-[#238636] dark:hover:bg-[#2ea043] text-white"
83
83
  >
84
84
  <CodeIcon className="h-4 w-4 mr-1.5" />
85
85
  Code
@@ -76,7 +76,6 @@ export default function PackageHeader({
76
76
  window.TSCIRCUIT_REGISTRY_API_BASE_URL =
77
77
  import.meta.env.VITE_TSCIRCUIT_REGISTRY_API_URL ??
78
78
  `${window.location.origin}/api`
79
- window.TSCIRCUIT_REGISTRY_TOKEN = sessionToken ?? ""
80
79
  // TODO: replace with production stripe checkout base url
81
80
  window.TSCIRCUIT_STRIPE_CHECKOUT_BASE_URL =
82
81
  import.meta.env.VITE_TSCIRCUIT_STRIPE_CHECKOUT_BASE_URL
@@ -0,0 +1,113 @@
1
+ import { useState } from "react"
2
+ import { Button } from "../ui/button"
3
+ import {
4
+ Dialog,
5
+ DialogContent,
6
+ DialogHeader,
7
+ DialogTitle,
8
+ DialogFooter,
9
+ } from "../ui/dialog"
10
+ import { Label } from "../ui/label"
11
+ import {
12
+ Select,
13
+ SelectContent,
14
+ SelectItem,
15
+ SelectTrigger,
16
+ SelectValue,
17
+ } from "../ui/select"
18
+ import { Checkbox } from "../ui/checkbox"
19
+ import { AnyCircuitElement } from "circuit-json"
20
+ import { createUseDialog } from "./create-use-dialog"
21
+ import {
22
+ downloadPcbSvg,
23
+ DownloadPcbSvgOptions,
24
+ } from "@/lib/download-fns/download-pcb-svg"
25
+
26
+ interface PcbDownloadDialogProps {
27
+ open: boolean
28
+ onOpenChange: (open: boolean) => void
29
+ circuitJson: AnyCircuitElement[]
30
+ fileName: string
31
+ }
32
+
33
+ export const PcbDownloadDialog = ({
34
+ open,
35
+ onOpenChange,
36
+ circuitJson,
37
+ fileName,
38
+ }: PcbDownloadDialogProps) => {
39
+ const [layer, setLayer] = useState<"all" | "top" | "bottom">("all")
40
+ const [drawPadding, setDrawPadding] = useState(true)
41
+ const [transparentBg, setTransparentBg] = useState(false)
42
+ const [matchAspectRatio, setMatchAspectRatio] = useState(false)
43
+
44
+ const handleDownload = () => {
45
+ const options: DownloadPcbSvgOptions = {
46
+ layer,
47
+ drawPaddingOutsideBoard: drawPadding,
48
+ backgroundColor: transparentBg ? "transparent" : "#000",
49
+ matchAspectRatio,
50
+ }
51
+ downloadPcbSvg(circuitJson, fileName, options)
52
+ onOpenChange(false)
53
+ }
54
+
55
+ return (
56
+ <Dialog open={open} onOpenChange={onOpenChange}>
57
+ <DialogContent>
58
+ <DialogHeader>
59
+ <DialogTitle>Download PCB SVG</DialogTitle>
60
+ </DialogHeader>
61
+ <div className="space-y-4 py-2">
62
+ <div className="grid grid-cols-4 items-center gap-4">
63
+ <Label htmlFor="layer" className="text-right">
64
+ Layer
65
+ </Label>
66
+ <Select value={layer} onValueChange={(v) => setLayer(v as any)}>
67
+ <SelectTrigger id="layer" className="col-span-3">
68
+ <SelectValue placeholder="Layer" />
69
+ </SelectTrigger>
70
+ <SelectContent className="!z-[999]">
71
+ <SelectItem value="all">All</SelectItem>
72
+ <SelectItem value="top">Top</SelectItem>
73
+ <SelectItem value="bottom">Bottom</SelectItem>
74
+ </SelectContent>
75
+ </Select>
76
+ </div>
77
+ <div className="flex items-center space-x-2">
78
+ <Checkbox
79
+ id="padding"
80
+ checked={drawPadding}
81
+ onCheckedChange={(v) => setDrawPadding(Boolean(v))}
82
+ />
83
+ <Label htmlFor="padding">Draw Padding and Board Outline</Label>
84
+ </div>
85
+ <div className="flex items-center space-x-2">
86
+ <Checkbox
87
+ id="transparentBg"
88
+ checked={transparentBg}
89
+ onCheckedChange={(v) => setTransparentBg(Boolean(v))}
90
+ />
91
+ <Label htmlFor="transparentBg">Transparent Background</Label>
92
+ </div>
93
+ <div className="flex items-center space-x-2">
94
+ <Checkbox
95
+ id="matchAspectRatio"
96
+ checked={matchAspectRatio}
97
+ onCheckedChange={(v) => setMatchAspectRatio(Boolean(v))}
98
+ />
99
+ <Label htmlFor="matchAspectRatio">Match Aspect Ratio</Label>
100
+ </div>
101
+ </div>
102
+ <DialogFooter>
103
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
104
+ Cancel
105
+ </Button>
106
+ <Button onClick={handleDownload}>Download</Button>
107
+ </DialogFooter>
108
+ </DialogContent>
109
+ </Dialog>
110
+ )
111
+ }
112
+
113
+ export const usePcbDownloadDialog = createUseDialog(PcbDownloadDialog)