From 6e5a71f489a63385cea017ea869e035dfa99b659 Mon Sep 17 00:00:00 2001 From: sagit <36596628+Sagit-chu@users.noreply.github.com> Date: Thu, 19 Feb 2026 16:10:02 +0800 Subject: [PATCH] fix(frontend): resolve scroll, diagnosis layout, and multi-select regressions (#147) * fix(frontend): restore modal scroll and bridged multi-select UI * fix(frontend): collapse multi-select panel and clean diagnosis labels --- test-results/.last-run.json | 4 + vite-frontend/src/components/ui/accordion.tsx | 11 +- vite-frontend/src/pages/forward.tsx | 4 +- vite-frontend/src/pages/tunnel.tsx | 4 +- vite-frontend/src/pages/user.tsx | 1 - .../src/shadcn-bridge/heroui/accordion.tsx | 25 ++- .../src/shadcn-bridge/heroui/modal.tsx | 87 ++++++-- .../src/shadcn-bridge/heroui/select.tsx | 196 +++++++++++++++--- 8 files changed, 278 insertions(+), 54 deletions(-) create mode 100644 test-results/.last-run.json diff --git a/test-results/.last-run.json b/test-results/.last-run.json new file mode 100644 index 0000000..5fca3f8 --- /dev/null +++ b/test-results/.last-run.json @@ -0,0 +1,4 @@ +{ + "status": "failed", + "failedTests": [] +} \ No newline at end of file diff --git a/vite-frontend/src/components/ui/accordion.tsx b/vite-frontend/src/components/ui/accordion.tsx index eba1eae..f3038aa 100644 --- a/vite-frontend/src/components/ui/accordion.tsx +++ b/vite-frontend/src/components/ui/accordion.tsx @@ -4,11 +4,16 @@ import { ChevronDownIcon } from "lucide-react"; import { cn } from "@/lib/utils"; -function Accordion({ ...props }: React.ComponentProps) { +function Accordion({ + ...props +}: React.ComponentProps) { return ; } -function AccordionItem({ className, ...props }: React.ComponentProps) { +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { return ( svg]:rotate-180", + "flex flex-1 items-center justify-between gap-3 px-3 py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180", className, )} data-slot="accordion-trigger" diff --git a/vite-frontend/src/pages/forward.tsx b/vite-frontend/src/pages/forward.tsx index 12aa1e9..4117fcb 100644 --- a/vite-frontend/src/pages/forward.tsx +++ b/vite-frontend/src/pages/forward.tsx @@ -2638,7 +2638,7 @@ export default function ForwardPage() { {result.success && quality ? ( {result.success && quality ? ( } value={searchKeyword} onChange={(e) => setSearchKeyword(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleSearch()} diff --git a/vite-frontend/src/shadcn-bridge/heroui/accordion.tsx b/vite-frontend/src/shadcn-bridge/heroui/accordion.tsx index b700757..73d9295 100644 --- a/vite-frontend/src/shadcn-bridge/heroui/accordion.tsx +++ b/vite-frontend/src/shadcn-bridge/heroui/accordion.tsx @@ -8,7 +8,8 @@ import { } from "@/components/ui/accordion"; import { cn } from "@/lib/utils"; -export interface AccordionProps extends Omit, "children"> { +export interface AccordionProps + extends Omit, "children"> { children: React.ReactNode; variant?: "bordered" | "light" | "splitted"; } @@ -21,9 +22,27 @@ export interface AccordionItemProps { value?: string; } -export function Accordion({ children, className }: AccordionProps) { +function variantClass(variant: AccordionProps["variant"]) { + if (variant === "splitted") { + return "[&>[data-slot=accordion-item]]:mb-3 [&>[data-slot=accordion-item]]:rounded-xl [&>[data-slot=accordion-item]]:border [&>[data-slot=accordion-item]]:border-divider [&>[data-slot=accordion-item]]:border-b-0 [&>[data-slot=accordion-item]]:bg-content1 [&>[data-slot=accordion-item]]:shadow-sm [&>[data-slot=accordion-item]]:overflow-hidden [&>[data-slot=accordion-item]:last-child]:mb-0"; + } + if (variant === "bordered") { + return "rounded-xl border border-divider bg-content1 overflow-hidden [&>[data-slot=accordion-item]]:border-b [&>[data-slot=accordion-item]:last-child]:border-b-0"; + } + + return ""; +} + +export function Accordion({ + children, + className, + variant = "light", +}: AccordionProps) { return ( - + {children} ); diff --git a/vite-frontend/src/shadcn-bridge/heroui/modal.tsx b/vite-frontend/src/shadcn-bridge/heroui/modal.tsx index aa08355..e9a5398 100644 --- a/vite-frontend/src/shadcn-bridge/heroui/modal.tsx +++ b/vite-frontend/src/shadcn-bridge/heroui/modal.tsx @@ -1,6 +1,9 @@ import * as React from "react"; -import { Dialog, DialogContent as BaseDialogContent } from "@/components/ui/dialog"; +import { + Dialog, + DialogContent as BaseDialogContent, +} from "@/components/ui/dialog"; import { cn } from "@/lib/utils"; interface DisclosureOptions { @@ -35,6 +38,8 @@ export function useDisclosure(options: DisclosureOptions = {}) { interface ModalContextValue { classNames?: Record; onClose: () => void; + scrollBehavior?: "inside" | "outside"; + size?: ModalSize; } const ModalContext = React.createContext(null); @@ -88,6 +93,8 @@ export function Modal({ isOpen = false, onClose, onOpenChange, + scrollBehavior, + size, }: ModalProps) { const handleOpenChange = (open: boolean) => { onOpenChange?.(open); @@ -101,16 +108,21 @@ export function Modal({ onClose: () => { handleOpenChange(false); }, + scrollBehavior, + size, }; return ( - {children} + + {children} + ); } -interface ModalContentProps extends Omit, "children"> { +interface ModalContentProps + extends Omit, "children"> { children: React.ReactNode | ((onClose: () => void) => React.ReactNode); scrollBehavior?: "inside" | "outside"; size?: ModalSize; @@ -124,15 +136,24 @@ export function ModalContent({ ...props }: ModalContentProps) { const context = useModalContext(); + const resolvedScrollBehavior = scrollBehavior ?? context?.scrollBehavior; + const resolvedSize = size ?? context?.size; const renderedChildren = - typeof children === "function" ? children(() => context?.onClose()) : children; + typeof children === "function" + ? children(() => context?.onClose()) + : children; return ( [data-slot=modal-body]]:min-h-0 [&>[data-slot=modal-body]]:flex-1 [&>[data-slot=modal-body]]:overflow-y-auto" + : "", className, )} showCloseButton={false} @@ -143,18 +164,54 @@ export function ModalContent({ ); } -export function ModalHeader({ className, ...props }: React.ComponentProps<"div">) { - return
; -} +export function ModalHeader({ + className, + ...props +}: React.ComponentProps<"div">) { + const context = useModalContext(); -export function ModalBody({ className, ...props }: React.ComponentProps<"div">) { - return
; -} - -export function ModalFooter({ className, ...props }: React.ComponentProps<"div">) { return (
+ ); +} + +export function ModalBody({ + className, + ...props +}: React.ComponentProps<"div">) { + const context = useModalContext(); + + return ( +
+ ); +} + +export function ModalFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + const context = useModalContext(); + + return ( +
); diff --git a/vite-frontend/src/shadcn-bridge/heroui/select.tsx b/vite-frontend/src/shadcn-bridge/heroui/select.tsx index 68b61fa..56a7aad 100644 --- a/vite-frontend/src/shadcn-bridge/heroui/select.tsx +++ b/vite-frontend/src/shadcn-bridge/heroui/select.tsx @@ -1,9 +1,11 @@ import * as React from "react"; - -import { cn } from "@/lib/utils"; +import { ChevronDownIcon } from "lucide-react"; import { FieldContainer, extractText, type FieldMetaProps } from "./shared"; +import { Checkbox as BaseCheckbox } from "@/components/ui/checkbox"; +import { cn } from "@/lib/utils"; + type SelectionMode = "single" | "multiple"; type SelectionValue = Iterable | Set | Array; @@ -131,6 +133,17 @@ function sizeClass(size: SelectProps["size"]) { return "h-9 text-sm"; } +function textSizeClass(size: SelectProps["size"]) { + if (size === "sm") { + return "text-xs"; + } + if (size === "lg") { + return "text-base"; + } + + return "text-sm"; +} + export function Select({ children, className, @@ -152,12 +165,41 @@ export function Select({ size, }: SelectProps) { const generatedId = React.useId(); - const options = React.useMemo(() => getOptions(children, items), [children, items]); + const options = React.useMemo( + () => getOptions(children, items), + [children, items], + ); + const [isExpanded, setIsExpanded] = React.useState(false); const selected = React.useMemo(() => toSet(selectedKeys), [selectedKeys]); const disabled = React.useMemo(() => toSet(disabledKeys), [disabledKeys]); const selectedArray = Array.from(selected); const singleValue = selectedArray[0] ?? ""; + const selectedText = + selectedArray.length > 0 + ? options + .filter((option) => selected.has(option.key)) + .map((option) => option.label) + .join("、") + : (placeholder ?? "请选择"); + + const updateMultipleSelection = (key: string, checked?: boolean) => { + if (isDisabled || disabled.has(key)) { + return; + } + + const next = new Set(selected); + const shouldSelect = + typeof checked === "boolean" ? checked : !next.has(key); + + if (shouldSelect) { + next.add(key); + } else { + next.delete(key); + } + + onSelectionChange?.(next); + }; const handleChange = (event: React.ChangeEvent) => { onChange?.(event); @@ -167,7 +209,9 @@ export function Select({ } if (selectionMode === "multiple") { - const values = Array.from(event.target.selectedOptions).map((option) => option.value); + const values = Array.from(event.target.selectedOptions).map( + (option) => option.value, + ); onSelectionChange(new Set(values)); @@ -193,31 +237,127 @@ export function Select({ isRequired={isRequired} label={label} > - - ) : null} - {options.map((option) => ( - - ))} - + {options.map((option) => ( + + ))} + + )} ); }