mirror of
https://github.com/ReaJason/MemShellParty.git
synced 2026-09-21 22:50:42 +08:00
feat: add more page
This commit is contained in:
+15
-8
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.5/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
@@ -13,7 +13,17 @@
|
||||
"noExplicitAny": "off"
|
||||
},
|
||||
"style": {
|
||||
"useImportType": "off"
|
||||
"useImportType": "off",
|
||||
"noParameterAssign": "error",
|
||||
"useAsConstAssertion": "error",
|
||||
"useDefaultParameterLast": "error",
|
||||
"useEnumInitializers": "error",
|
||||
"useSelfClosingElements": "error",
|
||||
"useSingleVarDeclarator": "error",
|
||||
"noUnusedTemplateLiteral": "error",
|
||||
"useNumberNamespace": "error",
|
||||
"noInferrableTypes": "error",
|
||||
"noUselessElse": "error"
|
||||
},
|
||||
"recommended": true,
|
||||
"a11y": {
|
||||
@@ -24,19 +34,16 @@
|
||||
"noUnusedImports": "warn"
|
||||
}
|
||||
},
|
||||
"ignore": ["node_modules", "tsconfig*", "dist", "components/ui/**"]
|
||||
"includes": ["**", "!**/node_modules", "!**/tsconfig*", "!**/dist", "!**/components/ui/**"]
|
||||
},
|
||||
"formatter": {
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2,
|
||||
"enabled": true,
|
||||
"lineWidth": 120,
|
||||
"ignore": ["node_modules", "tsconfig*", "dist"]
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true,
|
||||
"ignore": ["node_modules", "dist", "components/ui/**"]
|
||||
"includes": ["**", "!**/node_modules", "!**/tsconfig*", "!**/dist"]
|
||||
},
|
||||
"assist": { "actions": { "source": { "organizeImports": "on" } } },
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double"
|
||||
|
||||
Binary file not shown.
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { cp } from "node:fs/promises";
|
||||
import { join, resolve } from "node:path";
|
||||
|
||||
|
||||
+20
-41
@@ -14,67 +14,46 @@
|
||||
"lint:fix": "bunx @biomejs/biome check ./ --write"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/react": "^19.1.6",
|
||||
"@biomejs/biome": "2.0.5",
|
||||
"@types/node": "^24.0.4",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-copy-to-clipboard": "^5.0.7",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@vitejs/plugin-react": "^4.5.1",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"rimraf": "^6.0.1",
|
||||
"tailwindcss": "^4.1.8",
|
||||
"tailwindcss": "^4.1.10",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vite": "^7.0.0",
|
||||
"vite-bundle-visualizer": "^1.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.0.1",
|
||||
"@radix-ui/react-accordion": "^1.2.11",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.14",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.7",
|
||||
"@radix-ui/react-avatar": "^1.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.2",
|
||||
"@radix-ui/react-context-menu": "^2.2.15",
|
||||
"@radix-ui/react-dialog": "^1.1.14",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.15",
|
||||
"@radix-ui/react-hover-card": "^1.1.14",
|
||||
"@radix-ui/react-label": "^2.1.7",
|
||||
"@radix-ui/react-menubar": "^1.1.15",
|
||||
"@hookform/resolvers": "^5.1.1",
|
||||
"@radix-ui/react-navigation-menu": "^1.2.13",
|
||||
"@radix-ui/react-popover": "^1.1.14",
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@radix-ui/react-radio-group": "^1.3.7",
|
||||
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||
"@radix-ui/react-select": "^2.2.5",
|
||||
"@radix-ui/react-separator": "^1.1.7",
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"@radix-ui/react-toggle": "^1.1.9",
|
||||
"@radix-ui/react-toggle-group": "^1.1.10",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@tailwindcss/vite": "^4.1.8",
|
||||
"@tanstack/react-query": "^5.80.3",
|
||||
"@tailwindcss/vite": "^4.1.10",
|
||||
"@tanstack/react-query": "^5.81.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"framer-motion": "^12.19.1",
|
||||
"i18next": "^25.2.1",
|
||||
"lucide-react": "^0.513.0",
|
||||
"lucide-react": "^0.523.0",
|
||||
"motion": "^12.19.1",
|
||||
"radix-ui": "^1.4.2",
|
||||
"react": "^19.1.0",
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"react-day-picker": "9.7.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hook-form": "^7.57.0",
|
||||
"react-i18next": "^15.5.2",
|
||||
"react-hook-form": "^7.58.1",
|
||||
"react-i18next": "^15.5.3",
|
||||
"react-router": "^7.6.2",
|
||||
"react-router-dom": "^7.6.2",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"sonner": "^2.0.5",
|
||||
"tailwind-merge": "^3.3.0",
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tw-animate-css": "^1.3.4",
|
||||
"yup": "^1.6.1",
|
||||
"zod": "^3.25.51"
|
||||
"yup": "^1.6.1"
|
||||
},
|
||||
"trustedDependencies": ["@biomejs/biome"]
|
||||
"trustedDependencies": [
|
||||
"@biomejs/biome"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { VariantProps } from "class-variance-authority";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
import { type HTMLProps, type ReactNode, useCallback, useEffect, useState } from "react";
|
||||
@@ -8,6 +6,8 @@ import { useTranslation } from "react-i18next";
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||
import { materialDark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||
import { toast } from "sonner";
|
||||
import { Button, buttonVariants } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface CopyButtonProps extends React.ComponentProps<"button"> {
|
||||
value: string;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { CopyToClipboard } from "react-copy-to-clipboard";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
interface CopyableFieldProps {
|
||||
label: string;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { NavLink } from "react-router";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { LanguageSwitcher } from "@/components/language-switcher";
|
||||
import { ModeToggle } from "@/components/mode-toggle.tsx";
|
||||
import { ThemeProvider } from "@/components/theme-provider.tsx";
|
||||
@@ -5,7 +7,8 @@ import { Button } from "@/components/ui/button";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import VersionBadge from "@/components/version-badge";
|
||||
import { GitHubIcon } from "@/icon";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { siteConfig } from "@/lib/config";
|
||||
import { MobileNav } from "../modile-nav";
|
||||
|
||||
export default function RootLayout() {
|
||||
return (
|
||||
@@ -13,11 +16,21 @@ export default function RootLayout() {
|
||||
<Toaster />
|
||||
<div className="flex flex-col h-screen">
|
||||
<header className="sticky top-0 z-50 border-b bg-background px-2 sm:px-4 py-2 sm:py-3">
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-2 sm:gap-0">
|
||||
<div className="flex items-center">
|
||||
<h2 className="text-base sm:text-lg font-semibold text-center">MemShellParty - JavaWeb</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<MobileNav className="flex lg:hidden" items={siteConfig.navItems} />
|
||||
<div className="hidden lg:flex">
|
||||
<NavLink to="/" className="text-base sm:text-lg font-semibold text-center">
|
||||
MemShellParty
|
||||
</NavLink>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5 sm:gap-1">
|
||||
<nav className="items-center gap-0.5 hidden lg:flex">
|
||||
{siteConfig.navItems.map((item) => (
|
||||
<Button key={item.href} variant="ghost" size="sm" asChild>
|
||||
<NavLink to={item.href}>{item.label}</NavLink>
|
||||
</Button>
|
||||
))}
|
||||
</nav>
|
||||
<div className="ml-auto flex items-center gap-2 md:flex-1 md:justify-end">
|
||||
<VersionBadge />
|
||||
<LanguageSwitcher />
|
||||
<Button
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { MotionProps, motion } from "motion/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface LineShadowTextProps extends Omit<React.HTMLAttributes<HTMLElement>, keyof MotionProps>, MotionProps {
|
||||
shadowColor?: string;
|
||||
as?: React.ElementType;
|
||||
}
|
||||
|
||||
export function LineShadowText({
|
||||
children,
|
||||
shadowColor = "black",
|
||||
className,
|
||||
as: Component = "span",
|
||||
...props
|
||||
}: LineShadowTextProps) {
|
||||
const MotionComponent = motion.create(Component);
|
||||
const content = typeof children === "string" ? children : null;
|
||||
|
||||
if (!content) {
|
||||
throw new Error("LineShadowText only accepts string content");
|
||||
}
|
||||
|
||||
return (
|
||||
<MotionComponent
|
||||
style={{ "--shadow-color": shadowColor } as React.CSSProperties}
|
||||
className={cn(
|
||||
"relative z-0 inline-flex",
|
||||
"after:absolute after:left-[0.04em] after:top-[0.04em] after:content-[attr(data-text)]",
|
||||
"after:bg-[linear-gradient(45deg,transparent_45%,var(--shadow-color)_45%,var(--shadow-color)_55%,transparent_0)]",
|
||||
"after:-z-10 after:bg-[length:0.06em_0.06em] after:bg-clip-text after:text-transparent",
|
||||
"after:animate-line-shadow",
|
||||
className,
|
||||
)}
|
||||
data-text={content}
|
||||
{...props}
|
||||
>
|
||||
{content}
|
||||
</MotionComponent>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, VariantProps } from "class-variance-authority";
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const rainbowButtonVariants = cva(
|
||||
cn(
|
||||
"relative cursor-pointer group transition-all animate-rainbow",
|
||||
"inline-flex items-center justify-center gap-2 shrink-0",
|
||||
"rounded-sm outline-none focus-visible:ring-[3px] aria-invalid:border-destructive",
|
||||
"text-sm font-medium whitespace-nowrap",
|
||||
"disabled:pointer-events-none disabled:opacity-50",
|
||||
"[&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0",
|
||||
),
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-0 bg-[linear-gradient(#121213,#121213),linear-gradient(#121213_50%,rgba(18,18,19,0.6)_80%,rgba(18,18,19,0)),linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))] bg-[length:200%] text-primary-foreground [background-clip:padding-box,border-box,border-box] [background-origin:border-box] [border:calc(0.125rem)_solid_transparent] before:absolute before:bottom-[-20%] before:left-1/2 before:z-0 before:h-1/5 before:w-3/5 before:-translate-x-1/2 before:animate-rainbow before:bg-[linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))] before:[filter:blur(0.75rem)] dark:bg-[linear-gradient(#fff,#fff),linear-gradient(#fff_50%,rgba(255,255,255,0.6)_80%,rgba(0,0,0,0)),linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))]",
|
||||
outline:
|
||||
"border border-input border-b-transparent bg-[linear-gradient(#ffffff,#ffffff),linear-gradient(#ffffff_50%,rgba(18,18,19,0.6)_80%,rgba(18,18,19,0)),linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))] bg-[length:200%] text-accent-foreground [background-clip:padding-box,border-box,border-box] [background-origin:border-box] before:absolute before:bottom-[-20%] before:left-1/2 before:z-0 before:h-1/5 before:w-3/5 before:-translate-x-1/2 before:animate-rainbow before:bg-[linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))] before:[filter:blur(0.75rem)] dark:bg-[linear-gradient(#0a0a0a,#0a0a0a),linear-gradient(#0a0a0a_50%,rgba(255,255,255,0.6)_80%,rgba(0,0,0,0)),linear-gradient(90deg,var(--color-1),var(--color-5),var(--color-3),var(--color-4),var(--color-2))]",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2",
|
||||
sm: "h-8 rounded-xl px-3 text-xs",
|
||||
lg: "h-11 rounded-xl px-8",
|
||||
icon: "size-9",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface RainbowButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof rainbowButtonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const RainbowButton = React.forwardRef<HTMLButtonElement, RainbowButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(rainbowButtonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
RainbowButton.displayName = "RainbowButton";
|
||||
|
||||
export { RainbowButton, rainbowButtonVariants, type RainbowButtonProps };
|
||||
@@ -1,3 +1,19 @@
|
||||
import {
|
||||
ArrowUpRightIcon,
|
||||
AxeIcon,
|
||||
CommandIcon,
|
||||
NetworkIcon,
|
||||
ServerIcon,
|
||||
ShieldOffIcon,
|
||||
SwordIcon,
|
||||
WaypointsIcon,
|
||||
ZapIcon,
|
||||
} from "lucide-react";
|
||||
import { JSX, useCallback, useEffect, useId, useState } from "react";
|
||||
import { FormProvider, UseFormReturn } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { JreTip } from "@/components/tips/jre-tip.tsx";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card.tsx";
|
||||
import {
|
||||
FormControl,
|
||||
FormDescription,
|
||||
@@ -13,23 +29,6 @@ import { Switch } from "@/components/ui/switch.tsx";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { FormSchema } from "@/types/schema.ts";
|
||||
import { JDKVersion, MainConfig, ServerConfig, ShellToolType } from "@/types/shell.ts";
|
||||
|
||||
import { JreTip } from "@/components/tips/jre-tip.tsx";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card.tsx";
|
||||
import {
|
||||
ArrowUpRightIcon,
|
||||
AxeIcon,
|
||||
CommandIcon,
|
||||
NetworkIcon,
|
||||
ServerIcon,
|
||||
ShieldOffIcon,
|
||||
SwordIcon,
|
||||
WaypointsIcon,
|
||||
ZapIcon,
|
||||
} from "lucide-react";
|
||||
import { JSX, useEffect, useState } from "react";
|
||||
import { FormProvider, UseFormReturn } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { AntSwordTabContent } from "./tools/antsword-tab";
|
||||
import { BehinderTabContent } from "./tools/behinder-tab";
|
||||
import { CommandTabContent } from "./tools/command-tab";
|
||||
@@ -73,16 +72,6 @@ export function MainConfigCard({
|
||||
const shellTool = form.watch("shellTool");
|
||||
const { t } = useTranslation();
|
||||
|
||||
// 处理一下默认值 server 不刷新 shellType 的问题
|
||||
useEffect(() => {
|
||||
if (mainConfig) {
|
||||
const initialServer = form.getValues("server");
|
||||
if (initialServer && mainConfig[initialServer]) {
|
||||
handleServerChange(initialServer);
|
||||
}
|
||||
}
|
||||
}, [mainConfig, form]);
|
||||
|
||||
// 处理一下 shellTypes 由于 server 或 shellTool 变更时无法正常为 form.shellType 赋值的问题
|
||||
useEffect(() => {
|
||||
if (shellTypes.length > 0) {
|
||||
@@ -90,7 +79,8 @@ export function MainConfigCard({
|
||||
}
|
||||
}, [shellTypes, form]);
|
||||
|
||||
const handleServerChange = (value: string) => {
|
||||
const handleServerChange = useCallback(
|
||||
(value: string) => {
|
||||
if (mainConfig) {
|
||||
const newShellToolMap = mainConfig[value];
|
||||
setShellToolMap(newShellToolMap);
|
||||
@@ -123,9 +113,22 @@ export function MainConfigCard({
|
||||
form.resetField("bypassJavaModule");
|
||||
form.resetField("urlPattern");
|
||||
}
|
||||
};
|
||||
},
|
||||
[form, mainConfig],
|
||||
);
|
||||
|
||||
const handleShellToolChange = (value: string) => {
|
||||
// 处理一下默认值 server 不刷新 shellType 的问题
|
||||
useEffect(() => {
|
||||
if (mainConfig) {
|
||||
const initialServer = form.getValues("server");
|
||||
if (initialServer && mainConfig[initialServer]) {
|
||||
handleServerChange(initialServer);
|
||||
}
|
||||
}
|
||||
}, [mainConfig, form, handleServerChange]);
|
||||
|
||||
const handleShellToolChange = useCallback(
|
||||
(value: string) => {
|
||||
const resetCommand = () => {
|
||||
form.resetField("commandParamName");
|
||||
form.resetField("implementationClass");
|
||||
@@ -194,7 +197,13 @@ export function MainConfigCard({
|
||||
}
|
||||
}
|
||||
form.setValue("shellTool", value);
|
||||
};
|
||||
},
|
||||
[form, servers, shellToolMap],
|
||||
);
|
||||
|
||||
const debugId = useId();
|
||||
const bypassId = useId();
|
||||
const shrinkId = useId();
|
||||
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
@@ -291,9 +300,9 @@ export function MainConfigCard({
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch id="debug" checked={field.value} onCheckedChange={field.onChange} />
|
||||
<Switch id={debugId} checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
<FormLabel htmlFor="debug">{t("mainConfig.debug")}</FormLabel>
|
||||
<FormLabel htmlFor={debugId}>{t("mainConfig.debug")}</FormLabel>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -303,9 +312,9 @@ export function MainConfigCard({
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch id="bypassJavaModule" checked={field.value} onCheckedChange={field.onChange} />
|
||||
<Switch id={bypassId} checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
<Label htmlFor="bypassJavaModule">{t("mainConfig.bypassJavaModule")}</Label>
|
||||
<Label htmlFor={bypassId}>{t("mainConfig.bypassJavaModule")}</Label>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
@@ -315,9 +324,9 @@ export function MainConfigCard({
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex items-center space-x-2 space-y-0">
|
||||
<FormControl>
|
||||
<Switch id="shrink" checked={field.value} onCheckedChange={field.onChange} />
|
||||
<Switch id={shrinkId} checked={field.value} onCheckedChange={field.onChange} />
|
||||
</FormControl>
|
||||
<Label htmlFor="shrink">{t("mainConfig.shrink")}</Label>
|
||||
<Label htmlFor={shrinkId}>{t("mainConfig.shrink")}</Label>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import React from "react";
|
||||
import { NavLink } from "react-router";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function MobileNav({
|
||||
items,
|
||||
className,
|
||||
}: Readonly<{
|
||||
items: { href: string; label: string }[];
|
||||
className?: string;
|
||||
}>) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"extend-touch-target h-8 touch-manipulation items-center justify-start gap-2.5 !p-0 hover:bg-transparent focus-visible:bg-transparent focus-visible:ring-0 active:bg-transparent dark:hover:bg-transparent",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="relative flex h-8 w-4 items-center justify-center">
|
||||
<div className="relative size-4">
|
||||
<span
|
||||
className={cn(
|
||||
"bg-foreground absolute left-0 block h-0.5 w-4 transition-all duration-100",
|
||||
open ? "top-[0.4rem] -rotate-45" : "top-1",
|
||||
)}
|
||||
/>
|
||||
<span
|
||||
className={cn(
|
||||
"bg-foreground absolute left-0 block h-0.5 w-4 transition-all duration-100",
|
||||
open ? "top-[0.4rem] rotate-45" : "top-2.5",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<span className="sr-only">Toggle Menu</span>
|
||||
</div>
|
||||
<span className="flex h-8 items-center text-lg leading-none font-medium">Menu</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="bg-background/90 no-scrollbar h-(--radix-popper-available-height) w-(--radix-popper-available-width) overflow-y-auto rounded-none border-none p-0 shadow-none backdrop-blur duration-100"
|
||||
align="start"
|
||||
side="bottom"
|
||||
alignOffset={-16}
|
||||
sideOffset={14}
|
||||
>
|
||||
<div className="flex flex-col gap-12 overflow-auto px-6 py-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="text-muted-foreground text-sm font-medium">Menu</div>
|
||||
<div className="flex flex-col gap-3">
|
||||
<MobileLink href="/" onOpenChange={setOpen}>
|
||||
Home
|
||||
</MobileLink>
|
||||
{items.map((item) => (
|
||||
<MobileLink key={item.href} href={item.href} onOpenChange={setOpen}>
|
||||
{item.label}
|
||||
</MobileLink>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileLink({
|
||||
href,
|
||||
className,
|
||||
children,
|
||||
onOpenChange,
|
||||
...props
|
||||
}: Readonly<{
|
||||
href: string;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}>) {
|
||||
return (
|
||||
<NavLink
|
||||
to={href}
|
||||
onClick={() => {
|
||||
onOpenChange?.(false);
|
||||
}}
|
||||
className={cn("text-2xl font-medium", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
import { PackageIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormProvider, UseFormReturn } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card.tsx";
|
||||
import { FormControl, FormField, FormItem, FormLabel } from "@/components/ui/form.tsx";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group.tsx";
|
||||
import { FormSchema } from "@/types/schema.ts";
|
||||
import { PackerConfig } from "@/types/shell.ts";
|
||||
import { PackageIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FormProvider, UseFormReturn } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type Option = {
|
||||
name: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card.tsx";
|
||||
import { ScrollTextIcon } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card.tsx";
|
||||
|
||||
export function QuickUsage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { downloadBytes, formatBytes } from "@/lib/utils";
|
||||
import { GenerateResult } from "@/types/shell";
|
||||
import { ScrollTextIcon } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { downloadBytes, formatBytes } from "@/lib/utils";
|
||||
import { GenerateResult } from "@/types/shell";
|
||||
import { Button } from "../ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
|
||||
import { Separator } from "../ui/separator";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { FileTextIcon } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { shouldHidden } from "@/lib/utils";
|
||||
import {
|
||||
AntSwordShellToolConfig,
|
||||
@@ -9,8 +11,6 @@ import {
|
||||
ShellToolType,
|
||||
Suo5ShellToolConfig,
|
||||
} from "@/types/shell";
|
||||
import { FileTextIcon } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CopyableField } from "../copyable-field";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
|
||||
import { Separator } from "../ui/separator";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { downloadBytes } from "@/lib/utils";
|
||||
import { GenerateResult } from "@/types/shell";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
export function JarResult({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { GenerateResult } from "@/types/shell";
|
||||
import { TFunction } from "i18next";
|
||||
import { GenerateResult } from "@/types/shell";
|
||||
import { CodeViewer } from "../code-viewer";
|
||||
import { AgentResult } from "./agent";
|
||||
import { JarResult } from "./jar-result";
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { DownloadIcon } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { CodeViewer } from "@/components/code-viewer.tsx";
|
||||
import { QuickUsage } from "@/components/quick-usage.tsx";
|
||||
import { Button } from "@/components/ui/button.tsx";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs.tsx";
|
||||
import { downloadBytes } from "@/lib/utils.ts";
|
||||
import { GenerateResult } from "@/types/shell.ts";
|
||||
import { DownloadIcon } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { BasicInfo } from "./results/basic-info";
|
||||
import { ResultComponent } from "./results/result-component";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip.tsx";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip.tsx";
|
||||
|
||||
export function JreTip() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { FormSchema } from "@/types/schema";
|
||||
import { FormProvider, UseFormReturn } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FormSchema } from "@/types/schema";
|
||||
import { Card, CardContent } from "../ui/card";
|
||||
import { FormField, FormFieldItem, FormFieldLabel } from "../ui/form";
|
||||
import { FormControl, FormField, FormFieldItem, FormFieldLabel } from "../ui/form";
|
||||
import { Input } from "../ui/input";
|
||||
import { TabsContent } from "../ui/tabs";
|
||||
import { OptionalClassFormField } from "./classname-field";
|
||||
@@ -42,7 +42,9 @@ export function AntSwordTabContent({
|
||||
render={({ field }) => (
|
||||
<FormFieldItem>
|
||||
<FormFieldLabel>{t("shellToolConfig.headerName")}</FormFieldLabel>
|
||||
<FormControl>
|
||||
<Input {...field} placeholder={t("placeholders.input")} />
|
||||
</FormControl>
|
||||
</FormFieldItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FormSchema } from "@/types/schema";
|
||||
import { FormProvider, UseFormReturn } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FormSchema } from "@/types/schema";
|
||||
import { Card, CardContent } from "../ui/card";
|
||||
import { FormField, FormFieldItem, FormFieldLabel } from "../ui/form";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import { ChevronDown, ChevronUp, Settings } from "lucide-react";
|
||||
import { Fragment, useId, useState } from "react";
|
||||
import { FormProvider, UseFormReturn } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FormField, FormFieldItem, FormFieldLabel } from "@/components/ui/form.tsx";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import { FormSchema } from "@/types/schema.ts";
|
||||
import { ChevronDown, ChevronUp, Settings } from "lucide-react";
|
||||
import { Fragment, useState } from "react";
|
||||
import { FormProvider, UseFormReturn } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
export function OptionalClassFormField({ form }: Readonly<{ form: UseFormReturn<FormSchema> }>) {
|
||||
const { t } = useTranslation();
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const shellClassNameId = useId();
|
||||
const injectClassNameId = useId();
|
||||
return (
|
||||
<Fragment>
|
||||
<div className="pt-2">
|
||||
@@ -32,10 +34,10 @@ export function OptionalClassFormField({ form }: Readonly<{ form: UseFormReturn<
|
||||
name="shellClassName"
|
||||
render={({ field }) => (
|
||||
<FormFieldItem>
|
||||
<FormFieldLabel>
|
||||
<FormFieldLabel htmlFor={shellClassNameId}>
|
||||
{t("mainConfig.shellClassName")} {t("optional")}
|
||||
</FormFieldLabel>
|
||||
<Input id="shellClassName" {...field} placeholder={t("placeholders.input")} />
|
||||
<Input id={shellClassNameId} {...field} placeholder={t("placeholders.input")} />
|
||||
</FormFieldItem>
|
||||
)}
|
||||
/>
|
||||
@@ -44,10 +46,10 @@ export function OptionalClassFormField({ form }: Readonly<{ form: UseFormReturn<
|
||||
name="injectorClassName"
|
||||
render={({ field }) => (
|
||||
<FormFieldItem>
|
||||
<FormFieldLabel>
|
||||
<FormFieldLabel htmlFor={injectClassNameId}>
|
||||
{t("mainConfig.injectorClassName")} {t("optional")}
|
||||
</FormFieldLabel>
|
||||
<Input id="injectorClassName" {...field} placeholder={t("placeholders.input")} />
|
||||
<Input id={injectClassNameId} {...field} placeholder={t("placeholders.input")} />
|
||||
</FormFieldItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { env } from "@/config";
|
||||
import { FormSchema } from "@/types/schema";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { FormProvider, UseFormReturn } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { env } from "@/config";
|
||||
import { FormSchema } from "@/types/schema";
|
||||
import { Card, CardContent } from "../ui/card";
|
||||
import { FormControl, FormField, FormFieldItem, FormFieldLabel } from "../ui/form";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { t } from "i18next";
|
||||
import { useId, useState } from "react";
|
||||
import { FormProvider, UseFormReturn } from "react-hook-form";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { TabsContent } from "@/components/ui/tabs";
|
||||
import { FormSchema } from "@/types/schema";
|
||||
import { t } from "i18next";
|
||||
import { useState } from "react";
|
||||
import { FormProvider, UseFormReturn } from "react-hook-form";
|
||||
import { FormControl, FormField, FormFieldItem, FormFieldLabel, FormMessage } from "../ui/form";
|
||||
import { Input } from "../ui/input";
|
||||
import { Label } from "../ui/label";
|
||||
@@ -18,7 +18,8 @@ export default function CustomTabContent({
|
||||
shellTypes,
|
||||
}: Readonly<{ form: UseFormReturn<FormSchema>; shellTypes: Array<string> }>) {
|
||||
const [isFile, setIsFile] = useState(false);
|
||||
|
||||
const optionOneId = useId();
|
||||
const optionTwoId = useId();
|
||||
return (
|
||||
<FormProvider {...form}>
|
||||
<TabsContent value="Custom">
|
||||
@@ -43,12 +44,12 @@ export default function CustomTabContent({
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="base64" id="option-one" />
|
||||
<Label htmlFor="option-one">Base64</Label>
|
||||
<RadioGroupItem value="base64" id={optionOneId} />
|
||||
<Label htmlFor={optionOneId}>Base64</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="file" id="option-two" />
|
||||
<Label htmlFor="option-two">File</Label>
|
||||
<RadioGroupItem value="file" id={optionTwoId} />
|
||||
<Label htmlFor={optionTwoId}>File</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<FormControl className="mt-2">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FormSchema } from "@/types/schema";
|
||||
import { FormProvider, UseFormReturn } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FormSchema } from "@/types/schema";
|
||||
import { Card, CardContent } from "../ui/card";
|
||||
import { FormField, FormFieldItem, FormFieldLabel } from "../ui/form";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FormSchema } from "@/types/schema";
|
||||
import { FormProvider, UseFormReturn } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FormSchema } from "@/types/schema";
|
||||
import { Card, CardContent } from "../ui/card";
|
||||
import { FormField, FormFieldItem, FormFieldLabel } from "../ui/form";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { FormProvider, UseFormReturn } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FormControl, FormField, FormFieldItem, FormFieldLabel } from "@/components/ui/form.tsx";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select.tsx";
|
||||
import { FormSchema } from "@/types/schema.ts";
|
||||
|
||||
import { FormProvider, UseFormReturn } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function ShellTypeFormField({
|
||||
form,
|
||||
shellTypes,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FormSchema } from "@/types/schema";
|
||||
import { FormProvider, UseFormReturn } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FormSchema } from "@/types/schema";
|
||||
import { Card, CardContent } from "../ui/card";
|
||||
import { FormField, FormFieldItem, FormFieldLabel } from "../ui/form";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { FormProvider, UseFormReturn } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FormField, FormFieldItem, FormFieldLabel, FormMessage } from "@/components/ui/form.tsx";
|
||||
import { Input } from "@/components/ui/input.tsx";
|
||||
import { shouldHidden } from "@/lib/utils";
|
||||
import { FormSchema } from "@/types/schema.ts";
|
||||
import { FormProvider, UseFormReturn } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export function UrlPatternFormField({ form }: Readonly<{ form: UseFormReturn<FormSchema> }>) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { AlertDialog as AlertDialogPrimitive } from "radix-ui";
|
||||
import * as React from "react";
|
||||
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function AlertDialog({ ...props }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { Slot as SlotPrimitive } from "radix-ui";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary: "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline: "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> & VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? SlotPrimitive.Slot : "span";
|
||||
|
||||
return <Comp data-slot="badge" className={cn(badgeVariants({ variant }), className)} {...props} />;
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
@@ -0,0 +1,92 @@
|
||||
import { ChevronRight, MoreHorizontal } from "lucide-react";
|
||||
import { Slot as SlotPrimitive } from "radix-ui";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
|
||||
}
|
||||
|
||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||
return (
|
||||
<ol
|
||||
data-slot="breadcrumb-list"
|
||||
className={cn(
|
||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
return <li data-slot="breadcrumb-item" className={cn("inline-flex items-center gap-1.5", className)} {...props} />;
|
||||
}
|
||||
|
||||
function BreadcrumbLink({
|
||||
asChild,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? SlotPrimitive.Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp data-slot="breadcrumb-link" className={cn("hover:text-foreground transition-colors", className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("text-foreground font-normal", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-separator"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:size-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRight />}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="size-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { Slot as SlotPrimitive } from "radix-ui";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -42,7 +42,7 @@ function Button({
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const Comp = asChild ? SlotPrimitive.Slot : "button";
|
||||
|
||||
return <Comp data-slot="button" className={cn(buttonVariants({ variant, size, className }))} {...props} />;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import { Checkbox as CheckboxPrimitive } from "radix-ui";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
import { ContextMenu as ContextMenuPrimitive } from "radix-ui";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function ContextMenu({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Root>) {
|
||||
return <ContextMenuPrimitive.Root data-slot="context-menu" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuTrigger({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Trigger>) {
|
||||
return <ContextMenuPrimitive.Trigger data-slot="context-menu-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuGroup({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Group>) {
|
||||
return <ContextMenuPrimitive.Group data-slot="context-menu-group" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuPortal({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Portal>) {
|
||||
return <ContextMenuPrimitive.Portal data-slot="context-menu-portal" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuSub({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Sub>) {
|
||||
return <ContextMenuPrimitive.Sub data-slot="context-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuRadioGroup({ ...props }: React.ComponentProps<typeof ContextMenuPrimitive.RadioGroup>) {
|
||||
return <ContextMenuPrimitive.RadioGroup data-slot="context-menu-radio-group" {...props} />;
|
||||
}
|
||||
|
||||
function ContextMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubTrigger
|
||||
data-slot="context-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronRightIcon className="ml-auto" />
|
||||
</ContextMenuPrimitive.SubTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSubContent({ className, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.SubContent
|
||||
data-slot="context-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuContent({ className, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Content>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Portal>
|
||||
<ContextMenuPrimitive.Content
|
||||
data-slot="context-menu-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</ContextMenuPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Item
|
||||
data-slot="context-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.CheckboxItem
|
||||
data-slot="context-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<CheckIcon className="size-4" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.RadioItem
|
||||
data-slot="context-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<ContextMenuPrimitive.ItemIndicator>
|
||||
<CircleIcon className="size-2 fill-current" />
|
||||
</ContextMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</ContextMenuPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ContextMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Label
|
||||
data-slot="context-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn("text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuSeparator({ className, ...props }: React.ComponentProps<typeof ContextMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<ContextMenuPrimitive.Separator
|
||||
data-slot="context-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ContextMenuShortcut({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="context-menu-shortcut"
|
||||
className={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
ContextMenu,
|
||||
ContextMenuTrigger,
|
||||
ContextMenuContent,
|
||||
ContextMenuItem,
|
||||
ContextMenuCheckboxItem,
|
||||
ContextMenuRadioItem,
|
||||
ContextMenuLabel,
|
||||
ContextMenuSeparator,
|
||||
ContextMenuShortcut,
|
||||
ContextMenuGroup,
|
||||
ContextMenuPortal,
|
||||
ContextMenuSub,
|
||||
ContextMenuSubContent,
|
||||
ContextMenuSubTrigger,
|
||||
ContextMenuRadioGroup,
|
||||
};
|
||||
@@ -0,0 +1,121 @@
|
||||
import { XIcon } from "lucide-react";
|
||||
import { Dialog as DialogPrimitive } from "radix-ui";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||
}
|
||||
|
||||
function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DialogClose({ ...props }: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||
}
|
||||
|
||||
function DialogOverlay({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTitle({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogDescription({ className, ...props }: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
import { Label as LabelPrimitive, Slot as SlotPrimitive } from "radix-ui";
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import {
|
||||
Controller,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
useFormState,
|
||||
type ControllerProps,
|
||||
type FieldPath,
|
||||
type FieldValues,
|
||||
FormProvider,
|
||||
useFormContext,
|
||||
useFormState,
|
||||
} from "react-hook-form";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Form = FormProvider;
|
||||
|
||||
@@ -115,11 +113,11 @@ function FormFieldLabel({ className, ...props }: React.ComponentProps<typeof Lab
|
||||
);
|
||||
}
|
||||
|
||||
function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
|
||||
function FormControl({ ...props }: React.ComponentProps<typeof SlotPrimitive.Slot>) {
|
||||
const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
|
||||
|
||||
return (
|
||||
<Slot
|
||||
<SlotPrimitive.Slot
|
||||
data-slot="form-control"
|
||||
id={formItemId}
|
||||
aria-describedby={!error ? `${formDescriptionId}` : `${formDescriptionId} ${formMessageId}`}
|
||||
|
||||
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-8 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-8 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
|
||||
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
className,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Label as LabelPrimitive } from "radix-ui";
|
||||
import * as React from "react";
|
||||
import * as LabelPrimitive from "@radix-ui/react-label";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { ChevronDownIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function NavigationMenu({
|
||||
className,
|
||||
children,
|
||||
viewport = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
|
||||
viewport?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Root
|
||||
data-slot="navigation-menu"
|
||||
data-viewport={viewport}
|
||||
className={cn("group/navigation-menu relative flex max-w-max flex-1 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{viewport && <NavigationMenuViewport />}
|
||||
</NavigationMenuPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuList({ className, ...props }: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.List
|
||||
data-slot="navigation-menu-list"
|
||||
className={cn("group flex flex-1 list-none items-center justify-center gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuItem({ className, ...props }: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Item data-slot="navigation-menu-item" className={cn("relative", className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1",
|
||||
);
|
||||
|
||||
function NavigationMenuTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Trigger
|
||||
data-slot="navigation-menu-trigger"
|
||||
className={cn(navigationMenuTriggerStyle(), "group", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}{" "}
|
||||
<ChevronDownIcon
|
||||
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuPrimitive.Trigger>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuContent({ className, ...props }: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Content
|
||||
data-slot="navigation-menu-content"
|
||||
className={cn(
|
||||
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
|
||||
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuViewport({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
|
||||
return (
|
||||
<div className={cn("absolute top-full left-0 isolate z-50 flex justify-center")}>
|
||||
<NavigationMenuPrimitive.Viewport
|
||||
data-slot="navigation-menu-viewport"
|
||||
className={cn(
|
||||
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuLink({ className, ...props }: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Link
|
||||
data-slot="navigation-menu-link"
|
||||
className={cn(
|
||||
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function NavigationMenuIndicator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
|
||||
return (
|
||||
<NavigationMenuPrimitive.Indicator
|
||||
data-slot="navigation-menu-indicator"
|
||||
className={cn(
|
||||
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
|
||||
</NavigationMenuPrimitive.Indicator>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
NavigationMenu,
|
||||
NavigationMenuList,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuTrigger,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuIndicator,
|
||||
NavigationMenuViewport,
|
||||
navigationMenuTriggerStyle,
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Popover({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Root>) {
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
|
||||
}
|
||||
|
||||
function PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Trigger>) {
|
||||
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function PopoverContent({
|
||||
className,
|
||||
align = "center",
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
data-slot="popover-content"
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 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 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function PopoverAnchor({ ...props }: React.ComponentProps<typeof PopoverPrimitive.Anchor>) {
|
||||
return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
|
||||
}
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||
import { CircleIcon } from "lucide-react";
|
||||
import { RadioGroup as RadioGroupPrimitive } from "radix-ui";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { ScrollArea as ScrollAreaPrimitive } from "radix-ui";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function ScrollArea({ className, children, ...props }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root data-slot="scroll-area" className={cn("relative", className)} {...props}>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none p-px transition-colors select-none",
|
||||
orientation === "vertical" && "h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" && "h-2.5 flex-col border-t border-t-transparent",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
);
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar };
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as React from "react";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||
import { Select as SelectPrimitive } from "radix-ui";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Separator as SeparatorPrimitive } from "radix-ui";
|
||||
import * as React from "react";
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Switch as SwitchPrimitive } from "radix-ui";
|
||||
import * as React from "react";
|
||||
import * as SwitchPrimitive from "@radix-ui/react-switch";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
return (
|
||||
<div data-slot="table-container" className="relative w-full overflow-x-auto">
|
||||
<table data-slot="table" className={cn("w-full caption-bottom text-sm", className)} {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return <thead data-slot="table-header" className={cn("[&_tr]:border-b", className)} {...props} />;
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
return <tbody data-slot="table-body" className={cn("[&_tr:last-child]:border-0", className)} {...props} />;
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn("bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn("hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TableCaption({ className, ...props }: React.ComponentProps<"caption">) {
|
||||
return (
|
||||
<caption data-slot="table-caption" className={cn("text-muted-foreground mt-4 text-sm", className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption };
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Tabs as TabsPrimitive } from "radix-ui";
|
||||
import * as React from "react";
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui";
|
||||
import * as React from "react";
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { env } from "@/config";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { CircleX, LoaderCircle, RefreshCcw } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { env } from "@/config";
|
||||
import { Button } from "./ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
|
||||
|
||||
|
||||
@@ -45,6 +45,11 @@
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.705 0.015 286.067);
|
||||
--color-1: oklch(66.2% 0.225 25.9);
|
||||
--color-2: oklch(60.4% 0.26 302);
|
||||
--color-3: oklch(69.6% 0.165 251);
|
||||
--color-4: oklch(80.2% 0.134 225);
|
||||
--color-5: oklch(90.7% 0.231 133);
|
||||
}
|
||||
|
||||
.dark {
|
||||
@@ -79,6 +84,11 @@
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.552 0.016 285.938);
|
||||
--color-1: oklch(66.2% 0.225 25.9);
|
||||
--color-2: oklch(60.4% 0.26 302);
|
||||
--color-3: oklch(69.6% 0.165 251);
|
||||
--color-4: oklch(80.2% 0.134 225);
|
||||
--color-5: oklch(90.7% 0.231 133);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
@@ -118,6 +128,29 @@
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
--animate-line-shadow: line-shadow 15s linear infinite;
|
||||
@keyframes line-shadow {
|
||||
0% {
|
||||
background-position: 0 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 100% -100%;
|
||||
}
|
||||
}
|
||||
--animate-rainbow: rainbow var(--speed, 2s) infinite linear;
|
||||
--color-color-5: var(--color-5);
|
||||
--color-color-4: var(--color-4);
|
||||
--color-color-3: var(--color-3);
|
||||
--color-color-2: var(--color-2);
|
||||
--color-color-1: var(--color-1);
|
||||
@keyframes rainbow {
|
||||
0% {
|
||||
background-position: 0%;
|
||||
}
|
||||
100% {
|
||||
background-position: 200%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export const siteConfig = {
|
||||
name: "MemShellParty",
|
||||
url: "https://party.memshell.news",
|
||||
github: "https://github.com/ReaJason/MemShellParty",
|
||||
navItems: [
|
||||
{
|
||||
href: "/memshell",
|
||||
label: "MemShellGenerator",
|
||||
},
|
||||
{
|
||||
href: "/detection",
|
||||
label: "DetectionGenerator",
|
||||
},
|
||||
],
|
||||
};
|
||||
+2
-2
@@ -1,9 +1,9 @@
|
||||
import ReactDOM from "react-dom/client";
|
||||
import "./index.css";
|
||||
import { TailwindIndicator } from "@/components/tailwind-indicator.tsx";
|
||||
import { env } from "@/config.ts";
|
||||
import { I18nextProvider } from "react-i18next";
|
||||
import { RouterProvider } from "react-router-dom";
|
||||
import { TailwindIndicator } from "@/components/tailwind-indicator.tsx";
|
||||
import { env } from "@/config.ts";
|
||||
import i18n from "./i18n/i18n";
|
||||
import { QueryProvider } from "./providers/query-client-provider";
|
||||
import { router } from "./router";
|
||||
|
||||
@@ -0,0 +1,802 @@
|
||||
/** biome-ignore-all lint/suspicious/noThenProperty: no way */
|
||||
|
||||
import { yupResolver } from "@hookform/resolvers/yup";
|
||||
import { AlertTriangle, Trash2 } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useId, useState } from "react";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import * as yup from "yup";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardFooter } from "@/components/ui/card";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormFieldItem,
|
||||
FormFieldLabel,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
// --- 类型定义 ---
|
||||
|
||||
// 回显方式
|
||||
type EchoMethod = "header" | "body" | "dnslog" | "httplog" | "sleep";
|
||||
|
||||
// 回显内容
|
||||
type EchoContent = "middleware" | "os" | "jdk" | "bytecode" | "command";
|
||||
|
||||
// 表单值的完整类型
|
||||
interface PayloadFormValues {
|
||||
echoMethod: EchoMethod;
|
||||
echoContent?: EchoContent;
|
||||
// Header
|
||||
headerName?: string;
|
||||
// DNSLog / HTTPLog
|
||||
logAddress?: string;
|
||||
middlewareType?: "tomcat" | "jetty" | "springboot" | "weblogic";
|
||||
paramSource?: "builtin" | "requestParam" | "requestHeader";
|
||||
requestParamName?: string;
|
||||
requestHeaderName?: string;
|
||||
bytecodeBase64?: string;
|
||||
command?: string;
|
||||
// Sleep 选项
|
||||
sleepMiddleware?: "tomcat" | "jetty" | "springboot" | "weblogic";
|
||||
sleepMiddlewareDelay?: number;
|
||||
sleepOsDelays?: { os: "linux" | "windows" | "macos"; delay: number }[];
|
||||
sleepJdkDetectionType?: "version" | "type";
|
||||
sleepJdkVersion?: "8" | "11" | "17";
|
||||
sleepJdkType?: "jdk" | "jre";
|
||||
sleepJdkDelay?: number;
|
||||
sleepBytecodeSuccessDelay?: number;
|
||||
}
|
||||
|
||||
const formSchema = yup.object().shape({
|
||||
echoMethod: yup.string().required("请选择一种回显方式"),
|
||||
|
||||
// 条件:当 echoMethod 是 'header'
|
||||
headerName: yup.string().when("echoMethod", {
|
||||
is: "header",
|
||||
then: (schema) => schema.required("必须填写 Header Name"),
|
||||
otherwise: (schema) => schema.optional(),
|
||||
}),
|
||||
|
||||
// 条件:当 echoMethod 是 'dnslog' 或 'httplog'
|
||||
logAddress: yup.string().when("echoMethod", {
|
||||
is: (val: EchoMethod) => val === "dnslog" || val === "httplog",
|
||||
then: (schema) => schema.required("必须填写 Log 地址"),
|
||||
otherwise: (schema) => schema.optional(),
|
||||
}),
|
||||
|
||||
// 只有在选择了具体内容时才需要
|
||||
echoContent: yup.string().required("请选择回显内容"),
|
||||
|
||||
// --- 嵌套条件验证 ---
|
||||
|
||||
// 中间件类型在特定组合下是必须的
|
||||
middlewareType: yup.string().when(["echoMethod", "echoContent"], {
|
||||
is: (echoMethod: EchoMethod, echoContent: EchoContent) =>
|
||||
(echoMethod === "header" || echoMethod === "body") &&
|
||||
(echoContent === "os" || echoContent === "jdk" || echoContent === "bytecode"),
|
||||
then: (schema) => schema.required("请选择中间件类型"),
|
||||
otherwise: (schema) => schema.optional(),
|
||||
}),
|
||||
|
||||
// 字节码相关
|
||||
paramSource: yup.string().when(["echoMethod", "echoContent"], {
|
||||
is: (echoMethod: EchoMethod, echoContent: EchoContent) =>
|
||||
(echoMethod === "header" || echoMethod === "body") && echoContent === "bytecode",
|
||||
then: (schema) => schema.required("请选择字节码来源"),
|
||||
otherwise: (schema) => schema.optional(),
|
||||
}),
|
||||
bytecodeBase64: yup.string().when(["echoContent", "paramSource"], {
|
||||
is: (echoContent: EchoContent, paramSource: string) => echoContent === "bytecode" && paramSource === "builtin",
|
||||
then: (schema) => schema.required("必须填写字节码 Base64"),
|
||||
otherwise: (schema) => schema.optional(),
|
||||
}),
|
||||
requestParamName: yup.string().when("paramSource", {
|
||||
is: (paramSource: string) => paramSource === "requestParam",
|
||||
then: (schema) => schema.required("必须填写 Request Param Name"),
|
||||
otherwise: (schema) => schema.optional(),
|
||||
}),
|
||||
requestHeaderName: yup.string().when("paramSource", {
|
||||
is: (paramSource: string) => paramSource === "requestHeader",
|
||||
then: (schema) => schema.required("必须填写 Request Header Name"),
|
||||
otherwise: (schema) => schema.optional(),
|
||||
}),
|
||||
command: yup.string().when("echoContent", {
|
||||
is: (echoContent: EchoContent) => echoContent === "command",
|
||||
then: (schema) => schema.required("必须填写执行的命令"),
|
||||
otherwise: (schema) => schema.optional(),
|
||||
}),
|
||||
|
||||
// --- Sleep 相关验证 ---
|
||||
sleepMiddleware: yup.string().when(["echoMethod", "echoContent"], {
|
||||
is: (method: string, content: string) => method === "sleep" && content === "middleware",
|
||||
then: (s) => s.required("请选择目标中间件"),
|
||||
otherwise: (s) => s.optional(),
|
||||
}),
|
||||
sleepMiddlewareDelay: yup.number().when(["echoMethod", "echoContent"], {
|
||||
is: (method: string, content: string) => method === "sleep" && content === "middleware",
|
||||
then: (s) => s.required("请填写延迟时间").min(1, "延迟至少为 1 秒").typeError("请输入有效的秒数"),
|
||||
otherwise: (s) => s.optional(),
|
||||
}),
|
||||
sleepOsDelays: yup.array().when(["echoMethod", "echoContent"], {
|
||||
is: (method: string, content: string) => method === "sleep" && content === "os",
|
||||
then: (s) =>
|
||||
s
|
||||
.of(
|
||||
yup.object().shape({
|
||||
os: yup.string().required(),
|
||||
delay: yup.number().required("请填写延迟时间").min(1, "延迟至少为 1 秒").typeError("请输入有效的秒数"),
|
||||
}),
|
||||
)
|
||||
.min(1, "至少需要一个操作系统延迟配置"),
|
||||
otherwise: (s) => s.optional(),
|
||||
}),
|
||||
sleepJdkDetectionType: yup.string().when(["echoMethod", "echoContent"], {
|
||||
is: (method: string, content: string) => method === "sleep" && content === "jdk",
|
||||
then: (s) => s.required("请选择 JDK 探测类型"),
|
||||
otherwise: (s) => s.optional(),
|
||||
}),
|
||||
sleepJdkVersion: yup.string().when("sleepJdkDetectionType", {
|
||||
is: "version",
|
||||
then: (s) => s.required("请选择 JDK 版本"),
|
||||
otherwise: (s) => s.optional(),
|
||||
}),
|
||||
sleepJdkType: yup.string().when("sleepJdkDetectionType", {
|
||||
is: "type",
|
||||
then: (s) => s.required("请选择 JDK/JRE 类型"),
|
||||
otherwise: (s) => s.optional(),
|
||||
}),
|
||||
sleepJdkDelay: yup.number().when(["echoMethod", "echoContent"], {
|
||||
is: (method: string, content: string) => method === "sleep" && content === "jdk",
|
||||
then: (s) => s.required("请填写延迟时间").min(1, "延迟至少为 1 秒").typeError("请输入有效的秒数"),
|
||||
otherwise: (s) => s.optional(),
|
||||
}),
|
||||
sleepBytecodeSuccessDelay: yup.number().when(["echoMethod", "echoContent"], {
|
||||
is: (method: string, content: string) => method === "sleep" && content === "bytecode",
|
||||
then: (s) => s.required("请填写延迟时间").min(1, "延迟至少为 1 秒").typeError("请输入有效的秒数"),
|
||||
otherwise: (s) => s.optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
export default function EchoPayloadGenerator() {
|
||||
const form = useForm<PayloadFormValues>({
|
||||
resolver: yupResolver(formSchema) as any,
|
||||
defaultValues: {
|
||||
echoMethod: "header",
|
||||
echoContent: undefined,
|
||||
headerName: "X-Echo",
|
||||
logAddress: "",
|
||||
middlewareType: undefined,
|
||||
paramSource: "builtin",
|
||||
requestHeaderName: "X-Data",
|
||||
requestParamName: "payload",
|
||||
sleepOsDelays: [{ os: "linux", delay: 5 }],
|
||||
sleepJdkDetectionType: "version",
|
||||
sleepJdkType: "jdk",
|
||||
sleepJdkVersion: "8",
|
||||
sleepJdkDelay: 5,
|
||||
sleepBytecodeSuccessDelay: 5,
|
||||
sleepMiddlewareDelay: 5,
|
||||
},
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "sleepOsDelays",
|
||||
});
|
||||
|
||||
// 监视表单值的变化以动态渲染 UI
|
||||
const watchedEchoMethod = form.watch("echoMethod");
|
||||
const watchedEchoContent = form.watch("echoContent");
|
||||
const watchedParamSource = form.watch("paramSource");
|
||||
const watchedSleepJdkDetectionType = form.watch("sleepJdkDetectionType");
|
||||
|
||||
// 重置子选项
|
||||
React.useEffect(() => {
|
||||
form.setValue("echoContent", undefined);
|
||||
}, [form.setValue]);
|
||||
|
||||
function onSubmit(values: PayloadFormValues) {
|
||||
// 在这里可以构建最终的 Payload
|
||||
console.log("Form Submitted:", values);
|
||||
}
|
||||
|
||||
// --- 动态内容渲染 ---
|
||||
const [isFile, setIsFile] = useState(false);
|
||||
const optionOneId = useId();
|
||||
const optionTwoId = useId();
|
||||
|
||||
const renderContentOptions = () => {
|
||||
const baseOptions: { value: EchoContent; label: string }[] = [
|
||||
{ value: "middleware", label: "中间件类型" },
|
||||
{ value: "os", label: "操作系统类型" },
|
||||
{ value: "jdk", label: "JDK 信息" },
|
||||
{ value: "command", label: "命令执行" },
|
||||
{ value: "bytecode", label: "自定义字节码执行" },
|
||||
];
|
||||
|
||||
let filteredOptions = baseOptions;
|
||||
|
||||
// 根据规则过滤选项
|
||||
if (watchedEchoMethod === "header" || watchedEchoMethod === "body") {
|
||||
filteredOptions = baseOptions.filter((opt) => opt.value !== "middleware");
|
||||
}
|
||||
|
||||
if (watchedEchoMethod === "dnslog" || watchedEchoMethod === "httplog" || watchedEchoMethod === "sleep") {
|
||||
filteredOptions = baseOptions.filter((opt) => opt.value !== "command" && opt.value !== "bytecode");
|
||||
}
|
||||
|
||||
return (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="echoContent"
|
||||
render={({ field }) => (
|
||||
<FormFieldItem>
|
||||
<FormFieldLabel>回显内容</FormFieldLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择要回显的内容..." />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{filteredOptions.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormFieldItem>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const renderDynamicFields = () => {
|
||||
// --- Response Header / Body ---
|
||||
if ((watchedEchoMethod === "header" || watchedEchoMethod === "body") && watchedEchoContent) {
|
||||
return (
|
||||
<>
|
||||
{watchedEchoContent === "bytecode" && (
|
||||
<div className="space-y-4 pt-4 border-t mt-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="paramSource"
|
||||
render={({ field }) => (
|
||||
<FormFieldItem className="space-y-2">
|
||||
<FormFieldLabel>字节码来源</FormFieldLabel>
|
||||
<FormControl>
|
||||
<RadioGroup onValueChange={field.onChange} defaultValue={field.value} className="flex gap-4">
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="builtin" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">内嵌字节码</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="requestHeader" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">通过 Request Header 传递</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="requestParam" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">通过 Request Param 传递</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormFieldItem>
|
||||
)}
|
||||
/>
|
||||
{watchedParamSource === "builtin" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="bytecodeBase64"
|
||||
render={({ field }) => (
|
||||
<FormFieldItem>
|
||||
<FormFieldLabel>字节码 Base64</FormFieldLabel>
|
||||
<RadioGroup
|
||||
value={isFile ? "file" : "base64"}
|
||||
onValueChange={(value) => {
|
||||
field.onChange("");
|
||||
setIsFile(value === "file");
|
||||
}}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="base64" id={optionOneId} />
|
||||
<Label htmlFor={optionOneId}>Base64</Label>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<RadioGroupItem value="file" id={optionTwoId} />
|
||||
<Label htmlFor={optionTwoId}>File</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
<FormControl className="mt-2 items-center">
|
||||
{isFile ? (
|
||||
<Input
|
||||
onChange={(e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
const base64String = (event.target?.result as string)?.split(",")[1] || "";
|
||||
field.onChange(base64String);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}}
|
||||
accept=".class"
|
||||
placeholder="请输入"
|
||||
type="file"
|
||||
/>
|
||||
) : (
|
||||
<Textarea {...field} placeholder="请输入" className="h-24" />
|
||||
)}
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormFieldItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{watchedParamSource === "requestParam" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="requestParamName"
|
||||
render={({ field }) => (
|
||||
<FormFieldItem>
|
||||
<FormFieldLabel>Request Param Name</FormFieldLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="例如: cmd, data, ..." {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormFieldItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{watchedParamSource === "requestHeader" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="requestHeaderName"
|
||||
render={({ field }) => (
|
||||
<FormFieldItem>
|
||||
<FormFieldLabel>Request Header Name</FormFieldLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="例如: cmd, data, ..." {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormFieldItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{watchedEchoContent === "command" && (
|
||||
<div className="space-y-4 pt-4 border-t mt-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="paramSource"
|
||||
render={({ field }) => (
|
||||
<FormFieldItem className="space-y-2">
|
||||
<FormFieldLabel>命令来源</FormFieldLabel>
|
||||
<FormControl>
|
||||
<RadioGroup onValueChange={field.onChange} defaultValue={field.value} className="flex gap-4">
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="builtin" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">内嵌命令</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="requestHeader" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">通过 Request Header 传递</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="requestParam" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">通过 Request Param 传递</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormFieldItem>
|
||||
)}
|
||||
/>
|
||||
{watchedParamSource === "builtin" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="command"
|
||||
render={({ field }) => (
|
||||
<FormFieldItem>
|
||||
<FormFieldLabel>执行命令</FormFieldLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="例如: whoami, id, hostname" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormFieldItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{watchedParamSource === "requestParam" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="requestParamName"
|
||||
render={({ field }) => (
|
||||
<FormFieldItem>
|
||||
<FormFieldLabel>Request Param Name</FormFieldLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="例如: cmd, data, ..." {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormFieldItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{watchedParamSource === "requestHeader" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="requestHeaderName"
|
||||
render={({ field }) => (
|
||||
<FormFieldItem>
|
||||
<FormFieldLabel>Request Header Name</FormFieldLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="例如: cmd, data, ..." {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormFieldItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Sleep Delay ---
|
||||
if (watchedEchoMethod === "sleep" && watchedEchoContent) {
|
||||
return (
|
||||
<div className="space-y-4 pt-4 border-t mt-4">
|
||||
{watchedEchoContent === "middleware" && (
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sleepMiddleware"
|
||||
render={({ field }) => (
|
||||
<FormFieldItem>
|
||||
<FormFieldLabel>目标中间件</FormFieldLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择中间件..." />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="tomcat">Tomcat</SelectItem>
|
||||
<SelectItem value="jetty">Jetty</SelectItem>
|
||||
<SelectItem value="springboot">Spring Boot</SelectItem>
|
||||
<SelectItem value="weblogic">WebLogic</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormFieldItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sleepMiddlewareDelay"
|
||||
render={({ field }) => (
|
||||
<FormFieldItem>
|
||||
<FormFieldLabel>命中延迟时间 (秒)</FormFieldLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="例如: 5"
|
||||
{...field}
|
||||
onChange={(event) => field.onChange(+event.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormFieldItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{watchedEchoContent === "os" && (
|
||||
<div className="space-y-4">
|
||||
<FormFieldLabel>操作系统与延迟时间</FormFieldLabel>
|
||||
{fields.map((item, index) => (
|
||||
<div key={item.id} className="flex items-center gap-2">
|
||||
<Controller
|
||||
control={form.control}
|
||||
name={`sleepOsDelays.${index}.os`}
|
||||
render={({ field }) => (
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="linux">Linux</SelectItem>
|
||||
<SelectItem value="windows">Windows</SelectItem>
|
||||
<SelectItem value="macos">macOS</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name={`sleepOsDelays.${index}.delay`}
|
||||
render={({ field }) => (
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="延迟(秒)"
|
||||
{...field}
|
||||
onChange={(event) => field.onChange(+event.target.value)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Button type="button" variant="destructive" size="icon" onClick={() => remove(index)}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => append({ os: "windows", delay: 10 })}>
|
||||
添加探测系统
|
||||
</Button>
|
||||
<FormMessage>{form.formState.errors.sleepOsDelays?.message}</FormMessage>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{watchedEchoContent === "jdk" && (
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sleepJdkDetectionType"
|
||||
render={({ field }) => (
|
||||
<FormFieldItem className="space-y-2">
|
||||
<FormFieldLabel>JDK 探测类型</FormFieldLabel>
|
||||
<FormControl>
|
||||
<RadioGroup onValueChange={field.onChange} defaultValue={field.value} className="flex gap-4">
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="version" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">按版本 (大于等于)</FormLabel>
|
||||
</FormItem>
|
||||
<FormItem className="flex items-center space-x-2">
|
||||
<FormControl>
|
||||
<RadioGroupItem value="type" />
|
||||
</FormControl>
|
||||
<FormLabel className="font-normal">按类型 (JDK/JRE)</FormLabel>
|
||||
</FormItem>
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
</FormFieldItem>
|
||||
)}
|
||||
/>
|
||||
{watchedSleepJdkDetectionType === "version" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sleepJdkVersion"
|
||||
render={({ field }) => (
|
||||
<FormFieldItem>
|
||||
<FormFieldLabel>JDK 版本</FormFieldLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="8">JDK 8</SelectItem>
|
||||
<SelectItem value="11">JDK 11</SelectItem>
|
||||
<SelectItem value="17">JDK 17</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormFieldItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{watchedSleepJdkDetectionType === "type" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sleepJdkType"
|
||||
render={({ field }) => (
|
||||
<FormFieldItem>
|
||||
<FormFieldLabel>目标类型</FormFieldLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="jdk">JDK</SelectItem>
|
||||
<SelectItem value="jre">JRE</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormFieldItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sleepJdkDelay"
|
||||
render={({ field }) => (
|
||||
<FormFieldItem>
|
||||
<FormFieldLabel>命中延迟时间 (秒)</FormFieldLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="例如: 5"
|
||||
{...field}
|
||||
onChange={(event) => field.onChange(+event.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormFieldItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{watchedEchoContent === "bytecode" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="sleepBytecodeSuccessDelay"
|
||||
render={({ field }) => (
|
||||
<FormFieldItem>
|
||||
<FormFieldLabel>执行成功延迟时间 (秒)</FormFieldLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="number"
|
||||
placeholder="例如: 8"
|
||||
{...field}
|
||||
onChange={(event) => field.onChange(+event.target.value)}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormFieldItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl mx-auto p-6 space-y-6">
|
||||
<Card>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<CardContent className="mt-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="echoMethod"
|
||||
render={({ field }) => (
|
||||
<FormFieldItem>
|
||||
<FormFieldLabel>回显方式</FormFieldLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="header">Response Header</SelectItem>
|
||||
<SelectItem value="body">Response Body</SelectItem>
|
||||
<SelectItem value="dnslog">DNSLog</SelectItem>
|
||||
<SelectItem value="httplog">HTTPLog</SelectItem>
|
||||
<SelectItem value="sleep">Sleep 延迟探测</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormFieldItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* --- 根据回显方式出现的特定字段 --- */}
|
||||
{(watchedEchoMethod === "header" || watchedEchoMethod === "body") && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="middlewareType"
|
||||
render={({ field }) => (
|
||||
<FormFieldItem>
|
||||
<FormFieldLabel>中间件类型</FormFieldLabel>
|
||||
<FormDescription>不同的中间件获取 Request/Response 的方式不同。</FormDescription>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="选择中间件..." />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="tomcat">Tomcat</SelectItem>
|
||||
<SelectItem value="jetty">Jetty</SelectItem>
|
||||
<SelectItem value="springboot">Spring Boot</SelectItem>
|
||||
<SelectItem value="weblogic">WebLogic</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormFieldItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{watchedEchoMethod === "header" && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="headerName"
|
||||
render={({ field }) => (
|
||||
<FormFieldItem>
|
||||
<FormFieldLabel>Response Header Name</FormFieldLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="例如: X-Echo-Result" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormFieldItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(watchedEchoMethod === "dnslog" || watchedEchoMethod === "httplog") && (
|
||||
<>
|
||||
<Alert className="mt-2 mb-2">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
<AlertTitle>注意</AlertTitle>
|
||||
<AlertDescription>此方式要求目标服务器能够访问外网 (出网)。</AlertDescription>
|
||||
</Alert>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="logAddress"
|
||||
render={({ field }) => (
|
||||
<FormFieldItem>
|
||||
<FormFieldLabel>{watchedEchoMethod === "dnslog" ? "DNSLog" : "HTTPLog"} 地址</FormFieldLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="例如: abcde.dnslog.cn" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormFieldItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{watchedEchoMethod && renderContentOptions()}
|
||||
{renderDynamicFields()}
|
||||
</CardContent>
|
||||
<CardFooter className="mt-2">
|
||||
<Button type="submit">生成载荷</Button>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,296 @@
|
||||
import { motion } from "framer-motion";
|
||||
import {
|
||||
CombineIcon,
|
||||
GithubIcon,
|
||||
MinimizeIcon,
|
||||
ServerIcon,
|
||||
ShellIcon,
|
||||
Shield,
|
||||
ShieldCheckIcon,
|
||||
TerminalIcon,
|
||||
ZapIcon,
|
||||
} from "lucide-react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import { LineShadowText } from "@/components/magicui/line-shadow-text";
|
||||
import { RainbowButton } from "@/components/magicui/rainbow-button";
|
||||
import { useTheme } from "@/components/theme-provider";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
export default function MemShellPartyLandingPage() {
|
||||
const features = [
|
||||
{
|
||||
icon: <ShieldCheckIcon className="w-8 h-8 text-green-400" />,
|
||||
title: "Non-Intrusive",
|
||||
description:
|
||||
"Generated memshells don't interfere with normal middleware traffic, even with multiple shells injected.",
|
||||
},
|
||||
{
|
||||
icon: <ZapIcon className="w-8 h-8 text-yellow-400" />,
|
||||
title: "High Availability",
|
||||
description: "Comes with comprehensive CI integration tests, covering mainstream middleware for reliability.",
|
||||
},
|
||||
{
|
||||
icon: <MinimizeIcon className="w-8 h-8 text-blue-400" />,
|
||||
title: "Minimal Size",
|
||||
description: "The core code of the memshell is kept as concise as possible for efficient transfer and injection.",
|
||||
},
|
||||
{
|
||||
icon: <CombineIcon className="w-8 h-8 text-purple-400" />,
|
||||
title: "Strong Compatibility",
|
||||
description: "Covers common middleware and frameworks like Tomcat, WebLogic, and Spring.",
|
||||
},
|
||||
];
|
||||
|
||||
const supportedMiddleware = [
|
||||
"Tomcat",
|
||||
"Jetty",
|
||||
"GlassFish",
|
||||
"Payara",
|
||||
"Resin",
|
||||
"SpringWebMVC",
|
||||
"SpringWebFlux",
|
||||
"XXL-JOB",
|
||||
"JBossAS",
|
||||
"JBossEAP",
|
||||
"WildFly",
|
||||
"Undertow",
|
||||
"WebLogic",
|
||||
"WebSphere",
|
||||
"BES",
|
||||
"TongWeb",
|
||||
"InforSuite AS",
|
||||
"Apusic AS",
|
||||
"Primeton",
|
||||
];
|
||||
const supportedShells = [
|
||||
"Godzilla",
|
||||
"Behinder",
|
||||
"AntSword",
|
||||
"Suo5",
|
||||
"Neo-reGeorg",
|
||||
"Command Execution",
|
||||
"Custom Shell",
|
||||
];
|
||||
|
||||
const theme = useTheme();
|
||||
const shadowColor = theme.theme === "dark" ? "#ffffff" : "#000000";
|
||||
|
||||
return (
|
||||
<div className="min-h-screen font-sans bg-background text-foreground">
|
||||
<section className="relative text-center py-20 overflow-hidden">
|
||||
<div className="absolute top-0 left-0 w-full h-full bg-grid-black/[0.05] dark:bg-grid-white/[0.05] [mask-image:linear-gradient(to_bottom,white_10%,transparent_100%)]" />
|
||||
<div className="container mx-auto px-4 relative z-10">
|
||||
<motion.div initial={{ opacity: 0, y: -50 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.8 }}>
|
||||
<div className="inline-flex items-center px-4 py-2 rounded-full border sm:h-8 mb-8 transition-colors dark:bg-red-900/20 dark:border-red-800 dark:text-red-300 bg-red-50 border-red-200 text-red-700">
|
||||
<Shield className="w-4 h-4 mr-2" />
|
||||
<span className="text-sm">For Security Research & Authorized Testing Only</span>
|
||||
</div>
|
||||
<h1 className="text-5xl md:text-7xl font-bold mb-8">
|
||||
<span className="dark:text-white text-gray-900">
|
||||
MemShell
|
||||
<LineShadowText className="italic" shadowColor={shadowColor}>
|
||||
Party
|
||||
</LineShadowText>
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-xl md:text-2xl mb-12 max-w-4xl mx-auto leading-relaxed text-muted-foreground">
|
||||
A self-hosted, visual platform for one-click generation of Java memory shells for common middleware and
|
||||
frameworks. The ultimate learning platform for security researchers.
|
||||
</p>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 50 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.3 }}
|
||||
className="mt-8 flex justify-center gap-4 flex-wrap"
|
||||
>
|
||||
<NavLink to="/memshell">
|
||||
<Button size="lg">
|
||||
<TerminalIcon className="mr-2 h-5 w-5" />
|
||||
Get Started
|
||||
</Button>
|
||||
</NavLink>
|
||||
<a href="https://github.com/ReaJason/MemShellParty" target="_blank" rel="noopener noreferrer">
|
||||
<Button size="lg" variant="outline">
|
||||
<GithubIcon className="mr-2 h-5 w-5" />
|
||||
View on GitHub
|
||||
</Button>
|
||||
</a>
|
||||
</motion.div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-20 bg-secondary/30">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold tracking-tight">Why Choose MemShellParty?</h2>
|
||||
<p className="mt-3 text-lg text-muted-foreground">Designed for modern offensive and defensive scenarios.</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{features.map((feature, index) => (
|
||||
<motion.div
|
||||
key={feature.title}
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.5 }}
|
||||
transition={{ delay: index * 0.1 }}
|
||||
>
|
||||
<Card className="h-full border-border/60 bg-card">
|
||||
<CardHeader className="flex flex-row items-center gap-4">
|
||||
{feature.icon}
|
||||
<CardTitle className="text-lg">{feature.title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<CardDescription>{feature.description}</CardDescription>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-20">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold tracking-tight">Extensive Support Matrix</h2>
|
||||
<p className="mt-3 text-lg text-muted-foreground">
|
||||
Compatible with a wide range of industry-standard technologies.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true, amount: 0.5 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Card className="h-full">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<ServerIcon className="w-6 h-6" />
|
||||
Supported Middleware
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-wrap gap-2 mt-2">
|
||||
{supportedMiddleware.map((tech) => (
|
||||
<Badge key={tech} variant="secondary" className="text-base py-1 px-3">
|
||||
{tech}
|
||||
</Badge>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
whileInView={{ opacity: 1, x: 0 }}
|
||||
viewport={{ once: true, amount: 0.5 }}
|
||||
transition={{ duration: 0.5 }}
|
||||
>
|
||||
<Card className="h-full">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<ShellIcon className="w-6 h-6" />
|
||||
Supported Shell Tools
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="flex flex-wrap gap-2 mt-2">
|
||||
{supportedShells.map((shell) => (
|
||||
<Badge key={shell} variant="secondary" className="text-base py-1 px-3">
|
||||
{shell}
|
||||
</Badge>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="py-20">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold tracking-tight">Get Your Shell in 3 Simple Steps</h2>
|
||||
<p className="mt-3 text-lg text-muted-foreground">A streamlined process from selection to generation.</p>
|
||||
</div>
|
||||
<div className="max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-3 gap-8 text-center">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.5 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
className="flex flex-col items-center"
|
||||
>
|
||||
<div className="flex items-center justify-center w-16 h-16 rounded-full bg-primary text-primary-foreground text-2xl font-bold mb-4">
|
||||
1
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-2">Select Middleware</h3>
|
||||
<p className="text-muted-foreground">Choose your target environment like Tomcat or Spring.</p>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.5 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
className="flex flex-col items-center"
|
||||
>
|
||||
<div className="flex items-center justify-center w-16 h-16 rounded-full bg-primary text-primary-foreground text-2xl font-bold mb-4">
|
||||
2
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-2">Configure Shell</h3>
|
||||
<p className="text-muted-foreground">Pick a shell type and set parameters like passwords or keys.</p>
|
||||
</motion.div>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, amount: 0.5 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
className="flex flex-col items-center"
|
||||
>
|
||||
<div className="flex items-center justify-center w-16 h-16 rounded-full bg-primary text-primary-foreground text-2xl font-bold mb-4">
|
||||
3
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold mb-2">Generate Payload</h3>
|
||||
<p className="text-muted-foreground">Click generate to get the final payload for your operation.</p>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* --- Final CTA Section --- */}
|
||||
<section className="py-20 text-center bg-secondary/30">
|
||||
<div className="container mx-auto px-4">
|
||||
<h2 className="text-3xl md:text-4xl font-bold tracking-tight">Ready to Join the Party?</h2>
|
||||
<p className="mt-4 text-lg text-muted-foreground max-w-xl mx-auto">
|
||||
Visit the GitHub repository to start your memshell exploration, or contribute to the project!
|
||||
</p>
|
||||
<div className="mt-8">
|
||||
<a href="https://github.com/ReaJason/MemShellParty" target="_blank" rel="noopener noreferrer">
|
||||
<RainbowButton size="lg">
|
||||
<GithubIcon className="mr-2 h-5 w-5" />
|
||||
Star & Fork on GitHub
|
||||
</RainbowButton>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* --- Footer --- */}
|
||||
<footer className="border-t py-8">
|
||||
<div className="container mx-auto px-4 flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between text-center sm:text-left">
|
||||
<div>
|
||||
<p className="text-sm font-semibold">MemShellParty</p>
|
||||
<p className="text-xs">
|
||||
Built with ❤️ by{" "}
|
||||
<a href="https://reajason.eu.org" rel="noreferrer noopener" target="_blank" className="font-medium">
|
||||
ReaJason
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,10 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { LoaderCircle, WandSparklesIcon } from "lucide-react";
|
||||
import { useState, useTransition } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLoaderData } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
import { MainConfigCard } from "@/components/main-config-card.tsx";
|
||||
import { PackageConfigCard } from "@/components/package-config-card.tsx";
|
||||
import { ShellResult } from "@/components/shell-result.tsx";
|
||||
@@ -15,15 +22,8 @@ import {
|
||||
ShellToolType,
|
||||
} from "@/types/shell.ts";
|
||||
import { transformToPostData } from "@/utils/transformer.ts";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { LoaderCircle, WandSparklesIcon } from "lucide-react";
|
||||
import { useState, useTransition } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLoaderData } from "react-router-dom";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function IndexPage() {
|
||||
export default function MemShellPage() {
|
||||
const urlParams = useLoaderData();
|
||||
|
||||
const { data: serverConfig } = useQuery<ServerConfig>({
|
||||
+13
-3
@@ -1,8 +1,10 @@
|
||||
import { createBrowserRouter } from "react-router-dom";
|
||||
import RootLayout from "@/components/layouts/root-layout";
|
||||
import { env } from "@/config";
|
||||
import IndexPage from "@/pages";
|
||||
import MemShellPage from "@/pages/memshell";
|
||||
import { FormSchema } from "@/types/schema";
|
||||
import { createBrowserRouter } from "react-router-dom";
|
||||
import DetectionPage from "./pages/detection";
|
||||
import MemShellPartyLandingPage from "./pages/landing";
|
||||
|
||||
// Function to parse URL parameters into form default values
|
||||
const parseUrlParams = (searchParams: URLSearchParams): Partial<FormSchema> => {
|
||||
@@ -48,12 +50,20 @@ export const router = createBrowserRouter(
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <IndexPage />,
|
||||
element: <MemShellPartyLandingPage />,
|
||||
},
|
||||
{
|
||||
path: "memshell",
|
||||
element: <MemShellPage />,
|
||||
loader: ({ request }) => {
|
||||
const url = new URL(request.url);
|
||||
return parseUrlParams(url.searchParams);
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "detection",
|
||||
element: <DetectionPage />,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -80,15 +80,14 @@ export const useYupValidationResolver = (validationSchema: yup.ObjectSchema<any>
|
||||
return {
|
||||
values: {} as FormSchema,
|
||||
errors: errors.inner.reduce(
|
||||
(allErrors, currentError) => ({
|
||||
// biome-ignore lint/performance/noAccumulatingSpread: <explanation>
|
||||
...allErrors,
|
||||
[currentError.path as keyof FormSchema]: {
|
||||
(allErrors, currentError) => {
|
||||
allErrors[currentError.path as keyof FormSchema] = {
|
||||
type: currentError.type ?? "validation",
|
||||
message: currentError.message,
|
||||
};
|
||||
return allErrors;
|
||||
},
|
||||
}),
|
||||
{},
|
||||
{} as FieldErrors<FormSchema>,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user