Add monorepo support to project (#682)
* feat: add monorepo support * fix: correct relatedProjects structure in vercel.json * update github actions with monorepo support * removed legacy dep * moved jsr config into web package
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import { type ComponentRef, forwardRef } from "react";
|
||||
|
||||
export const Accordion = AccordionPrimitive.Root;
|
||||
|
||||
export const AccordionHeader = AccordionPrimitive.Header;
|
||||
|
||||
export const AccordionItem = AccordionPrimitive.Item;
|
||||
|
||||
export const AccordionTrigger = forwardRef<
|
||||
ComponentRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex justify-between items-center w-full p-4 border-b border-slate-200 dark:border-slate-800 group",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
<ChevronDownIcon
|
||||
className="h-5 w-5 transition-transform duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden
|
||||
/>
|
||||
</AccordionPrimitive.Trigger>
|
||||
));
|
||||
|
||||
export const AccordionContent = forwardRef<
|
||||
ComponentRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"p-4 border-b border-slate-200 dark:border-slate-800",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
@@ -0,0 +1,147 @@
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { LockKeyholeOpenIcon, StarIcon } from "lucide-react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipArrow,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@components/UI/Tooltip.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type RGBColor = {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
a: number;
|
||||
};
|
||||
|
||||
interface AvatarProps {
|
||||
text: string | number;
|
||||
size?: "sm" | "lg";
|
||||
className?: string;
|
||||
showError?: boolean;
|
||||
showFavorite?: boolean;
|
||||
}
|
||||
|
||||
class ColorUtils {
|
||||
static hexToRgb(hex: number): RGBColor {
|
||||
return {
|
||||
r: (hex & 0xff0000) >> 16,
|
||||
g: (hex & 0x00ff00) >> 8,
|
||||
b: hex & 0x0000ff,
|
||||
a: 255,
|
||||
};
|
||||
}
|
||||
|
||||
static rgbToHex(color: RGBColor): number {
|
||||
return (
|
||||
(Math.round(color.a) << 24) |
|
||||
(Math.round(color.r) << 16) |
|
||||
(Math.round(color.g) << 8) |
|
||||
Math.round(color.b)
|
||||
);
|
||||
}
|
||||
|
||||
static isLight(color: RGBColor): boolean {
|
||||
const brightness = (color.r * 299 + color.g * 587 + color.b * 114) / 1000;
|
||||
return brightness > 127.5;
|
||||
}
|
||||
}
|
||||
|
||||
const getColorFromText = (text: string): RGBColor => {
|
||||
if (!text) {
|
||||
return { r: 0, g: 0, b: 0, a: 255 };
|
||||
}
|
||||
let hash = 0;
|
||||
for (let i = 0; i < text?.length; i++) {
|
||||
hash = text.charCodeAt(i) + ((hash << 5) - hash);
|
||||
hash |= 0;
|
||||
}
|
||||
|
||||
return {
|
||||
r: (hash & 0xff0000) >> 16,
|
||||
g: (hash & 0x00ff00) >> 8,
|
||||
b: hash & 0x0000ff,
|
||||
a: 255,
|
||||
};
|
||||
};
|
||||
|
||||
export const Avatar = ({
|
||||
text,
|
||||
size = "sm",
|
||||
showError = false,
|
||||
showFavorite = false,
|
||||
className,
|
||||
}: AvatarProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const sizes = {
|
||||
sm: "size-10 text-xs font-light",
|
||||
lg: "size-16 text-lg",
|
||||
};
|
||||
|
||||
const safeText = text?.toString().toUpperCase();
|
||||
const bgColor = getColorFromText(safeText);
|
||||
const isLight = ColorUtils.isLight(bgColor);
|
||||
const textColor = isLight ? "#000000" : "#FFFFFF";
|
||||
const initials = safeText?.slice(0, 4) ?? t("unknown.shortName");
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
`relative flex items-center justify-center rounded-full font-semibold
|
||||
`,
|
||||
sizes[size],
|
||||
className,
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: `rgb(${bgColor.r}, ${bgColor.g}, ${bgColor.b})`,
|
||||
color: textColor,
|
||||
}}
|
||||
>
|
||||
{showFavorite
|
||||
? (
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<StarIcon
|
||||
className="absolute -top-0.5 -right-0.5 z-10 size-4 stroke-1 fill-yellow-400"
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
color: `rgb(${bgColor.r}, ${bgColor.g}, ${bgColor.b})`,
|
||||
}}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-slate-800 dark:bg-slate-600 text-white px-4 py-1 rounded text-xs">
|
||||
{t("nodeDetail.favorite.label", { ns: "nodes" })}
|
||||
<TooltipArrow className="fill-slate-800 dark:fill-slate-600" />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
: null}
|
||||
{showError
|
||||
? (
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<LockKeyholeOpenIcon
|
||||
className="absolute -bottom-0.5 -right-0.5 z-10 size-4 text-red-500 stroke-3"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="bg-slate-800 dark:bg-slate-600 text-white px-4 py-1 rounded text-xs">
|
||||
{t("nodeDetail.error.label", { ns: "nodes" })}
|
||||
<TooltipArrow className="fill-slate-800 dark:fill-slate-600" />
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
: null}
|
||||
<p className="p-1 text-nowrap">
|
||||
{initials}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:opacity-50 dark:focus:ring-slate-400 disabled:cursor-not-allowed dark:focus:ring-offset-slate-900 cursor-pointer",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-slate-900 text-white dark:bg-slate-50 hover:dark:bg-slate-200 dark:text-slate-900 hover:bg-slate-500",
|
||||
destructive:
|
||||
"bg-red-500 text-white hover:bg-red-600 dark:hover:bg-red-600",
|
||||
success:
|
||||
"bg-green-500 text-white hover:bg-green-600 dark:hover:bg-green-600",
|
||||
outline:
|
||||
"bg-transparent border border-slate-400 hover:text-slate-400 dark:hover:text-slate-300 dark:border-slate-400 dark:text-slate-100 ",
|
||||
subtle:
|
||||
"bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-500 dark:text-white dark:hover:bg-slate-400",
|
||||
ghost:
|
||||
"bg-transparent hover:bg-slate-100 dark:hover:bg-slate-800 dark:text-slate-100 dark:hover:text-slate-100 data-[state=open]:bg-transparent dark:data-[state=open]:bg-transparent",
|
||||
link:
|
||||
"bg-transparent underline-offset-4 hover:underline text-slate-900 dark:text-slate-100 hover:bg-transparent dark:hover:bg-transparent",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 py-2 px-4",
|
||||
sm: "h-9 px-2 rounded-md",
|
||||
lg: "h-11 px-8 rounded-md",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export type ButtonVariant = VariantProps<typeof buttonVariants>["variant"];
|
||||
|
||||
export interface ButtonProps
|
||||
extends
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
icon?: React.ReactNode;
|
||||
iconAlignment?: "left" | "right";
|
||||
}
|
||||
|
||||
const Button = ({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
disabled,
|
||||
icon,
|
||||
iconAlignment = "left",
|
||||
children,
|
||||
...props
|
||||
}: ButtonProps) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
buttonVariants({ variant, size, className }),
|
||||
{ "cursor-not-allowed": disabled },
|
||||
"inline-flex items-center",
|
||||
)}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
>
|
||||
{icon && iconAlignment === "left" && (
|
||||
<span className={cn({ "mr-2": !!children })}>{icon}</span>
|
||||
)}
|
||||
{children}
|
||||
{icon && iconAlignment === "right" && (
|
||||
<span className={cn({ "ml-2": !!children })}>{icon}</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -0,0 +1,120 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||
import { Checkbox } from "@components/UI/Checkbox/index.tsx";
|
||||
import React from "react";
|
||||
|
||||
vi.mock("@components/UI/Label.tsx", () => ({
|
||||
Label: (
|
||||
{ children, className, htmlFor, id }: {
|
||||
children: React.ReactNode;
|
||||
className: string;
|
||||
htmlFor: string;
|
||||
id: string;
|
||||
},
|
||||
) => (
|
||||
<label
|
||||
data-testid="label-component"
|
||||
className={className}
|
||||
htmlFor={htmlFor}
|
||||
id={id}
|
||||
>
|
||||
{children}
|
||||
</label>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("Checkbox", () => {
|
||||
beforeEach(cleanup);
|
||||
|
||||
it("renders unchecked by default", () => {
|
||||
render(<Checkbox />);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
expect(checkbox).not.toBeChecked();
|
||||
expect(screen.queryByText("Check")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders checked when checked prop is true", () => {
|
||||
render(<Checkbox checked />);
|
||||
expect(screen.getByRole("checkbox")).toBeChecked();
|
||||
expect(screen.getByRole("presentation")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls onChange when clicked", () => {
|
||||
const onChange = vi.fn();
|
||||
render(<Checkbox onChange={onChange} />);
|
||||
|
||||
fireEvent.click(screen.getByRole("presentation"));
|
||||
expect(onChange).toHaveBeenCalledWith(true);
|
||||
|
||||
fireEvent.click(screen.getByRole("presentation"));
|
||||
expect(onChange).toHaveBeenCalledWith(false);
|
||||
});
|
||||
|
||||
it("uses provided id", () => {
|
||||
render(<Checkbox id="custom-id" />);
|
||||
expect(screen.getByRole("checkbox").id).toBe("custom-id");
|
||||
});
|
||||
|
||||
it("renders children in Label component", () => {
|
||||
render(<Checkbox>Test Label</Checkbox>);
|
||||
expect(screen.getByTestId("label-component")).toHaveTextContent(
|
||||
"Test Label",
|
||||
);
|
||||
});
|
||||
|
||||
it("applies custom className", () => {
|
||||
const { container } = render(<Checkbox className="custom-class" />);
|
||||
expect(container.firstChild).toHaveClass("custom-class");
|
||||
});
|
||||
|
||||
it("applies labelClassName to Label", () => {
|
||||
render(<Checkbox labelClassName="label-class">Test</Checkbox>);
|
||||
expect(screen.getByTestId("label-component")).toHaveClass("label-class");
|
||||
});
|
||||
|
||||
it("disables checkbox when disabled prop is true", () => {
|
||||
render(<Checkbox disabled />);
|
||||
expect(screen.getByRole("checkbox")).toBeDisabled();
|
||||
expect(screen.getByRole("presentation")).toHaveClass("opacity-50");
|
||||
});
|
||||
|
||||
it("does not call onChange when disabled", () => {
|
||||
const onChange = vi.fn();
|
||||
render(<Checkbox onChange={onChange} disabled />);
|
||||
|
||||
fireEvent.click(screen.getByRole("presentation"));
|
||||
expect(onChange).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sets required attribute when required prop is true", () => {
|
||||
render(<Checkbox required />);
|
||||
expect(screen.getByRole("checkbox")).toHaveAttribute("required");
|
||||
});
|
||||
|
||||
it("sets name attribute when name prop is provided", () => {
|
||||
render(<Checkbox name="test-name" />);
|
||||
expect(screen.getByRole("checkbox")).toHaveAttribute("name", "test-name");
|
||||
});
|
||||
|
||||
it("passes through additional props", () => {
|
||||
render(<Checkbox data-testid="extra-prop" />);
|
||||
expect(screen.getByRole("checkbox")).toHaveAttribute(
|
||||
"data-testid",
|
||||
"extra-prop",
|
||||
);
|
||||
});
|
||||
|
||||
it("toggles checked state correctly", () => {
|
||||
render(<Checkbox />);
|
||||
const checkbox = screen.getByRole("checkbox");
|
||||
const presentation = screen.getByRole("presentation");
|
||||
|
||||
expect(checkbox).not.toBeChecked();
|
||||
|
||||
fireEvent.click(presentation);
|
||||
expect(checkbox).toBeChecked();
|
||||
|
||||
fireEvent.click(presentation);
|
||||
expect(checkbox).not.toBeChecked();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,100 @@
|
||||
import { useEffect, useId, useState } from "react";
|
||||
import { Check } from "lucide-react";
|
||||
import { Label } from "@components/UI/Label.tsx";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
interface CheckboxProps {
|
||||
checked?: boolean;
|
||||
onChange?: (checked: boolean) => void;
|
||||
className?: string;
|
||||
labelClassName?: string;
|
||||
id?: string;
|
||||
children?: React.ReactNode;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export function Checkbox({
|
||||
checked,
|
||||
onChange,
|
||||
className,
|
||||
labelClassName,
|
||||
id: propId,
|
||||
children,
|
||||
disabled = false,
|
||||
required = false,
|
||||
name,
|
||||
...rest
|
||||
}: CheckboxProps) {
|
||||
const generatedId = useId();
|
||||
const id = propId || generatedId;
|
||||
|
||||
const [isChecked, setIsChecked] = useState(checked || false);
|
||||
|
||||
// Make sure setIsChecked state updates with checked
|
||||
useEffect(() => {
|
||||
setIsChecked(checked || false);
|
||||
}, [checked]);
|
||||
|
||||
const handleToggle = () => {
|
||||
if (disabled) return;
|
||||
|
||||
const newChecked = !isChecked;
|
||||
setIsChecked(newChecked);
|
||||
onChange?.(newChecked);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center", className)}>
|
||||
<div className="relative flex items-start">
|
||||
<div className="flex items-center h-5">
|
||||
<input
|
||||
type="checkbox"
|
||||
id={id}
|
||||
checked={isChecked}
|
||||
onChange={handleToggle}
|
||||
disabled={disabled}
|
||||
required={required}
|
||||
name={name}
|
||||
className="sr-only"
|
||||
{...rest}
|
||||
/>
|
||||
<div
|
||||
onClick={handleToggle}
|
||||
role="presentation"
|
||||
className={cn(
|
||||
"w-6 h-6 border-2 border-gray-500 rounded-md flex items-center justify-center",
|
||||
disabled
|
||||
? "opacity-50 cursor-not-allowed"
|
||||
: "cursor-pointer focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2",
|
||||
isChecked ? "" : "",
|
||||
)}
|
||||
>
|
||||
{isChecked && (
|
||||
<div className="animate-fade-in scale-100 opacity-100">
|
||||
<Check className="w-4 h-4" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{children && (
|
||||
<div className="ml-3 text-sm">
|
||||
<Label
|
||||
htmlFor={id}
|
||||
id={`${id}-label`}
|
||||
className={cn(
|
||||
"text-gray-900 dark:text-gray-900",
|
||||
disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer",
|
||||
labelClassName,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import type { DialogProps } from "@radix-ui/react-dialog";
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import { Search } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { Dialog, DialogContent } from "@components/UI/Dialog.tsx";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const Command = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-full w-full flex-col overflow-hidden rounded-lg bg-white dark:bg-slate-800",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Command.displayName = CommandPrimitive.displayName;
|
||||
|
||||
const CommandDialog = ({ children, ...props }: DialogProps) => {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogContent className="overflow-hidden p-0 shadow-2xl [&_[dialog-overlay]]:bg-red-100">
|
||||
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-slate-500 [&_[cmdk-group]]:px-2 [&_[cmdk-group]_+[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
const CommandInput = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Input>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
className="flex items-center border-b border-b-slate-100 px-4 dark:border-b-slate-700"
|
||||
cmdk-input-wrapper=""
|
||||
>
|
||||
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50 text-white" />
|
||||
<CommandPrimitive.Input
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-slate-400 disabled:cursor-not-allowed disabled:opacity-50 dark:text-slate-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
|
||||
CommandInput.displayName = CommandPrimitive.Input.displayName;
|
||||
|
||||
const CommandList = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.List
|
||||
ref={ref}
|
||||
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandList.displayName = CommandPrimitive.List.displayName;
|
||||
|
||||
const CommandEmpty = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Empty>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
|
||||
>((props, ref) => (
|
||||
<CommandPrimitive.Empty
|
||||
ref={ref}
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandEmpty.displayName = CommandPrimitive.Empty.displayName;
|
||||
|
||||
const CommandGroup = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Group>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Group
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"overflow-hidden py-3 px-2 text-slate-700 dark:text-slate-200 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:pb-1.5 [&_[cmdk-group-heading]]:text-sm [&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:text-slate-900 dark:[&_[cmdk-group-heading]]:text-slate-300",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandGroup.displayName = CommandPrimitive.Group.displayName;
|
||||
|
||||
const CommandSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 h-px bg-slate-100 dark:bg-slate-200", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
CommandSeparator.displayName = CommandPrimitive.Separator.displayName;
|
||||
|
||||
const CommandItem = React.forwardRef<
|
||||
React.ElementRef<typeof CommandPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-md py-1.5 px-2 text-sm font-medium outline-hidden aria-selected:bg-slate-100 data-[disabled='true']:pointer-events-none data-[disabled='true']:opacity-50 dark:aria-selected:bg-slate-700 dark:text-white ",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
CommandItem.displayName = CommandPrimitive.Item.displayName;
|
||||
|
||||
const CommandShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-slate-500",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
CommandShortcut.displayName = "CommandShortcut";
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
CommandShortcut,
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { X } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const Dialog = DialogPrimitive.Root;
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
const DialogPortal = ({
|
||||
children,
|
||||
...props
|
||||
}: DialogPrimitive.DialogPortalProps) => (
|
||||
<DialogPrimitive.Portal {...props}>
|
||||
<div className="fixed inset-0 z-50 flex items-start justify-center sm:items-center">
|
||||
{children}
|
||||
</div>
|
||||
</DialogPrimitive.Portal>
|
||||
);
|
||||
DialogPortal.displayName = DialogPrimitive.Portal.displayName;
|
||||
|
||||
const DialogOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/50 backdrop-blur-xs transition-opacity animate-in fade-in",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed z-50 grid w-full bg-white dark:bg-slate-800 dark:text-slate-200 text-slate-900 max-w-[512px] max-h-[100vh] overflow-y-auto scale-100 gap-4 p-6 opacity-100 animate-in fade-in-90 slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 sm:slide-in-from-bottom-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogClose = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof DialogPrimitive.Close>) => (
|
||||
<DialogPrimitive.Close
|
||||
aria-label="Close"
|
||||
data-testid="dialog-close-button"
|
||||
className={cn(
|
||||
"absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:outline-hidden focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-slate-100 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
);
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left ",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogHeader.displayName = "DialogHeader";
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DialogFooter.displayName = "DialogFooter";
|
||||
|
||||
const DialogTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold text-slate-900 dark:text-slate-100",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName;
|
||||
|
||||
const DialogDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-slate-800 dark:text-slate-200", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
@@ -0,0 +1,201 @@
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
const DropdownMenuSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-xs py-1.5 px-2 text-sm font-medium outline-hidden focus:bg-slate-100 data-[state=open]:bg-slate-100 dark:focus:bg-slate-700 dark:data-[state=open]:bg-slate-700",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
));
|
||||
DropdownMenuSubTrigger.displayName =
|
||||
DropdownMenuPrimitive.SubTrigger.displayName;
|
||||
|
||||
const DropdownMenuSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-100 bg-white p-1 text-slate-700 shadow-md animate-in slide-in-from-left-1 dark:border-slate-800 dark:bg-slate-800 dark:text-slate-400",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSubContent.displayName =
|
||||
DropdownMenuPrimitive.SubContent.displayName;
|
||||
|
||||
const DropdownMenuContent = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Portal>
|
||||
<DropdownMenuPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-100 bg-white p-1 text-slate-700 shadow-md animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 dark:border-slate-800 dark:bg-slate-800 dark:text-slate-400",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
));
|
||||
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
|
||||
|
||||
const DropdownMenuItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-xs py-1.5 px-2 text-sm font-medium outline-hidden focus:bg-slate-100 data-disabled:pointer-events-none data-disabled:opacity-50 dark:focus:bg-slate-700",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
|
||||
|
||||
const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-xs py-1.5 pl-8 pr-2 text-sm font-medium outline-hidden focus:bg-slate-100 data-disabled:pointer-events-none data-disabled:opacity-50 dark:focus:bg-slate-700",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
));
|
||||
DropdownMenuCheckboxItem.displayName =
|
||||
DropdownMenuPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const DropdownMenuRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-xs py-1.5 pl-8 pr-2 text-sm font-medium outline-hidden focus:bg-slate-100 data-disabled:pointer-events-none data-disabled:opacity-50 dark:focus:bg-slate-700",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
));
|
||||
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName;
|
||||
|
||||
const DropdownMenuLabel = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold text-slate-900 dark:text-slate-300",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
|
||||
|
||||
const DropdownMenuSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-slate-100 dark:bg-slate-700", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
|
||||
|
||||
const DropdownMenuShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-slate-500",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut";
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
import newGithubIssueUrl from "@core/utils/github.ts";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
import { Heading } from "@components/UI/Typography/Heading.tsx";
|
||||
import { Link } from "@components/UI/Typography/Link.tsx";
|
||||
import { P } from "@components/UI/Typography/P.tsx";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
export function ErrorPage({ error }: { error: Error }) {
|
||||
if (!error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<article className="w-full h-screen overflow-y-auto bg-background-primary text-text-primary">
|
||||
<section className="flex shrink md:flex-row gap-16 mt-20 px-4 md:px-8 text-lg md:text-xl space-y-2 place-items-center dark:bg-background-primary text-slate-900 dark:text-text-primary">
|
||||
<div>
|
||||
<Heading as="h2" className="text-text-primary">
|
||||
{t("errorPage.title")}
|
||||
</Heading>
|
||||
<P>
|
||||
{t("errorPage.description1")}
|
||||
</P>
|
||||
<P>
|
||||
{t("errorPage.description2")}
|
||||
</P>
|
||||
<P>Please include the following information in your report:</P>
|
||||
<ul className="list-disc list-inside text-sm">
|
||||
<li>{t("errorPage.reportSteps.step1")}</li>
|
||||
<li>{t("errorPage.reportSteps.step2")}</li>
|
||||
<li>{t("errorPage.reportSteps.step3")}</li>
|
||||
<li>{t("errorPage.reportSteps.step4")}</li>
|
||||
</ul>
|
||||
<P>
|
||||
<Trans
|
||||
i18nKey="errorPage.reportLink"
|
||||
components={[
|
||||
<Link
|
||||
key="github"
|
||||
href={newGithubIssueUrl({
|
||||
repoUrl: "https://github.com/meshtastic/web",
|
||||
template: "bug.yml",
|
||||
title:
|
||||
"[Bug]: An unhandled error occurred. <Add details here>",
|
||||
logs: error?.stack,
|
||||
})}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
<ExternalLink size={24} className="inline-block ml-2" />
|
||||
</P>
|
||||
<P>
|
||||
<Trans
|
||||
i18nKey="errorPage.dashboardLink"
|
||||
components={[<Link key="dashboard" href="/" />]}
|
||||
/>
|
||||
</P>
|
||||
</div>
|
||||
|
||||
<div className="hidden md:block md:max-w-64 lg:max-w-80 w-full aspect-suqare">
|
||||
<img
|
||||
src="/chirpy.svg"
|
||||
alt="Chirpy the Meshtastic error"
|
||||
className="max-w-full h-auto"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
<details className="mt-8 px-4 md:px-8 text-lg md:text-xl space-y-2 text-md whitespace-pre-wrap break-all">
|
||||
<summary className="cursor-pointer">
|
||||
{t("errorPage.detailsSummary")}
|
||||
</summary>
|
||||
<span className="text-sm mt-4">
|
||||
{error?.message && (
|
||||
<>
|
||||
<label htmlFor="message">
|
||||
{t("errorPage.errorMessageLabel")}
|
||||
</label>
|
||||
<p
|
||||
id="message"
|
||||
className="text-slate-400 break-words overflow-wrap"
|
||||
>
|
||||
{error.message}
|
||||
</p>
|
||||
</> // TODO: Use Trans for the label and message together?
|
||||
)}
|
||||
{error?.stack && (
|
||||
<>
|
||||
<label htmlFor="stack">{t("errorPage.stackTraceLabel")}</label>
|
||||
<p
|
||||
id="stack"
|
||||
className="text-slate-400 break-words overflow-wrap"
|
||||
>
|
||||
{error.stack}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{!error?.message && !error?.stack && (
|
||||
<p className="text-slate-400">
|
||||
{t("errorPage.fallbackError", { error: error.toString() })}
|
||||
</p>
|
||||
)}
|
||||
</span>
|
||||
</details>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
|
||||
type FooterProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const Footer = ({ className, ...props }: FooterProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<footer
|
||||
className={cn(
|
||||
"flex mt-auto justify-center py-2 px-4 text-sm lg:text-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="justify-start px-2">
|
||||
<span className="font-semibold text-gray-500/40 dark:text-gray-400/40">
|
||||
{t("footer.commitSha", {
|
||||
sha: String(import.meta.env.VITE_COMMIT_HASH)?.toUpperCase(),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<p className="ml-auto mr-auto text-gray-500 dark:text-gray-400">
|
||||
<Trans
|
||||
i18nKey="footer.text"
|
||||
components={[
|
||||
<a
|
||||
key="vercel"
|
||||
rel="noopener noreferrer"
|
||||
href="https://vercel.com/?utm_source=meshtastic&utm_campaign=oss"
|
||||
className="hover:underline text-link"
|
||||
/>,
|
||||
<a
|
||||
key="legal"
|
||||
rel="noopener noreferrer"
|
||||
href="https://meshtastic.org/docs/legal"
|
||||
className="hover:underline text-link"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
</footer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Footer;
|
||||
@@ -0,0 +1,141 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Button, type ButtonVariant } from "@components/UI/Button.tsx";
|
||||
import { Input } from "@components/UI/Input.tsx";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@components/UI/Select.tsx";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface ActionButton {
|
||||
text: string;
|
||||
onClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||
variant: ButtonVariant;
|
||||
className?: string;
|
||||
}
|
||||
[];
|
||||
|
||||
export interface GeneratorProps extends React.BaseHTMLAttributes<HTMLElement> {
|
||||
type: "text" | "password";
|
||||
devicePSKBitCount?: number;
|
||||
value: string;
|
||||
id: string;
|
||||
variant: "default" | "invalid" | "dirty";
|
||||
actionButtons: ActionButton[];
|
||||
bits?: { text: string; value: string; key: string }[];
|
||||
selectChange: (event: string) => void;
|
||||
inputChange: React.ChangeEventHandler<HTMLInputElement>;
|
||||
showPasswordToggle?: boolean;
|
||||
showCopyButton?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const Generator = (
|
||||
{
|
||||
type,
|
||||
devicePSKBitCount,
|
||||
id = "pskInput",
|
||||
variant,
|
||||
value,
|
||||
actionButtons,
|
||||
bits,
|
||||
selectChange,
|
||||
inputChange,
|
||||
disabled,
|
||||
showPasswordToggle,
|
||||
showCopyButton,
|
||||
...props
|
||||
}: GeneratorProps,
|
||||
) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const passwordRequiredBitSize = bits ? bits : [
|
||||
{
|
||||
text: t("security.256bit"),
|
||||
value: "32",
|
||||
key: "bit256",
|
||||
},
|
||||
{
|
||||
text: t("security.128bit"),
|
||||
value: "16",
|
||||
key: "bit128",
|
||||
},
|
||||
{
|
||||
text: t("security.8bit"),
|
||||
value: "1",
|
||||
key: "bit8",
|
||||
},
|
||||
{
|
||||
text: t("security.0bit"),
|
||||
value: "0",
|
||||
key: "bit0",
|
||||
},
|
||||
];
|
||||
|
||||
// Invokes onChange event on the input element when the value changes from the parent component
|
||||
useEffect(() => {
|
||||
if (!inputRef.current) return;
|
||||
const setValue = Object.getOwnPropertyDescriptor(
|
||||
HTMLInputElement.prototype,
|
||||
"value",
|
||||
)?.set;
|
||||
|
||||
if (!setValue) return;
|
||||
inputRef.current.value = "";
|
||||
setValue.call(inputRef.current, value);
|
||||
inputRef.current.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
}, [value]);
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
type={type}
|
||||
id={id}
|
||||
variant={variant}
|
||||
value={value}
|
||||
onChange={inputChange}
|
||||
disabled={disabled}
|
||||
ref={inputRef}
|
||||
showCopyButton={showCopyButton}
|
||||
showPasswordToggle={showPasswordToggle}
|
||||
/>
|
||||
<Select
|
||||
value={devicePSKBitCount?.toString()}
|
||||
onValueChange={(e) => selectChange(e)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger className="w-36 ml-2">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="w-36">
|
||||
{passwordRequiredBitSize.map(({ text, value, key }) => (
|
||||
<SelectItem key={key} value={value} className="w-36">
|
||||
{text}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex ml-2 space-x-2">
|
||||
{actionButtons?.map(({ text, onClick, variant, className }) => (
|
||||
<Button
|
||||
key={text}
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
disabled={disabled}
|
||||
variant={variant}
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Generator.displayName = "Button";
|
||||
|
||||
export { Generator };
|
||||
@@ -0,0 +1,219 @@
|
||||
import * as React from "react";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { Check, Copy, Eye, EyeOff, type LucideIcon, X } from "lucide-react";
|
||||
import { useCopyToClipboard } from "@core/hooks/useCopyToClipboard.ts";
|
||||
import { usePasswordVisibilityToggle } from "@core/hooks/usePasswordVisibilityToggle.ts";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const cnInvalidBase = "border-2 border-red-500 dark:border-red-500";
|
||||
const cnDirtyBase = "border-2 border-sky-500 dark:border-sky-500";
|
||||
|
||||
const inputVariants = cva(
|
||||
"flex h-10 w-full rounded-md border border-slate-300 bg-transparent py-2 px-3 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-1 focus:ring-slate-400 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-500 dark:bg-transparet dark:text-slate-100 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-600",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-slate-300 dark:border-slate-500",
|
||||
invalid: `${cnInvalidBase} focus:ring-red-500 dark:focus:ring-red-500`,
|
||||
dirty: `${cnDirtyBase} focus:ring-sky-500 dark:focus:ring-sky-500`,
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
type InputActionType = {
|
||||
id: string;
|
||||
icon: LucideIcon;
|
||||
onClick: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
ariaLabel: string;
|
||||
tooltip?: string;
|
||||
condition?: boolean;
|
||||
};
|
||||
|
||||
export interface InputProps
|
||||
extends
|
||||
Omit<React.InputHTMLAttributes<HTMLInputElement>, "prefix" | "suffix">,
|
||||
VariantProps<typeof inputVariants> {
|
||||
prefix?: React.ReactNode;
|
||||
suffix?: React.ReactNode;
|
||||
showPasswordToggle?: boolean;
|
||||
showCopyButton?: boolean;
|
||||
showClearButton?: boolean;
|
||||
containerClassName?: string;
|
||||
}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
containerClassName,
|
||||
variant,
|
||||
disabled,
|
||||
type = "text",
|
||||
prefix,
|
||||
suffix,
|
||||
showPasswordToggle,
|
||||
showCopyButton,
|
||||
showClearButton,
|
||||
value,
|
||||
onChange,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { isVisible, toggleVisibility } = usePasswordVisibilityToggle();
|
||||
const { copy, isCopied } = useCopyToClipboard({ timeout: 1500 });
|
||||
const { t } = useTranslation("ui");
|
||||
|
||||
const potentialActions: InputActionType[] = [
|
||||
{
|
||||
id: "clear-input",
|
||||
icon: X,
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
if (onChange) {
|
||||
const event = {
|
||||
target: { value: "" },
|
||||
currentTarget: { value: "" },
|
||||
} as React.ChangeEvent<HTMLInputElement>;
|
||||
onChange(event);
|
||||
}
|
||||
if (ref && typeof ref !== "function" && ref.current) {
|
||||
ref.current.focus();
|
||||
}
|
||||
},
|
||||
ariaLabel: t("clearInput.label"),
|
||||
tooltip: t("clearInput.label"),
|
||||
condition: !!showClearButton && !!value,
|
||||
},
|
||||
{
|
||||
id: "toggle-visibility",
|
||||
icon: isVisible ? EyeOff : Eye,
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
toggleVisibility();
|
||||
},
|
||||
ariaLabel: isVisible
|
||||
? t("notifications.hidePassword.label")
|
||||
: t("notifications.showPassword.label"),
|
||||
tooltip: isVisible
|
||||
? t("notifications.hidePassword.label")
|
||||
: t("notifications.showPassword.label"),
|
||||
condition: !!showPasswordToggle && type === "password",
|
||||
},
|
||||
{
|
||||
id: "copy-value",
|
||||
icon: isCopied ? Check : Copy,
|
||||
onClick: (e) => {
|
||||
e.stopPropagation();
|
||||
if (value !== undefined && value !== null) {
|
||||
copy(String(value));
|
||||
}
|
||||
},
|
||||
ariaLabel: isCopied
|
||||
? t("notifications.copied.label")
|
||||
: t("notifications.copyToClipboard.label"),
|
||||
tooltip: isCopied
|
||||
? t("notifications.copied.label")
|
||||
: t("notifications.copyToClipboard.label"),
|
||||
condition: !!showCopyButton,
|
||||
},
|
||||
];
|
||||
|
||||
const actions = potentialActions.filter((action) => action.condition);
|
||||
|
||||
const inputType = showPasswordToggle
|
||||
? (isVisible ? "text" : "password")
|
||||
: type;
|
||||
|
||||
const hasPrefix = !!prefix;
|
||||
const hasSuffix = !!suffix;
|
||||
const hasActions = actions.length > 0;
|
||||
|
||||
const inputClassName = cn(
|
||||
inputVariants({ variant }),
|
||||
hasActions && !hasSuffix && "pr-10",
|
||||
hasPrefix && "rounded-l-none",
|
||||
className,
|
||||
);
|
||||
|
||||
const extrasClassName = cn([
|
||||
variant === "invalid" && `${cnInvalidBase} border-l-0`,
|
||||
variant === "dirty" && `${cnDirtyBase} border-l-0`,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("relative flex w-full items-stretch", containerClassName)}
|
||||
>
|
||||
{prefix && (
|
||||
<span className="inline-flex items-center rounded-l-md border border-r-0 border-slate-300 bg-slate-100/80 px-3 text-sm text-slate-600 dark:border-slate-700 dark:bg-slate-200 dark:text-slate-700">
|
||||
{prefix}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<input
|
||||
type={inputType === "password" && isVisible ? "text" : inputType}
|
||||
className={inputClassName}
|
||||
ref={ref}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<div className="absolute right-0 top-0 flex h-full items-stretch">
|
||||
{suffix && (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center border border-l-0 border-slate-300 bg-slate-100/80 px-3 text-sm text-slate-600 dark:border-slate-700 dark:bg-slate-700 dark:text-slate-300",
|
||||
extrasClassName,
|
||||
!hasActions && "rounded-r-md",
|
||||
)}
|
||||
>
|
||||
{suffix}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{hasActions && (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center divide-x divide-slate-300 border border-l-0 border-slate-300 dark:divide-slate-700 dark:border-slate-500",
|
||||
extrasClassName,
|
||||
disabled &&
|
||||
"border-slate-200 dark:border-slate-700 divide-slate-200",
|
||||
!hasSuffix && "rounded-r-md",
|
||||
"bg-white dark:bg-slate-800",
|
||||
)}
|
||||
>
|
||||
{actions.map((action) => (
|
||||
<button
|
||||
key={action.id}
|
||||
type="button"
|
||||
className={cn(
|
||||
"inline-flex h-full items-center justify-center px-2.5 text-slate-500 hover:bg-slate-100 hover:text-slate-700 focus:outline-none focus:ring-1 focus:ring-slate-400 focus:ring-offset-0 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-slate-200 dark:focus:ring-slate-500 last:hover:rounded-r-md last:dark:hover:rounded-r-md",
|
||||
disabled && "text-slate-300 dark:text-slate-600",
|
||||
action.id === "copy-value" && isCopied &&
|
||||
"text-green-600 dark:text-green-500",
|
||||
)}
|
||||
onClick={action.onClick}
|
||||
aria-label={action.ariaLabel}
|
||||
title={action.tooltip || action.ariaLabel}
|
||||
>
|
||||
<action.icon size={18} aria-hidden="true" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input, inputVariants };
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Label.displayName = LabelPrimitive.Root.displayName;
|
||||
|
||||
export { Label };
|
||||
@@ -0,0 +1,234 @@
|
||||
import * as MenubarPrimitive from "@radix-ui/react-menubar";
|
||||
import { Check, ChevronRight, Circle } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const MenubarMenu = MenubarPrimitive.Menu;
|
||||
|
||||
const MenubarGroup = MenubarPrimitive.Group;
|
||||
|
||||
const MenubarPortal = MenubarPrimitive.Portal;
|
||||
|
||||
const MenubarSub = MenubarPrimitive.Sub;
|
||||
|
||||
const MenubarRadioGroup = MenubarPrimitive.RadioGroup;
|
||||
|
||||
const Menubar = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 items-center space-x-1 rounded-md border border-slate-300 bg-white p-1 dark:border-slate-700 dark:bg-slate-800",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Menubar.displayName = MenubarPrimitive.Root.displayName;
|
||||
|
||||
const MenubarTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-[0.2rem] py-1.5 px-3 text-sm font-medium outline-hidden focus:bg-slate-100 data-[state=open]:bg-slate-100 dark:focus:bg-slate-700 dark:data-[state=open]:bg-slate-700",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarTrigger.displayName = MenubarPrimitive.Trigger.displayName;
|
||||
|
||||
const MenubarSubTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.SubTrigger>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, children, ...props }, ref) => (
|
||||
<MenubarPrimitive.SubTrigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex cursor-default select-none items-center rounded-xs py-1.5 px-2 text-sm font-medium outline-hidden focus:bg-slate-100 data-[state=open]:bg-slate-100 dark:focus:bg-slate-700 dark:data-[state=open]:bg-slate-700",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRight className="ml-auto h-4 w-4" />
|
||||
</MenubarPrimitive.SubTrigger>
|
||||
));
|
||||
MenubarSubTrigger.displayName = MenubarPrimitive.SubTrigger.displayName;
|
||||
|
||||
const MenubarSubContent = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.SubContent>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.SubContent>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-100 bg-white p-1 shadow-md animate-in slide-in-from-left-1 dark:border-slate-700 dark:bg-slate-800",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarSubContent.displayName = MenubarPrimitive.SubContent.displayName;
|
||||
|
||||
const MenubarContent = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Content>
|
||||
>(
|
||||
(
|
||||
{ className, align = "start", alignOffset = -4, sideOffset = 8, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<MenubarPrimitive.Portal>
|
||||
<MenubarPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
alignOffset={alignOffset}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[12rem] overflow-hidden rounded-md border border-slate-100 bg-white p-1 text-slate-700 shadow-md animate-in slide-in-from-top-1 dark:border-slate-800 dark:bg-slate-800 dark:text-slate-400",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</MenubarPrimitive.Portal>
|
||||
),
|
||||
);
|
||||
MenubarContent.displayName = MenubarPrimitive.Content.displayName;
|
||||
|
||||
const MenubarItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<MenubarPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-xs py-1.5 px-2 text-sm font-medium outline-hidden focus:bg-slate-100 data-disabled:pointer-events-none data-disabled:opacity-50 dark:focus:bg-slate-700",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarItem.displayName = MenubarPrimitive.Item.displayName;
|
||||
|
||||
const MenubarCheckboxItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.CheckboxItem>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.CheckboxItem>
|
||||
>(({ className, children, checked, ...props }, ref) => (
|
||||
<MenubarPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-xs py-1.5 pl-8 pr-2 text-sm font-medium outline-hidden focus:bg-slate-100 data-disabled:pointer-events-none data-disabled:opacity-50 dark:focus:bg-slate-700",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.CheckboxItem>
|
||||
));
|
||||
MenubarCheckboxItem.displayName = MenubarPrimitive.CheckboxItem.displayName;
|
||||
|
||||
const MenubarRadioItem = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.RadioItem>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.RadioItem>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<MenubarPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-xs py-1.5 pl-8 pr-2 text-sm font-medium outline-hidden focus:bg-slate-100 data-disabled:pointer-events-none data-disabled:opacity-50 dark:focus:bg-slate-700",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<MenubarPrimitive.ItemIndicator>
|
||||
<Circle className="h-2 w-2 fill-current" />
|
||||
</MenubarPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</MenubarPrimitive.RadioItem>
|
||||
));
|
||||
MenubarRadioItem.displayName = MenubarPrimitive.RadioItem.displayName;
|
||||
|
||||
const MenubarLabel = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}
|
||||
>(({ className, inset, ...props }, ref) => (
|
||||
<MenubarPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold text-slate-900 dark:text-slate-300",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarLabel.displayName = MenubarPrimitive.Label.displayName;
|
||||
|
||||
const MenubarSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof MenubarPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof MenubarPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<MenubarPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-slate-100 dark:bg-slate-700", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
MenubarSeparator.displayName = MenubarPrimitive.Separator.displayName;
|
||||
|
||||
const MenubarShortcut = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLSpanElement>) => {
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-slate-500",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
MenubarShortcut.displayname = "MenubarShortcut";
|
||||
|
||||
export {
|
||||
Menubar,
|
||||
MenubarCheckboxItem,
|
||||
MenubarContent,
|
||||
MenubarGroup,
|
||||
MenubarItem,
|
||||
MenubarLabel,
|
||||
MenubarMenu,
|
||||
MenubarPortal,
|
||||
MenubarRadioGroup,
|
||||
MenubarRadioItem,
|
||||
MenubarSeparator,
|
||||
MenubarShortcut,
|
||||
MenubarSub,
|
||||
MenubarSubContent,
|
||||
MenubarSubTrigger,
|
||||
MenubarTrigger,
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
import { cn } from "../../core/utils/cn.ts";
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
interface MultiSelectProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const MultiSelect = ({ children, className = "" }: MultiSelectProps) => {
|
||||
return <div className={cn("flex flex-wrap gap-2", className)}>{children}
|
||||
</div>;
|
||||
};
|
||||
|
||||
interface MultiSelectItemProps {
|
||||
name: string;
|
||||
value: string;
|
||||
checked: boolean;
|
||||
onCheckedChange: (name: string, value: boolean) => void;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const MultiSelectItem = ({
|
||||
name,
|
||||
value,
|
||||
checked,
|
||||
onCheckedChange,
|
||||
children,
|
||||
className = "",
|
||||
}: MultiSelectItemProps) => {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
name={name}
|
||||
id={value}
|
||||
checked={checked}
|
||||
onCheckedChange={(val) => onCheckedChange(name, !!val)}
|
||||
className={cn(
|
||||
`
|
||||
inline-flex items-center rounded-md px-3 py-2 text-sm transition-colors
|
||||
border border-slate-300
|
||||
hover:bg-slate-100 dark:hover:bg-slate-800
|
||||
focus:outline-hidden focus:ring-2 focus:ring-slate-400 focus:ring-offset-2
|
||||
data-[state=checked]:bg-slate-100 dark:data-[state=checked]:bg-slate-700`,
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator className="mr-2">
|
||||
<Check className="h-4 w-4 animate-in zoom-in duration-200" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
{children}
|
||||
</CheckboxPrimitive.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export { MultiSelect, MultiSelectItem };
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const Popover = PopoverPrimitive.Root;
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger;
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border border-slate-100 bg-white p-4 shadow-md outline-hidden animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 dark:border-slate-800 dark:bg-slate-800",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
));
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName;
|
||||
|
||||
export { Popover, PopoverContent, PopoverTrigger };
|
||||
@@ -0,0 +1,46 @@
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
));
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 border-t border-t-transparent p-[1px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-slate-300 dark:bg-slate-700" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
));
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
|
||||
|
||||
export { ScrollArea, ScrollBar };
|
||||
@@ -0,0 +1,111 @@
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { Check, ChevronDown } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
const SelectValue = SelectPrimitive.Value;
|
||||
|
||||
const SelectTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex h-10 w-full items-center justify-between rounded-md border border-slate-300 bg-transparent py-2 px-3 text-sm placeholder:text-slate-400 focus:outline-hidden focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||
</SelectPrimitive.Trigger>
|
||||
));
|
||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
|
||||
|
||||
const SelectContent = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-100 bg-white text-slate-700 shadow-md animate-in fade-in-80 dark:border-slate-800 dark:text-slate-900",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.Viewport className="p-1">
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
));
|
||||
SelectContent.displayName = SelectPrimitive.Content.displayName;
|
||||
|
||||
const SelectLabel = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"py-1.5 pr-2 pl-8 text-sm font-semibold text-slate-900 dark:text-white",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectLabel.displayName = SelectPrimitive.Label.displayName;
|
||||
|
||||
const SelectItem = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-xs py-1.5 pr-2 pl-8 text-sm font-medium outline-hidden focus:bg-slate-200 data-disabled:pointer-events-none data-disabled:opacity-50 dark:focus:bg-slate-200",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Check className="h-4 w-4" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
));
|
||||
SelectItem.displayName = SelectPrimitive.Item.displayName;
|
||||
|
||||
const SelectSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Separator
|
||||
ref={ref}
|
||||
className={cn("-mx-1 my-1 h-px bg-slate-100 dark:bg-slate-700", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const Separator = React.forwardRef<
|
||||
React.ElementRef<typeof SeparatorPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
|
||||
>(
|
||||
(
|
||||
{ className, orientation = "horizontal", decorative = true, ...props },
|
||||
ref,
|
||||
) => (
|
||||
<SeparatorPrimitive.Root
|
||||
ref={ref}
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-slate-200 dark:bg-slate-700",
|
||||
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Separator.displayName = SeparatorPrimitive.Root.displayName;
|
||||
|
||||
export { Separator };
|
||||
@@ -0,0 +1,84 @@
|
||||
import React from "react";
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { useSidebar } from "@core/stores/sidebarStore.tsx";
|
||||
|
||||
export interface SidebarButtonProps {
|
||||
label: string;
|
||||
count?: number;
|
||||
active?: boolean;
|
||||
Icon?: LucideIcon;
|
||||
children?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
disabled?: boolean;
|
||||
preventCollapse?: boolean;
|
||||
isDirty?: boolean;
|
||||
}
|
||||
|
||||
export const SidebarButton = ({
|
||||
label,
|
||||
active,
|
||||
Icon,
|
||||
count,
|
||||
children,
|
||||
onClick,
|
||||
disabled = false,
|
||||
preventCollapse = false,
|
||||
isDirty,
|
||||
}: SidebarButtonProps) => {
|
||||
const { isCollapsed: isSidebarCollapsed } = useSidebar();
|
||||
const isButtonCollapsed = isSidebarCollapsed && !preventCollapse;
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={onClick}
|
||||
variant={active ? "subtle" : "ghost"}
|
||||
size="sm"
|
||||
className={cn(
|
||||
"flex w-full items-center text-wrap",
|
||||
isButtonCollapsed
|
||||
? "justify-center gap-0 px-2 h-9"
|
||||
: "justify-start gap-2 min-h-9",
|
||||
)}
|
||||
disabled={disabled}
|
||||
>
|
||||
{Icon && (
|
||||
<Icon
|
||||
size={isButtonCollapsed ? 20 : 18}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
)}
|
||||
|
||||
{children}
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
"flex flex-wrap justify-start text-left text-balance break-all",
|
||||
"min-w-0",
|
||||
"px-1",
|
||||
"transition-all duration-300 ease-in-out",
|
||||
isButtonCollapsed
|
||||
? "opacity-0 max-w-0 invisible w-0 overflow-hidden"
|
||||
: "opacity-100 max-w-full visible whitespace-normal",
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
|
||||
{!isButtonCollapsed && ((!active && count && count > 0) || isDirty) && (
|
||||
<div
|
||||
className={cn(
|
||||
"ml-auto flex-shrink-0 justify-end text-white text-xs rounded-full px-1.5 py-0.5",
|
||||
"flex-shrink-0",
|
||||
"transition-opacity duration-300 ease-in-out",
|
||||
isButtonCollapsed ? "opacity-0 invisible" : "opacity-100 visible",
|
||||
isDirty ? "bg-sky-500" : "bg-red-600",
|
||||
)}
|
||||
>
|
||||
{count}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import React from "react";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { Heading } from "@components/UI/Typography/Heading.tsx";
|
||||
import { useSidebar } from "@core/stores/sidebarStore.tsx";
|
||||
|
||||
interface SidebarSectionProps {
|
||||
label: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const SidebarSection = ({
|
||||
label,
|
||||
children,
|
||||
className,
|
||||
}: SidebarSectionProps) => {
|
||||
const { isCollapsed } = useSidebar();
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"py-2",
|
||||
isCollapsed ? "px-0" : "px-4",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<Heading
|
||||
as="h3"
|
||||
className={cn(
|
||||
"mb-2",
|
||||
"uppercase tracking-wider text-md",
|
||||
"transition-all duration-300 ease-in-out",
|
||||
"whitespace-nowrap overflow-hidden",
|
||||
isCollapsed
|
||||
? "opacity-0 max-w-0 h-0 invisible px-0 mb-0"
|
||||
: "opacity-100 max-w-xs h-auto visible px-1 mb-1",
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</Heading>
|
||||
|
||||
<div className="space-y-1">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useId, useState } from "react";
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
export interface SliderProps {
|
||||
value: number[];
|
||||
step?: number;
|
||||
min?: number;
|
||||
max: number;
|
||||
onValueChange?: (value: number[]) => void;
|
||||
onValueCommit?: (value: number[]) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
trackClassName?: string;
|
||||
rangeClassName?: string;
|
||||
thumbClassName?: string;
|
||||
}
|
||||
|
||||
export function Slider({
|
||||
value,
|
||||
step = 1,
|
||||
min = 0,
|
||||
max,
|
||||
onValueChange,
|
||||
onValueCommit,
|
||||
disabled = false,
|
||||
className,
|
||||
trackClassName,
|
||||
rangeClassName,
|
||||
thumbClassName,
|
||||
...props
|
||||
}: SliderProps) {
|
||||
const [internalValue, setInternalValue] = useState<number[]>(value);
|
||||
const isControlled = value !== undefined;
|
||||
const currentValue = isControlled ? value! : internalValue;
|
||||
const id = useId();
|
||||
|
||||
const handleValueChange = (newValue: number[]) => {
|
||||
if (!isControlled) setInternalValue(newValue);
|
||||
onValueChange?.(newValue);
|
||||
};
|
||||
|
||||
const handleValueCommit = (newValue: number[]) => {
|
||||
onValueCommit?.(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<SliderPrimitive.Root
|
||||
className={cn(
|
||||
"relative flex items-center select-none touch-none",
|
||||
className,
|
||||
)}
|
||||
value={currentValue}
|
||||
step={step}
|
||||
min={min}
|
||||
max={max}
|
||||
disabled={disabled}
|
||||
onValueChange={handleValueChange}
|
||||
onValueCommit={handleValueCommit}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track
|
||||
className={cn(
|
||||
"relative h-2 flex-1 rounded-full bg-slate-200",
|
||||
trackClassName,
|
||||
)}
|
||||
>
|
||||
<SliderPrimitive.Range
|
||||
className={cn(
|
||||
"absolute h-full rounded-full bg-blue-500",
|
||||
rangeClassName,
|
||||
)}
|
||||
/>
|
||||
</SliderPrimitive.Track>
|
||||
{currentValue.map((_, i) => (
|
||||
<SliderPrimitive.Thumb
|
||||
key={`${id}-thumb-${i}`}
|
||||
className={cn(
|
||||
"block w-4 h-4 rounded-full bg-white border border-slate-400 shadow-md",
|
||||
thumbClassName,
|
||||
)}
|
||||
aria-label={`Thumb ${i + 1}`}
|
||||
/>
|
||||
))}
|
||||
</SliderPrimitive.Root>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import { cn } from "../../core/utils/cn.ts";
|
||||
|
||||
interface SpinnerProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
size?: "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
sm: "h-4 w-4",
|
||||
md: "h-8 w-8",
|
||||
lg: "h-12 w-12",
|
||||
};
|
||||
|
||||
export function Spinner({ className, size = "md", ...props }: SpinnerProps) {
|
||||
return (
|
||||
<div
|
||||
aria-label="Loading..."
|
||||
className={cn(
|
||||
"flex items-center justify-center fade-in-50 fade-out-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<svg
|
||||
className={cn("animate-spin-slow stroke-current", sizeClasses[size])}
|
||||
role="img"
|
||||
aria-label="Loading spinner"
|
||||
viewBox="0 0 256 256"
|
||||
>
|
||||
<line
|
||||
x1="128"
|
||||
y1="32"
|
||||
x2="128"
|
||||
y2="64"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="24"
|
||||
/>
|
||||
<line
|
||||
x1="195.9"
|
||||
y1="60.1"
|
||||
x2="173.3"
|
||||
y2="82.7"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="24"
|
||||
/>
|
||||
<line
|
||||
x1="224"
|
||||
y1="128"
|
||||
x2="192"
|
||||
y2="128"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="24"
|
||||
/>
|
||||
<line
|
||||
x1="195.9"
|
||||
y1="195.9"
|
||||
x2="173.3"
|
||||
y2="173.3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="24"
|
||||
/>
|
||||
<line
|
||||
x1="128"
|
||||
y1="224"
|
||||
x2="128"
|
||||
y2="192"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="24"
|
||||
/>
|
||||
<line
|
||||
x1="60.1"
|
||||
y1="195.9"
|
||||
x2="82.7"
|
||||
y2="173.3"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="24"
|
||||
/>
|
||||
<line
|
||||
x1="32"
|
||||
y1="128"
|
||||
x2="64"
|
||||
y2="128"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="24"
|
||||
/>
|
||||
<line
|
||||
x1="60.1"
|
||||
y1="60.1"
|
||||
x2="82.7"
|
||||
y2="82.7"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="24"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus:outline-hidden focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-slate-900 data-[state=unchecked]:bg-slate-200 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 dark:data-[state=checked]:bg-slate-600 dark:data-[state=unchecked]:bg-slate-300",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"pointer-events-none block h-5 w-5 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0",
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
));
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName;
|
||||
|
||||
export { Switch };
|
||||
@@ -0,0 +1,53 @@
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex flex-wrap items-center rounded-md p-1 mt-2 bg-slate-200 dark:bg-slate-100",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
className={cn(
|
||||
"inline-flex min-w-[100px] items-center justify-center rounded-[0.185rem] px-3 py-1.5 text-sm font-medium text-slate-900 transition-all disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-slate-900 data-[state=active]:shadow-xs cursor-pointer",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
className={cn(
|
||||
"mt-2 rounded-md border border-slate-200 p-6 dark:border-slate-700",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsContent, TabsList, TabsTrigger };
|
||||
@@ -0,0 +1,128 @@
|
||||
import * as ToastPrimitives from "@radix-ui/react-toast";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { X } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "../../core/utils/cn.ts";
|
||||
|
||||
const ToastProvider = ToastPrimitives.Provider;
|
||||
|
||||
const ToastViewport = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Viewport>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Viewport
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed top-0 z-100 flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-24 sm:right-6 sm:top-auto sm:flex-col md:max-w-[420px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
|
||||
|
||||
const toastVariants = cva(
|
||||
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full sm:data-[state=open]:slide-in-from-bottom-full",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border bg-white text-slate-900 dark:text-slate-100 dark:bg-slate-800 dark:border-slate-700",
|
||||
destructive:
|
||||
"group destructive bg-red-600 text-white dark:border-red-900 dark:bg-red-900 dark:text-red-50",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const Toast = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Root>,
|
||||
& React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root>
|
||||
& VariantProps<typeof toastVariants>
|
||||
>(({ className, variant, ...props }, ref) => {
|
||||
return (
|
||||
<ToastPrimitives.Root
|
||||
ref={ref}
|
||||
className={cn(toastVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
Toast.displayName = ToastPrimitives.Root.displayName;
|
||||
|
||||
const ToastAction = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Action
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 hover:group-[.destructive]:border-destructive/30 hover:group-[.destructive]:bg-destructive hover:group-[.destructive]:text-destructive-foreground focus:group-[.destructive]:ring-destructive",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastAction.displayName = ToastPrimitives.Action.displayName;
|
||||
|
||||
const ToastClose = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Close>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Close
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-hidden focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 hover:group-[.destructive]:text-red-50 focus:group-[.destructive]:ring-red-400 focus:group-[.destructive]:ring-offset-red-600 text-black",
|
||||
className,
|
||||
)}
|
||||
toast-close=""
|
||||
{...props}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</ToastPrimitives.Close>
|
||||
));
|
||||
ToastClose.displayName = ToastPrimitives.Close.displayName;
|
||||
|
||||
const ToastTitle = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Title
|
||||
ref={ref}
|
||||
className={cn("text-sm font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastTitle.displayName = ToastPrimitives.Title.displayName;
|
||||
|
||||
const ToastDescription = React.forwardRef<
|
||||
React.ElementRef<typeof ToastPrimitives.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToastPrimitives.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm opacity-90", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToastDescription.displayName = ToastPrimitives.Description.displayName;
|
||||
|
||||
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
|
||||
|
||||
type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
||||
|
||||
export {
|
||||
Toast,
|
||||
ToastAction,
|
||||
type ToastActionElement,
|
||||
ToastClose,
|
||||
ToastDescription,
|
||||
type ToastProps,
|
||||
ToastProvider,
|
||||
ToastTitle,
|
||||
ToastViewport,
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
|
||||
import * as React from "react";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const toggleGroupItemClasses = [
|
||||
"flex flex-1 h-10 items-center justify-center first:rounded-l last:rounded-r ",
|
||||
"bg-slate-100",
|
||||
"dark:bg-slate-800",
|
||||
"data-[state=on]:bg-slate-600 data-[state=on]:text-white",
|
||||
"data-[state=on]:dark:bg-slate-950 data-[state=on]:text-white data-[state=on]:dark:text-slate-200",
|
||||
"data-[state=on]:hover:bg-slate-700 hover:bg-slate-700 hover:text-white hover:z-10 hover:shadow-[0_0_1px_2px] hover:outline-1 hover:outline-slate-700 hover:shadow-white/10",
|
||||
"data-[state=on]:dark:hover:bg-slate-700 dark:hover:bg-slate-700 dark:hover:text-slate-200 dark:hover:outline-slate-700 dark:hover:shadow-black/20",
|
||||
"disabled:text-slate-300 disabled:hover:bg-slate-100 disabled:hover:outline-none hover:shadow-none disabled:dark:text-slate-600 disabled:dark:hover:bg-slate-800 disabled:dark:hover:outline-none disabled:shadow-none",
|
||||
];
|
||||
|
||||
const ToggleGroup = React.forwardRef<
|
||||
React.ComponentRef<typeof ToggleGroupPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ToggleGroupPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex rounded shadow-md space-x-[1px] bg-slate-300 dark:bg-slate-800",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
|
||||
|
||||
const ToggleGroupItem = React.forwardRef<
|
||||
React.ComponentRef<typeof ToggleGroupPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ToggleGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
...toggleGroupItemClasses,
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</ToggleGroupPrimitive.Item>
|
||||
));
|
||||
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
|
||||
|
||||
export { ToggleGroup, ToggleGroupItem };
|
||||
@@ -0,0 +1,36 @@
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = ({ ...props }) => <TooltipPrimitive.Root {...props} />;
|
||||
Tooltip.displayName = TooltipPrimitive.Tooltip.displayName;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
const TooltipArrow = TooltipPrimitive.Arrow;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border border-slate-100 bg-white px-3 py-1.5 text-sm text-slate-700 shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1 dark:border-slate-800 dark:bg-slate-800 dark:text-slate-200",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export {
|
||||
Tooltip,
|
||||
TooltipArrow,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface BlockquoteProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const BlockQuote = ({ children }: BlockquoteProps) => (
|
||||
<blockquote className="mt-6 border-l-2 border-slate-300 pl-6 italic text-slate-800 dark:border-slate-600 dark:text-slate-200">
|
||||
{children}
|
||||
</blockquote>
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
export interface CodeProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Code = ({ children }: CodeProps) => (
|
||||
<code className="relative rounded-sm bg-slate-100 px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold text-slate-900 dark:bg-slate-800 dark:text-slate-400">
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
@@ -0,0 +1,31 @@
|
||||
import type React from "react";
|
||||
|
||||
const headingStyles = {
|
||||
h1: "scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl",
|
||||
h2:
|
||||
"scroll-m-20 border-b border-b-slate-200 pb-2 text-3xl font-semibold tracking-tight transition-colors first:mt-0 dark:border-b-slate-700",
|
||||
h3: "scroll-m-20 text-lg font-semibold tracking-tight",
|
||||
h4: "scroll-m-20 text-xl font-semibold tracking-tight",
|
||||
h5: "scroll-m-20 text-lg font-medium tracking-tight",
|
||||
};
|
||||
|
||||
interface HeadingProps {
|
||||
as?: "h1" | "h2" | "h3" | "h4" | "h5";
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Heading = ({
|
||||
as: Component = "h1",
|
||||
children,
|
||||
className = "",
|
||||
...props
|
||||
}: HeadingProps) => {
|
||||
const baseStyles = headingStyles[Component] || headingStyles.h1;
|
||||
|
||||
return (
|
||||
<Component className={`${baseStyles} ${className}`} {...props}>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import {
|
||||
Link as RouterLink,
|
||||
LinkProps as RouterLinkProps,
|
||||
} from "@tanstack/react-router";
|
||||
|
||||
export interface LinkProps extends RouterLinkProps {
|
||||
href: string;
|
||||
children?:
|
||||
| React.ReactNode
|
||||
| ((
|
||||
state: { isActive: boolean; isTransitioning: boolean },
|
||||
) => React.ReactNode);
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const Link = ({ href, children, className }: LinkProps) => (
|
||||
<RouterLink
|
||||
to={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
"font-medium text-slate-900 underline underline-offset-4 dark:text-slate-200",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</RouterLink>
|
||||
);
|
||||
@@ -0,0 +1,10 @@
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
export interface PProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const P = ({ children, className }: PProps) => (
|
||||
<p className={cn("leading-7 not-first:mt-6", className)}>{children}</p>
|
||||
);
|
||||
@@ -0,0 +1,12 @@
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
|
||||
export interface SubtleProps {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Subtle = ({ className, children }: SubtleProps) => (
|
||||
<p className={cn("text-sm text-slate-500 dark:text-slate-400", className)}>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
Reference in New Issue
Block a user