mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-10-07 10:16:38 +08:00
fix(frontend): switch grouped forward view to full list (#161)
* fix(frontend): left-align announcement logo in homepage banner * fix(frontend): keep multiselect panels floating and preserve summaries * fix(frontend): rebalance config card header spacing * fix(frontend): normalize formatting and fix multiselect modal behavior * fix(frontend): polish batch actions and sharing page guidance * fix(frontend): switch grouped forward view to full list * fix(backend): switch diagnosis internet target to bing * style(frontend): beautify forward group view list display * style: remove 'x' suffix from traffic ratio input and center config alert * fix(ui): expand exit node select upwards in modal * style: fix vertical alignment of logo and title in announcement banner
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import * as React from "react";
|
||||
import { CalendarIcon } from "lucide-react";
|
||||
|
||||
import { FieldContainer, type FieldMetaProps } from "./shared";
|
||||
|
||||
@@ -88,7 +87,6 @@ export function DatePicker({
|
||||
value,
|
||||
}: DatePickerProps) {
|
||||
const id = React.useId();
|
||||
const nativePickerRef = React.useRef<HTMLInputElement | null>(null);
|
||||
const formattedValue = React.useMemo(() => formatDateValue(value), [value]);
|
||||
const [textValue, setTextValue] = React.useState(formattedValue);
|
||||
|
||||
@@ -160,30 +158,6 @@ export function DatePicker({
|
||||
setTextValue(formattedValue);
|
||||
};
|
||||
|
||||
const openNativePicker = () => {
|
||||
if (isDisabled) {
|
||||
return;
|
||||
}
|
||||
const picker = nativePickerRef.current;
|
||||
|
||||
if (!picker) {
|
||||
return;
|
||||
}
|
||||
|
||||
const pickerWithShow = picker as HTMLInputElement & {
|
||||
showPicker?: () => void;
|
||||
};
|
||||
|
||||
if (typeof pickerWithShow.showPicker === "function") {
|
||||
pickerWithShow.showPicker();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
picker.focus();
|
||||
picker.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<FieldContainer
|
||||
description={description}
|
||||
@@ -193,71 +167,40 @@ export function DatePicker({
|
||||
isRequired={isRequired}
|
||||
label={label}
|
||||
>
|
||||
{shouldUseTextInput ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
aria-invalid={isInvalid}
|
||||
className={cn("flex-1", className)}
|
||||
disabled={isDisabled}
|
||||
id={id}
|
||||
inputMode="numeric"
|
||||
placeholder="YYYY-MM-DD"
|
||||
required={isRequired}
|
||||
type="text"
|
||||
value={textValue}
|
||||
onBlur={commitTextInput}
|
||||
onChange={(event) => {
|
||||
const nextValue = event.target.value;
|
||||
<Input
|
||||
aria-invalid={isInvalid}
|
||||
className={cn(className)}
|
||||
disabled={isDisabled}
|
||||
id={id}
|
||||
inputMode={shouldUseTextInput ? "numeric" : undefined}
|
||||
placeholder={shouldUseTextInput ? "YYYY-MM-DD" : undefined}
|
||||
required={isRequired}
|
||||
type={shouldUseTextInput ? "text" : "date"}
|
||||
value={shouldUseTextInput ? textValue : formattedValue}
|
||||
onChange={(event) => {
|
||||
const nextValue = event.target.value;
|
||||
|
||||
setTextValue(nextValue);
|
||||
notifyTextDateChange(nextValue);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
commitTextInput();
|
||||
if (shouldUseTextInput) {
|
||||
setTextValue(nextValue);
|
||||
notifyTextDateChange(nextValue);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
notifyNativeDateChange(nextValue);
|
||||
}}
|
||||
onBlur={shouldUseTextInput ? commitTextInput : undefined}
|
||||
onKeyDown={
|
||||
shouldUseTextInput
|
||||
? (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
commitTextInput();
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
aria-label="打开日历选择器"
|
||||
className="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-md border border-input bg-background text-default-600 shadow-sm transition-colors hover:bg-default-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
disabled={isDisabled}
|
||||
type="button"
|
||||
onClick={openNativePicker}
|
||||
>
|
||||
<CalendarIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<input
|
||||
ref={nativePickerRef}
|
||||
aria-hidden="true"
|
||||
className="sr-only"
|
||||
disabled={isDisabled}
|
||||
tabIndex={-1}
|
||||
type="date"
|
||||
value={formattedValue}
|
||||
onChange={(event) => {
|
||||
const nextValue = event.target.value;
|
||||
|
||||
setTextValue(nextValue);
|
||||
notifyNativeDateChange(nextValue);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Input
|
||||
aria-invalid={isInvalid}
|
||||
className={cn(className)}
|
||||
disabled={isDisabled}
|
||||
id={id}
|
||||
required={isRequired}
|
||||
type="date"
|
||||
value={formattedValue}
|
||||
onChange={(event) => {
|
||||
notifyNativeDateChange(event.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</FieldContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ export interface SelectProps<T = unknown> extends FieldMetaProps {
|
||||
selectionMode?: SelectionMode;
|
||||
size?: "sm" | "md" | "lg";
|
||||
variant?: string;
|
||||
dropdownPlacement?: "bottom" | "top";
|
||||
}
|
||||
|
||||
export interface SelectItemProps {
|
||||
@@ -163,6 +164,7 @@ export function Select<T>({
|
||||
selectedKeys,
|
||||
selectionMode = "single",
|
||||
size,
|
||||
dropdownPlacement = "bottom",
|
||||
}: SelectProps<T>) {
|
||||
const generatedId = React.useId();
|
||||
const options = React.useMemo(
|
||||
@@ -284,9 +286,16 @@ export function Select<T>({
|
||||
return null;
|
||||
}
|
||||
|
||||
const placementClasses = dropdownPlacement === "top"
|
||||
? "bottom-full mb-1"
|
||||
: "top-full mt-1";
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute left-0 top-full z-50 mt-1 max-h-56 w-full space-y-1 overflow-y-auto rounded-md border border-divider bg-background p-2 shadow-md"
|
||||
className={cn(
|
||||
"absolute left-0 z-50 w-full space-y-1 overflow-y-auto rounded-md border border-divider bg-background p-2 shadow-md max-h-56",
|
||||
placementClasses
|
||||
)}
|
||||
id={`${generatedId}-listbox`}
|
||||
ref={listboxRef}
|
||||
role="listbox"
|
||||
|
||||
@@ -4,11 +4,15 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
interface TableClassNames {
|
||||
th?: string;
|
||||
td?: string;
|
||||
tr?: string;
|
||||
wrapper?: string;
|
||||
}
|
||||
|
||||
interface TableStyleContextValue {
|
||||
thClassName?: string;
|
||||
tdClassName?: string;
|
||||
trClassName?: string;
|
||||
}
|
||||
|
||||
const TableStyleContext = React.createContext<TableStyleContextValue>({});
|
||||
@@ -24,7 +28,13 @@ export function Table({
|
||||
...props
|
||||
}: TableProps) {
|
||||
return (
|
||||
<TableStyleContext.Provider value={{ thClassName: classNames?.th }}>
|
||||
<TableStyleContext.Provider
|
||||
value={{
|
||||
thClassName: classNames?.th,
|
||||
tdClassName: classNames?.td,
|
||||
trClassName: classNames?.tr,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn("w-full overflow-auto rounded-md", classNames?.wrapper)}
|
||||
>
|
||||
@@ -151,11 +161,23 @@ export function TableColumn({
|
||||
}
|
||||
|
||||
export function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
const { trClassName } = React.useContext(TableStyleContext);
|
||||
|
||||
return (
|
||||
<tr className={cn("border-b last:border-b-0", className)} {...props} />
|
||||
<tr
|
||||
className={cn("border-b last:border-b-0", trClassName, className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
return <td className={cn("px-3 py-2 align-middle", className)} {...props} />;
|
||||
const { tdClassName } = React.useContext(TableStyleContext);
|
||||
|
||||
return (
|
||||
<td
|
||||
className={cn("px-3 py-2 align-middle", tdClassName, className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user