mirror of
https://github.com/nicotsx/ironmount.git
synced 2025-12-10 12:10:51 +01:00
ui: redesign tabs
This commit is contained in:
@@ -116,7 +116,7 @@ body {
|
|||||||
--muted-foreground: oklch(0.708 0 0);
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
--accent: oklch(0.269 0 0);
|
--accent: oklch(0.269 0 0);
|
||||||
--accent-foreground: oklch(0.985 0 0);
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
--destructive: oklch(0.704 0.191 22.216);
|
--destructive: #ff543a;
|
||||||
--border: oklch(1 0 0 / 10%);
|
--border: oklch(1 0 0 / 10%);
|
||||||
--input: oklch(1 0 0 / 15%);
|
--input: oklch(1 0 0 / 15%);
|
||||||
--ring: oklch(0.556 0 0);
|
--ring: oklch(0.556 0 0);
|
||||||
|
|||||||
@@ -14,14 +14,20 @@ export function AppBreadcrumb() {
|
|||||||
const breadcrumbs = useBreadcrumbs();
|
const breadcrumbs = useBreadcrumbs();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Breadcrumb className={cn("mb-2", { invisible: breadcrumbs.length <= 1 })}>
|
<Breadcrumb>
|
||||||
|
<BreadcrumbLink asChild></BreadcrumbLink>
|
||||||
<BreadcrumbList>
|
<BreadcrumbList>
|
||||||
|
{breadcrumbs.length === 1 && (
|
||||||
|
<BreadcrumbItem>
|
||||||
|
<Link to="/">Ironmount</Link>
|
||||||
|
</BreadcrumbItem>
|
||||||
|
)}
|
||||||
{breadcrumbs.map((breadcrumb, index) => {
|
{breadcrumbs.map((breadcrumb, index) => {
|
||||||
const isLast = index === breadcrumbs.length - 1;
|
const isLast = index === breadcrumbs.length - 1;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={`${breadcrumb.label}-${index}`} className="contents">
|
<div key={`${breadcrumb.label}-${index}`} className="contents">
|
||||||
<BreadcrumbItem>
|
<BreadcrumbItem className={cn({ invisible: breadcrumbs.length <= 1 })}>
|
||||||
{isLast || breadcrumb.isCurrentPage ? (
|
{isLast || breadcrumb.isCurrentPage ? (
|
||||||
<BreadcrumbPage>{breadcrumb.label}</BreadcrumbPage>
|
<BreadcrumbPage>{breadcrumb.label}</BreadcrumbPage>
|
||||||
) : breadcrumb.href ? (
|
) : breadcrumb.href ? (
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export const CreateVolumeDialog = ({ open, setOpen }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button className="bg-transparent border hover:bg-strong-accent">
|
<Button>
|
||||||
<Plus size={16} className="mr-2" />
|
<Plus size={16} className="mr-2" />
|
||||||
Create volume
|
Create volume
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ const buttonVariants = cva(
|
|||||||
{
|
{
|
||||||
variants: {
|
variants: {
|
||||||
variant: {
|
variant: {
|
||||||
default:
|
default: "bg-transparent text-white hover:bg-[#3A3A3A]/80 border dark:text-white dark:hover:bg-[#3A3A3A]/80",
|
||||||
"bg-[var(--strong-accent)] text-white hover:bg-[var(--strong-accent)]/90 focus-visible:ring-[var(--strong-accent)]/50",
|
primary: "bg-strong-accent text-white hover:bg-strong-accent/90 focus-visible:ring-strong-accent/50",
|
||||||
destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/50",
|
destructive:
|
||||||
|
"border border-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/50 text-destructive hover:text-white",
|
||||||
outline: "border border-border bg-background hover:bg-accent hover:text-accent-foreground",
|
outline: "border border-border bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
secondary:
|
secondary: "bg-transparent text-white hover:bg-[#3A3A3A]/80 border dark:text-white dark:hover:bg-[#3A3A3A]/80",
|
||||||
"bg-[#464646] text-white hover:bg-[#464646]/80 dark:bg-[#464646] dark:text-white dark:hover:bg-[#464646]/80",
|
|
||||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React, { useEffect } from "react";
|
|
||||||
import Prism from "prismjs";
|
import Prism from "prismjs";
|
||||||
import "prismjs/themes/prism-twilight.css";
|
import type React from "react";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import "prismjs/themes/prism-okaidia.css";
|
||||||
import "prismjs/components/prism-yaml";
|
import "prismjs/components/prism-yaml";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { copyToClipboard } from "~/utils/clipboard";
|
import { copyToClipboard } from "~/utils/clipboard";
|
||||||
@@ -22,8 +23,8 @@ export const CodeBlock: React.FC<CodeBlockProps> = ({ code, language = "jsx", fi
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden rounded-sm bg-slate-900 ring-1 ring-white/10">
|
<div className="overflow-hidden rounded-sm bg-card-header ring-1 ring-white/10">
|
||||||
<div className="flex items-center justify-between border-b border-white/10 px-4 py-2 text-xs text-slate-400">
|
<div className="flex items-center justify-between border-b border-white/10 px-4 py-2 text-xs">
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="h-2.5 w-2.5 rounded-full bg-rose-500" />
|
<span className="h-2.5 w-2.5 rounded-full bg-rose-500" />
|
||||||
<span className="h-2.5 w-2.5 rounded-full bg-amber-500" />
|
<span className="h-2.5 w-2.5 rounded-full bg-amber-500" />
|
||||||
@@ -33,12 +34,12 @@ export const CodeBlock: React.FC<CodeBlockProps> = ({ code, language = "jsx", fi
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleCopy()}
|
onClick={() => handleCopy()}
|
||||||
className="cursor-pointer rounded-md bg-white/5 px-2 py-1 text-[11px] font-medium text-slate-300 ring-1 ring-inset ring-white/10 transition hover:bg-white/10 active:translate-y-px"
|
className="cursor-pointer rounded-md bg-white/5 px-2 py-1 text-[11px] font-medium ring-1 ring-inset ring-white/10 transition hover:bg-white/10 active:translate-y-px"
|
||||||
>
|
>
|
||||||
Copy
|
Copy
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<pre className="overflow-x-auto leading-6 text-xs m-0" style={{ marginTop: 0, marginBottom: 0 }}>
|
<pre className="overflow-x-auto text-xs m-0" style={{ marginTop: 0, borderRadius: 0, marginBottom: 0 }}>
|
||||||
<code className={`language-${language}`}>{code}</code>
|
<code className={`language-${language}`}>{code}</code>
|
||||||
</pre>
|
</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,64 +1,58 @@
|
|||||||
import * as React from "react"
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
import type * as React from "react";
|
||||||
|
|
||||||
import { cn } from "~/lib/utils"
|
import { cn } from "~/lib/utils";
|
||||||
|
|
||||||
function Tabs({
|
function Tabs({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||||
className,
|
return <TabsPrimitive.Root data-slot="tabs" className={cn("flex flex-col gap-2", className)} {...props} />;
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.Root
|
|
||||||
data-slot="tabs"
|
|
||||||
className={cn("flex flex-col gap-2", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabsList({
|
function TabsList({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||||
className,
|
return (
|
||||||
...props
|
<TabsPrimitive.List
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
data-slot="tabs-list"
|
||||||
return (
|
className={cn("inline-flex h-7 items-center gap-4 text-xs text-muted-foreground", className)}
|
||||||
<TabsPrimitive.List
|
{...props}
|
||||||
data-slot="tabs-list"
|
/>
|
||||||
className={cn(
|
);
|
||||||
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabsTrigger({
|
function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||||
className,
|
return (
|
||||||
...props
|
<TabsPrimitive.Trigger
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
data-slot="tabs-trigger"
|
||||||
return (
|
className={cn(
|
||||||
<TabsPrimitive.Trigger
|
"cursor-pointer group relative inline-flex h-7 items-center whitespace-nowrap text-xs font-medium transition-colors",
|
||||||
data-slot="tabs-trigger"
|
"text-muted-foreground data-[state=active]:text-foreground disabled:pointer-events-none disabled:opacity-50",
|
||||||
className={cn(
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-destructive/40 focus-visible:ring-offset-2 focus-visible:ring-offset-background",
|
||||||
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
||||||
className
|
// Padding: 20px horizontal (8px for bracket tick + 12px gap to text)
|
||||||
)}
|
"px-5",
|
||||||
{...props}
|
// Transparent orange background for active state
|
||||||
/>
|
"data-[state=active]:bg-[#FF453A]/10",
|
||||||
)
|
// Left bracket - vertical line
|
||||||
|
"before:absolute before:left-0 before:top-0 before:h-7 before:w-0.5 before:bg-[#5D6570] before:transition-colors data-[state=active]:before:bg-[#FF453A]",
|
||||||
|
// Left bracket - top tick
|
||||||
|
"after:absolute after:left-0 after:top-[-1px] after:w-2 after:h-0.5 after:bg-[#5D6570] after:transition-colors data-[state=active]:after:bg-[#FF453A]",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="relative z-10">{props.children}</span>
|
||||||
|
{/* Left bracket - bottom tick */}
|
||||||
|
<span className="absolute left-0 bottom-[-1px] h-0.5 w-2 bg-[#5D6570] transition-colors group-data-[state=active]:bg-[#FF453A]" />
|
||||||
|
{/* Right bracket - top tick */}
|
||||||
|
<span className="absolute right-0 top-[-1px] h-0.5 w-2 bg-[#5D6570] transition-colors group-data-[state=active]:bg-[#FF453A]" />
|
||||||
|
{/* Right bracket - vertical line */}
|
||||||
|
<span className="absolute right-0 top-0 h-7 w-0.5 bg-[#5D6570] transition-colors group-data-[state=active]:bg-[#FF453A]" />
|
||||||
|
{/* Right bracket - bottom tick */}
|
||||||
|
<span className="absolute right-0 bottom-[-1px] h-0.5 w-2 bg-[#5D6570] transition-colors group-data-[state=active]:bg-[#FF453A]" />
|
||||||
|
</TabsPrimitive.Trigger>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TabsContent({
|
function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||||
className,
|
return <TabsPrimitive.Content data-slot="tabs-content" className={cn("flex-1 outline-none", className)} {...props} />;
|
||||||
...props
|
|
||||||
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
|
||||||
return (
|
|
||||||
<TabsPrimitive.Content
|
|
||||||
data-slot="tabs-content"
|
|
||||||
className={cn("flex-1 outline-none", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||||
|
|||||||
@@ -111,7 +111,6 @@ export default function DetailsPage({ loaderData }: Route.ComponentProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
|
||||||
onClick={() => mountVol.mutate({ path: { name } })}
|
onClick={() => mountVol.mutate({ path: { name } })}
|
||||||
loading={mountVol.isPending}
|
loading={mountVol.isPending}
|
||||||
className={cn({ hidden: volume.status === "mounted" })}
|
className={cn({ hidden: volume.status === "mounted" })}
|
||||||
@@ -131,8 +130,8 @@ export default function DetailsPage({ loaderData }: Route.ComponentProps) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Tabs defaultValue="info" className="mt-0">
|
<Tabs defaultValue="info" className="mt-4">
|
||||||
<TabsList>
|
<TabsList className="mb-2">
|
||||||
<TabsTrigger value="info">Configuration</TabsTrigger>
|
<TabsTrigger value="info">Configuration</TabsTrigger>
|
||||||
<TabsTrigger value="docker">Docker</TabsTrigger>
|
<TabsTrigger value="docker">Docker</TabsTrigger>
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ import { listVolumesOptions } from "~/api-client/@tanstack/react-query.gen";
|
|||||||
import { CreateVolumeDialog } from "~/components/create-volume-dialog";
|
import { CreateVolumeDialog } from "~/components/create-volume-dialog";
|
||||||
import { StatusDot } from "~/components/status-dot";
|
import { StatusDot } from "~/components/status-dot";
|
||||||
import { Button } from "~/components/ui/button";
|
import { Button } from "~/components/ui/button";
|
||||||
|
import { Card } from "~/components/ui/card";
|
||||||
import { Input } from "~/components/ui/input";
|
import { Input } from "~/components/ui/input";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select";
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/components/ui/select";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table";
|
||||||
import { VolumeIcon } from "~/components/volume-icon";
|
import { VolumeIcon } from "~/components/volume-icon";
|
||||||
import type { Route } from "./+types/home";
|
import type { Route } from "./+types/home";
|
||||||
import { Card, CardHeader } from "~/components/ui/card";
|
|
||||||
|
|
||||||
export function meta(_: Route.MetaArgs) {
|
export function meta(_: Route.MetaArgs) {
|
||||||
return [
|
return [
|
||||||
|
|||||||
Reference in New Issue
Block a user