Cloud sidebar: Beta badge, Get support modal, dev proxy auto-restart
Unified · Split
| @@ -5,7 +5,7 @@ · 4 unmodified lines | |||
| 5 | "type": "module", | 5 | "type": "module", |
| 6 | "scripts": { | 6 | "scripts": { |
| 7 | "dev": "bun run dev:proxy && concurrently -n db,vite -c blue,green \"bun run dev:db\" \"bun run dev:vite\"", | 7 | "dev": "bun run dev:proxy && concurrently -n db,vite -c blue,green \"bun run dev:db\" \"bun run dev:vite\"", |
| 8 | "dev:proxy": "portless proxy start --multiplex --shared-port --port 5394 || true", | 8 | "dev:proxy": "portless proxy start --multiplex --shared-port --port 5394 || (portless proxy stop -p 5394 && portless proxy start --multiplex --shared-port --port 5394)", |
| 9 | "dev:db": "bun run scripts/dev-db.ts", | 9 | "dev:db": "bun run scripts/dev-db.ts", |
| 10 | "dev:vite": "EXECUTOR_DIRECT_DATABASE_URL=true CLOUDFLARE_INCLUDE_PROCESS_ENV=true op run --env-file=.env.op -- portless --name executor-cloud vite dev", | 10 | "dev:vite": "EXECUTOR_DIRECT_DATABASE_URL=true CLOUDFLARE_INCLUDE_PROCESS_ENV=true op run --env-file=.env.op -- portless --name executor-cloud vite dev", |
| 11 | "db:schema": "node --import jiti/register ../../packages/core/cli/src/index.ts generate --config ./executor.config.ts --output ./src/services/executor-schema.ts", | 11 | "db:schema": "node --import jiti/register ../../packages/core/cli/src/index.ts generate --config ./executor.config.ts --output ./src/services/executor-schema.ts", |
| @@ -6,22 +6,12 @@ import posthog from "posthog-js"; · 5 unmodified lines | |||
| 6 | import { PostHogProvider } from "posthog-js/react"; | 6 | import { PostHogProvider } from "posthog-js/react"; |
| 7 | import type { FrontendErrorReporter } from "@executor-js/react/api/error-reporting"; | 7 | import type { FrontendErrorReporter } from "@executor-js/react/api/error-reporting"; |
| 8 | import { ExecutorProvider } from "@executor-js/react/api/provider"; | 8 | import { ExecutorProvider } from "@executor-js/react/api/provider"; |
| 9 | import { Button } from "@executor-js/react/components/button"; | ||
| 10 | import { | ||
| 11 | Dialog, | ||
| 12 | DialogClose, | ||
| 13 | DialogContent, | ||
| 14 | DialogDescription, | ||
| 15 | DialogFooter, | ||
| 16 | DialogHeader, | ||
| 17 | DialogTitle, | ||
| 18 | DialogTrigger, | ||
| 19 | } from "@executor-js/react/components/dialog"; | ||
| 20 | import { Skeleton } from "@executor-js/react/components/skeleton"; | 9 | import { Skeleton } from "@executor-js/react/components/skeleton"; |
| 21 | import { Toaster } from "@executor-js/react/components/sonner"; | 10 | import { Toaster } from "@executor-js/react/components/sonner"; |
| 22 | import { ExecutorPluginsProvider } from "@executor-js/sdk/client"; | 11 | import { ExecutorPluginsProvider } from "@executor-js/sdk/client"; |
| 23 | import { plugins as clientPlugins } from "virtual:executor/plugins-client"; | 12 | import { plugins as clientPlugins } from "virtual:executor/plugins-client"; |
| 24 | import { AuthProvider, useAuth } from "../web/auth"; | 13 | import { AuthProvider, useAuth } from "../web/auth"; |
| 14 | import { SupportOptions } from "../web/components/support-options"; | ||
| 25 | import { LoginPage } from "../web/pages/login"; | 15 | import { LoginPage } from "../web/pages/login"; |
| 26 | import { OnboardingPage } from "../web/pages/onboarding"; | 16 | import { OnboardingPage } from "../web/pages/onboarding"; |
| 27 | import { Shell } from "../web/shell"; | 17 | import { Shell } from "../web/shell"; |
| @@ -185,17 +175,6 @@ function ShellSkeleton() { · 157 unmodified lines | |||
| 185 | } | 175 | } |
| 186 | 176 | ||
| 187 | function ShellErrorFallback() { | 177 | function ShellErrorFallback() { |
| 188 | const [slackOpen, setSlackOpen] = React.useState(false); | ||
| 189 | const supportLinks = [ | ||
| 190 | { label: "Discord", href: "https://discord.gg/eF29HBHwM6", icon: DiscordMark }, | ||
| 191 | { | ||
| 192 | label: "GitHub Issues", | ||
| 193 | href: "https://github.com/RhysSullivan/executor/issues", | ||
| 194 | icon: GitHubMark, | ||
| 195 | }, | ||
| 196 | { label: "Email", href: "mailto:rhys@executor.sh?subject=Executor%20support", icon: MailMark }, | ||
| 197 | ] as const; | ||
| 198 | |||
| 199 | return ( | 178 | return ( |
| 200 | <main className="flex min-h-screen items-center justify-center bg-background px-6 py-10"> | 179 | <main className="flex min-h-screen items-center justify-center bg-background px-6 py-10"> |
| 201 | <section className="w-full max-w-md text-center"> | 180 | <section className="w-full max-w-md text-center"> |
| @@ -209,106 +188,14 @@ function ShellErrorFallback() { · 7 unmodified lines | |||
| 209 | <p className="mt-6 text-xs font-medium uppercase tracking-wide text-muted-foreground"> | 188 | <p className="mt-6 text-xs font-medium uppercase tracking-wide text-muted-foreground"> |
| 210 | Get support | 189 | Get support |
| 211 | </p> | 190 | </p> |
| 212 | <div className="mt-3 flex flex-wrap items-center justify-center gap-2 text-sm"> | 191 | <div className="mt-3"> |
| 213 | <Dialog open={slackOpen} onOpenChange={setSlackOpen}> | 192 | <SupportOptions /> |
| 214 | <DialogTrigger asChild> | ||
| 215 | <Button type="button" variant="outline" size="sm" className="gap-2"> | ||
| 216 | <SlackMark className="size-4" /> | ||
| 217 | Slack | ||
| 218 | </Button> | ||
| 219 | </DialogTrigger> | ||
| 220 | <DialogContent> | ||
| 221 | <DialogHeader> | ||
| 222 | <DialogTitle>Slack Connect</DialogTitle> | ||
| 223 | <DialogDescription> | ||
| 224 | Invite <span className="font-medium text-foreground">rhys@executor.sh</span> to | ||
| 225 | Slack Connect. | ||
| 226 | </DialogDescription> | ||
| 227 | </DialogHeader> | ||
| 228 | <DialogFooter> | ||
| 229 | <DialogClose asChild> | ||
| 230 | <Button type="button" variant="outline"> | ||
| 231 | Done | ||
| 232 | </Button> | ||
| 233 | </DialogClose> | ||
| 234 | </DialogFooter> | ||
| 235 | </DialogContent> | ||
| 236 | </Dialog> | ||
| 237 | {supportLinks.map((link) => ( | ||
| 238 | // oxlint-disable-next-line react/jsx-no-new-function-as-prop -- static support link component choice | ||
| 239 | <a | ||
| 240 | key={link.label} | ||
| 241 | href={link.href} | ||
| 242 | className="inline-flex h-9 items-center gap-2 rounded-md border border-border bg-background px-3 font-medium text-foreground transition-colors hover:bg-muted" | ||
| 243 | > | ||
| 244 | <link.icon className="size-4" /> | ||
| 245 | {link.label} | ||
| 246 | </a> | ||
| 247 | ))} | ||
| 248 | </div> | 193 | </div> |
| 249 | </section> | 194 | </section> |
| 250 | </main> | 195 | </main> |
| 251 | ); | 196 | ); |
| 252 | } | 197 | } |
| 253 | 198 | ||
| 254 | function DiscordMark({ className }: { className?: string }) { | ||
| 255 | return ( | ||
| 256 | <svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden> | ||
| 257 | <path d="M20.32 4.37A19.8 19.8 0 0 0 15.36 2.8a13.8 13.8 0 0 0-.64 1.32 18.4 18.4 0 0 0-5.44 0 13.8 13.8 0 0 0-.64-1.32 19.7 19.7 0 0 0-4.97 1.57C.53 9.09-.32 13.69.1 18.22a19.9 19.9 0 0 0 6.08 3.03 14.7 14.7 0 0 0 1.3-2.09 12.8 12.8 0 0 1-2.04-.97l.5-.38a14.2 14.2 0 0 0 12.12 0l.5.38c-.65.38-1.33.7-2.04.97.37.74.8 1.44 1.3 2.09a19.9 19.9 0 0 0 6.08-3.03c.5-5.25-.84-9.8-3.58-13.85ZM8.02 15.43c-1.18 0-2.15-1.08-2.15-2.4 0-1.33.95-2.41 2.15-2.41 1.2 0 2.17 1.09 2.15 2.4 0 1.33-.95 2.41-2.15 2.41Zm7.96 0c-1.18 0-2.15-1.08-2.15-2.4 0-1.33.95-2.41 2.15-2.41 1.2 0 2.17 1.09 2.15 2.4 0 1.33-.95 2.41-2.15 2.41Z" /> | ||
| 258 | </svg> | ||
| 259 | ); | ||
| 260 | } | ||
| 261 | |||
| 262 | function GitHubMark({ className }: { className?: string }) { | ||
| 263 | return ( | ||
| 264 | <svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden> | ||
| 265 | <path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.1.79-.25.79-.56v-2.15c-3.2.7-3.88-1.36-3.88-1.36-.52-1.33-1.28-1.68-1.28-1.68-1.05-.72.08-.7.08-.7 1.16.08 1.77 1.19 1.77 1.19 1.03 1.77 2.71 1.26 3.37.96.1-.75.4-1.26.73-1.55-2.55-.29-5.24-1.28-5.24-5.68 0-1.25.45-2.28 1.19-3.08-.12-.29-.52-1.46.11-3.04 0 0 .98-.31 3.17 1.18a10.9 10.9 0 0 1 5.78 0c2.2-1.49 3.17-1.18 3.17-1.18.63 1.58.23 2.75.11 3.04.74.8 1.19 1.83 1.19 3.08 0 4.42-2.69 5.39-5.25 5.68.41.36.78 1.06.78 2.14v3.16c0 .31.21.67.79.56A11.5 11.5 0 0 0 23.5 12C23.5 5.65 18.35.5 12 .5Z" /> | ||
| 266 | </svg> | ||
| 267 | ); | ||
| 268 | } | ||
| 269 | |||
| 270 | function MailMark({ className }: { className?: string }) { | ||
| 271 | return ( | ||
| 272 | <svg | ||
| 273 | viewBox="0 0 24 24" | ||
| 274 | fill="none" | ||
| 275 | stroke="currentColor" | ||
| 276 | strokeWidth="1.8" | ||
| 277 | className={className} | ||
| 278 | aria-hidden | ||
| 279 | > | ||
| 280 | <path | ||
| 281 | strokeLinecap="round" | ||
| 282 | strokeLinejoin="round" | ||
| 283 | d="M3 7l9 6 9-6M5 19h14a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2Z" | ||
| 284 | /> | ||
| 285 | </svg> | ||
| 286 | ); | ||
| 287 | } | ||
| 288 | |||
| 289 | function SlackMark({ className }: { className?: string }) { | ||
| 290 | return ( | ||
| 291 | <svg viewBox="0 0 24 24" fill="none" className={className} aria-hidden> | ||
| 292 | <path | ||
| 293 | d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zm1.271 0a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313z" | ||
| 294 | fill="#E01E5A" | ||
| 295 | /> | ||
| 296 | <path | ||
| 297 | d="M8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zm0 1.271a2.527 2.527 0 0 1 2.521 2.521 2.527 2.527 0 0 1-2.521 2.521H2.522A2.527 2.527 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312z" | ||
| 298 | fill="#36C5F0" | ||
| 299 | /> | ||
| 300 | <path | ||
| 301 | d="M18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zm-1.272 0a2.528 2.528 0 0 1-2.521 2.521 2.527 2.527 0 0 1-2.521-2.521V2.522A2.527 2.527 0 0 1 15.163 0a2.528 2.528 0 0 1 2.521 2.522v6.312z" | ||
| 302 | fill="#2EB67D" | ||
| 303 | /> | ||
| 304 | <path | ||
| 305 | d="M15.163 18.956a2.528 2.528 0 0 1 2.521 2.522A2.528 2.528 0 0 1 15.163 24a2.527 2.527 0 0 1-2.521-2.522v-2.522h2.521zm0-1.272a2.527 2.527 0 0 1-2.521-2.521 2.527 2.527 0 0 1 2.521-2.521h6.315A2.527 2.527 0 0 1 24 15.163a2.528 2.528 0 0 1-2.522 2.521h-6.315z" | ||
| 306 | fill="#ECB22E" | ||
| 307 | /> | ||
| 308 | </svg> | ||
| 309 | ); | ||
| 310 | } | ||
| 311 | |||
| 312 | function AuthGate() { | 199 | function AuthGate() { |
| 313 | const auth = useAuth(); | 200 | const auth = useAuth(); |
| 314 | 201 | ||
| @@ -0,0 +1,112 @@ | |||
| 1 | import { Button } from "@executor-js/react/components/button"; | ||
| 2 | import { | ||
| 3 | Popover, | ||
| 4 | PopoverContent, | ||
| 5 | PopoverDescription, | ||
| 6 | PopoverHeader, | ||
| 7 | PopoverTitle, | ||
| 8 | PopoverTrigger, | ||
| 9 | } from "@executor-js/react/components/popover"; | ||
| 10 | |||
| 11 | const supportLinks = [ | ||
| 12 | { label: "Discord", href: "https://discord.gg/eF29HBHwM6", icon: DiscordMark }, | ||
| 13 | { | ||
| 14 | label: "GitHub Issues", | ||
| 15 | href: "https://github.com/RhysSullivan/executor/issues", | ||
| 16 | icon: GitHubMark, | ||
| 17 | }, | ||
| 18 | { label: "Email", href: "mailto:rhys@executor.sh?subject=Executor%20support", icon: MailMark }, | ||
| 19 | ] as const; | ||
| 20 | |||
| 21 | export function SupportOptions() { | ||
| 22 | return ( | ||
| 23 | <div className="flex flex-wrap items-center justify-center gap-2 text-sm"> | ||
| 24 | <Popover> | ||
| 25 | <PopoverTrigger asChild> | ||
| 26 | <Button type="button" variant="outline" size="sm" className="gap-2"> | ||
| 27 | <SlackMark className="size-4" /> | ||
| 28 | Slack | ||
| 29 | </Button> | ||
| 30 | </PopoverTrigger> | ||
| 31 | <PopoverContent className="w-72"> | ||
| 32 | <PopoverHeader> | ||
| 33 | <PopoverTitle>Slack Connect</PopoverTitle> | ||
| 34 | <PopoverDescription> | ||
| 35 | Invite <span className="font-medium text-foreground">rhys@executor.sh</span> to Slack | ||
| 36 | Connect. | ||
| 37 | </PopoverDescription> | ||
| 38 | </PopoverHeader> | ||
| 39 | </PopoverContent> | ||
| 40 | </Popover> | ||
| 41 | {supportLinks.map((link) => ( | ||
| 42 | // oxlint-disable-next-line react/jsx-no-new-function-as-prop -- static support link component choice | ||
| 43 | <a | ||
| 44 | key={link.label} | ||
| 45 | href={link.href} | ||
| 46 | className="inline-flex h-9 items-center gap-2 rounded-md border border-border bg-background px-3 font-medium text-foreground transition-colors hover:bg-muted" | ||
| 47 | > | ||
| 48 | <link.icon className="size-4" /> | ||
| 49 | {link.label} | ||
| 50 | </a> | ||
| 51 | ))} | ||
| 52 | </div> | ||
| 53 | ); | ||
| 54 | } | ||
| 55 | |||
| 56 | function DiscordMark({ className }: { className?: string }) { | ||
| 57 | return ( | ||
| 58 | <svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden> | ||
| 59 | <path d="M20.32 4.37A19.8 19.8 0 0 0 15.36 2.8a13.8 13.8 0 0 0-.64 1.32 18.4 18.4 0 0 0-5.44 0 13.8 13.8 0 0 0-.64-1.32 19.7 19.7 0 0 0-4.97 1.57C.53 9.09-.32 13.69.1 18.22a19.9 19.9 0 0 0 6.08 3.03 14.7 14.7 0 0 0 1.3-2.09 12.8 12.8 0 0 1-2.04-.97l.5-.38a14.2 14.2 0 0 0 12.12 0l.5.38c-.65.38-1.33.7-2.04.97.37.74.8 1.44 1.3 2.09a19.9 19.9 0 0 0 6.08-3.03c.5-5.25-.84-9.8-3.58-13.85ZM8.02 15.43c-1.18 0-2.15-1.08-2.15-2.4 0-1.33.95-2.41 2.15-2.41 1.2 0 2.17 1.09 2.15 2.4 0 1.33-.95 2.41-2.15 2.41Zm7.96 0c-1.18 0-2.15-1.08-2.15-2.4 0-1.33.95-2.41 2.15-2.41 1.2 0 2.17 1.09 2.15 2.4 0 1.33-.95 2.41-2.15 2.41Z" /> | ||
| 60 | </svg> | ||
| 61 | ); | ||
| 62 | } | ||
| 63 | |||
| 64 | function GitHubMark({ className }: { className?: string }) { | ||
| 65 | return ( | ||
| 66 | <svg viewBox="0 0 24 24" fill="currentColor" className={className} aria-hidden> | ||
| 67 | <path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.1.79-.25.79-.56v-2.15c-3.2.7-3.88-1.36-3.88-1.36-.52-1.33-1.28-1.68-1.28-1.68-1.05-.72.08-.7.08-.7 1.16.08 1.77 1.19 1.77 1.19 1.03 1.77 2.71 1.26 3.37.96.1-.75.4-1.26.73-1.55-2.55-.29-5.24-1.28-5.24-5.68 0-1.25.45-2.28 1.19-3.08-.12-.29-.52-1.46.11-3.04 0 0 .98-.31 3.17 1.18a10.9 10.9 0 0 1 5.78 0c2.2-1.49 3.17-1.18 3.17-1.18.63 1.58.23 2.75.11 3.04.74.8 1.19 1.83 1.19 3.08 0 4.42-2.69 5.39-5.25 5.68.41.36.78 1.06.78 2.14v3.16c0 .31.21.67.79.56A11.5 11.5 0 0 0 23.5 12C23.5 5.65 18.35.5 12 .5Z" /> | ||
| 68 | </svg> | ||
| 69 | ); | ||
| 70 | } | ||
| 71 | |||
| 72 | function MailMark({ className }: { className?: string }) { | ||
| 73 | return ( | ||
| 74 | <svg | ||
| 75 | viewBox="0 0 24 24" | ||
| 76 | fill="none" | ||
| 77 | stroke="currentColor" | ||
| 78 | strokeWidth="1.8" | ||
| 79 | className={className} | ||
| 80 | aria-hidden | ||
| 81 | > | ||
| 82 | <path | ||
| 83 | strokeLinecap="round" | ||
| 84 | strokeLinejoin="round" | ||
| 85 | d="M3 7l9 6 9-6M5 19h14a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2Z" | ||
| 86 | /> | ||
| 87 | </svg> | ||
| 88 | ); | ||
| 89 | } | ||
| 90 | |||
| 91 | function SlackMark({ className }: { className?: string }) { | ||
| 92 | return ( | ||
| 93 | <svg viewBox="0 0 24 24" fill="none" className={className} aria-hidden> | ||
| 94 | <path | ||
| 95 | d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zm1.271 0a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313z" | ||
| 96 | fill="#E01E5A" | ||
| 97 | /> | ||
| 98 | <path | ||
| 99 | d="M8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zm0 1.271a2.527 2.527 0 0 1 2.521 2.521 2.527 2.527 0 0 1-2.521 2.521H2.522A2.527 2.527 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312z" | ||
| 100 | fill="#36C5F0" | ||
| 101 | /> | ||
| 102 | <path | ||
| 103 | d="M18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zm-1.272 0a2.528 2.528 0 0 1-2.521 2.521 2.527 2.527 0 0 1-2.521-2.521V2.522A2.527 2.527 0 0 1 15.163 0a2.528 2.528 0 0 1 2.521 2.522v6.312z" | ||
| 104 | fill="#2EB67D" | ||
| 105 | /> | ||
| 106 | <path | ||
| 107 | d="M15.163 18.956a2.528 2.528 0 0 1 2.521 2.522A2.528 2.528 0 0 1 15.163 24a2.527 2.527 0 0 1-2.521-2.522v-2.522h2.521zm0-1.272a2.527 2.527 0 0 1-2.521-2.521 2.527 2.527 0 0 1 2.521-2.521h6.315A2.527 2.527 0 0 1 24 15.163a2.528 2.528 0 0 1-2.522 2.521h-6.315z" | ||
| 108 | fill="#ECB22E" | ||
| 109 | /> | ||
| 110 | </svg> | ||
| 111 | ); | ||
| 112 | } | ||
| @@ -16,6 +16,7 @@ import { · 15 unmodified lines | |||
| 16 | DialogHeader, | 16 | DialogHeader, |
| 17 | DialogTitle, | 17 | DialogTitle, |
| 18 | } from "@executor-js/react/components/dialog"; | 18 | } from "@executor-js/react/components/dialog"; |
| 19 | import { SupportOptions } from "./components/support-options"; | ||
| 19 | import { | 20 | import { |
| 20 | DropdownMenu, | 21 | DropdownMenu, |
| 21 | DropdownMenuContent, | 22 | DropdownMenuContent, |
| @@ -37,6 +38,19 @@ import { · 15 unmodified lines | |||
| 37 | useCreateOrganizationForm, | 38 | useCreateOrganizationForm, |
| 38 | } from "./components/create-organization-form"; | 39 | } from "./components/create-organization-form"; |
| 39 | 40 | ||
| 41 | // ── Brand ──────────────────────────────────────────────────────────────── | ||
| 42 | |||
| 43 | function Brand(props: { onNavigate?: () => void }) { | ||
| 44 | return ( | ||
| 45 | <Link to="/" onClick={props.onNavigate} className="flex items-center gap-1.5"> | ||
| 46 | <span className="font-display text-base tracking-tight text-foreground">executor</span> | ||
| 47 | <span className="rounded-sm bg-primary/10 px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider text-primary"> | ||
| 48 | Beta | ||
| 49 | </span> | ||
| 50 | </Link> | ||
| 51 | ); | ||
| 52 | } | ||
| 53 | |||
| 40 | // ── NavItem ────────────────────────────────────────────────────────────── | 54 | // ── NavItem ────────────────────────────────────────────────────────────── |
| 41 | 55 | ||
| 42 | function NavItem(props: { to: string; label: string; active: boolean; onNavigate?: () => void }) { | 56 | function NavItem(props: { to: string; label: string; active: boolean; onNavigate?: () => void }) { |
| @@ -351,6 +365,50 @@ function UserFooter() { · 308 unmodified lines | |||
| 351 | ); | 365 | ); |
| 352 | } | 366 | } |
| 353 | 367 | ||
| 368 | // ── SupportButton ──────────────────────────────────────────────────────── | ||
| 369 | |||
| 370 | function HelpIcon({ className }: { className?: string }) { | ||
| 371 | return ( | ||
| 372 | <svg viewBox="0 0 16 16" fill="none" className={className} aria-hidden> | ||
| 373 | <circle cx="8" cy="8" r="6.5" stroke="currentColor" strokeWidth="1.3" /> | ||
| 374 | <path | ||
| 375 | d="M6.25 6.25c.25-1 1-1.5 1.85-1.5 1 0 1.9.7 1.9 1.7 0 .8-.5 1.2-1.1 1.6-.55.4-.9.7-.9 1.45M8 11.25v.05" | ||
| 376 | stroke="currentColor" | ||
| 377 | strokeWidth="1.3" | ||
| 378 | strokeLinecap="round" | ||
| 379 | /> | ||
| 380 | </svg> | ||
| 381 | ); | ||
| 382 | } | ||
| 383 | |||
| 384 | function SupportButton() { | ||
| 385 | const [open, setOpen] = useState(false); | ||
| 386 | return ( | ||
| 387 | <Dialog open={open} onOpenChange={setOpen}> | ||
| 388 | <Button | ||
| 389 | type="button" | ||
| 390 | variant="ghost" | ||
| 391 | onClick={() => setOpen(true)} | ||
| 392 | className="flex h-auto w-full items-center justify-start gap-2.5 rounded-md px-2.5 py-1.5 text-sm font-normal text-sidebar-foreground hover:bg-sidebar-active/60 hover:text-foreground" | ||
| 393 | > | ||
| 394 | <HelpIcon className="size-3.5 text-muted-foreground" /> | ||
| 395 | Get support | ||
| 396 | </Button> | ||
| 397 | <DialogContent className="sm:max-w-[440px]"> | ||
| 398 | <DialogHeader> | ||
| 399 | <DialogTitle className="font-display text-xl">Get support</DialogTitle> | ||
| 400 | <DialogDescription className="text-sm leading-relaxed"> | ||
| 401 | Reach out through any of the channels below. | ||
| 402 | </DialogDescription> | ||
| 403 | </DialogHeader> | ||
| 404 | <div className="py-2"> | ||
| 405 | <SupportOptions /> | ||
| 406 | </div> | ||
| 407 | </DialogContent> | ||
| 408 | </Dialog> | ||
| 409 | ); | ||
| 410 | } | ||
| 411 | |||
| 354 | // ── SidebarContent ─────────────────────────────────────────────────────── | 412 | // ── SidebarContent ─────────────────────────────────────────────────────── |
| 355 | 413 | ||
| 356 | function SidebarContent(props: { pathname: string; onNavigate?: () => void; showBrand?: boolean }) { | 414 | function SidebarContent(props: { pathname: string; onNavigate?: () => void; showBrand?: boolean }) { |
| @@ -365,9 +423,7 @@ function SidebarContent(props: { pathname: string; onNavigate?: () => void; show · 8 unmodified lines | |||
| 365 | <> | 423 | <> |
| 366 | {props.showBrand !== false && ( | 424 | {props.showBrand !== false && ( |
| 367 | <div className="flex h-12 shrink-0 items-center border-b border-sidebar-border px-4"> | 425 | <div className="flex h-12 shrink-0 items-center border-b border-sidebar-border px-4"> |
| 368 | <Link to="/" className="flex items-center gap-1.5"> | 426 | <Brand onNavigate={props.onNavigate} /> |
| 369 | <span className="font-display text-base tracking-tight text-foreground">executor</span> | ||
| 370 | </Link> | ||
| 371 | </div> | 427 | </div> |
| 372 | )} | 428 | )} |
| 373 | 429 | ||
| @@ -396,6 +452,10 @@ function SidebarContent(props: { pathname: string; onNavigate?: () => void; show · 22 unmodified lines | |||
| 396 | <SourceList pathname={props.pathname} onNavigate={props.onNavigate} /> | 452 | <SourceList pathname={props.pathname} onNavigate={props.onNavigate} /> |
| 397 | </nav> | 453 | </nav> |
| 398 | 454 | ||
| 455 | <div className="shrink-0 px-2 pb-2"> | ||
| 456 | <SupportButton /> | ||
| 457 | </div> | ||
| 458 | |||
| 399 | <UserFooter /> | 459 | <UserFooter /> |
| 400 | </> | 460 | </> |
| 401 | ); | 461 | ); |
| @@ -443,11 +503,7 @@ export function Shell() { · 41 unmodified lines | |||
| 443 | /> | 503 | /> |
| 444 | <div className="relative flex h-full w-[84vw] max-w-xs flex-col border-r border-sidebar-border bg-sidebar shadow-2xl"> | 504 | <div className="relative flex h-full w-[84vw] max-w-xs flex-col border-r border-sidebar-border bg-sidebar shadow-2xl"> |
| 445 | <div className="flex h-12 shrink-0 items-center justify-between border-b border-sidebar-border px-4"> | 505 | <div className="flex h-12 shrink-0 items-center justify-between border-b border-sidebar-border px-4"> |
| 446 | <Link to="/" className="flex items-center gap-1.5"> | 506 | <Brand onNavigate={() => setMobileSidebarOpen(false)} /> |
| 447 | <span className="font-display text-base tracking-tight text-foreground"> | ||
| 448 | executor | ||
| 449 | </span> | ||
| 450 | </Link> | ||
| 451 | <Button | 507 | <Button |
| 452 | variant="ghost" | 508 | variant="ghost" |
| 453 | size="icon-sm" | 509 | size="icon-sm" |
| @@ -496,9 +552,7 @@ export function Shell() { · 42 unmodified lines | |||
| 496 | /> | 552 | /> |
| 497 | </svg> | 553 | </svg> |
| 498 | </Button> | 554 | </Button> |
| 499 | <Link to="/" className="flex items-center gap-1.5"> | 555 | <Brand /> |
| 500 | <span className="font-display text-base tracking-tight text-foreground">executor</span> | ||
| 501 | </Link> | ||
| 502 | <div className="w-8 shrink-0" /> | 556 | <div className="w-8 shrink-0" /> |
| 503 | </div> | 557 | </div> |
| 504 | 558 | ||