mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-09-04 22:46:55 +08:00
feat: redesign settings page
This commit is contained in:
@@ -2,11 +2,11 @@ import * as React from "react";
|
||||
|
||||
export type ButtonType = "primary" | "danger" | null;
|
||||
|
||||
export default function IconButton(props: {
|
||||
export default function Btn(props: {
|
||||
onClick?: () => void;
|
||||
icon?: JSX.Element;
|
||||
type?: ButtonType;
|
||||
text?: string;
|
||||
text?: React.ReactNode;
|
||||
bordered?: boolean;
|
||||
shadow?: boolean;
|
||||
className?: string;
|
||||
@@ -20,8 +20,6 @@ export default function IconButton(props: {
|
||||
icon,
|
||||
type,
|
||||
text,
|
||||
bordered,
|
||||
shadow,
|
||||
className,
|
||||
title,
|
||||
disabled,
|
||||
@@ -29,18 +27,30 @@ export default function IconButton(props: {
|
||||
autoFocus,
|
||||
} = props;
|
||||
|
||||
let btnClassName;
|
||||
|
||||
switch (type) {
|
||||
case "primary":
|
||||
btnClassName = `${disabled ? "bg-blue-300" : "bg-blue-600"} text-white`;
|
||||
break;
|
||||
case "danger":
|
||||
btnClassName = `${
|
||||
disabled ? "bg-blue-300" : "bg-blue-600"
|
||||
} text-text-danger`;
|
||||
break;
|
||||
default:
|
||||
btnClassName = `${
|
||||
disabled ? "bg-gray-100" : "bg-gray-300"
|
||||
} text-gray-500`;
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`
|
||||
${className ?? ""}
|
||||
py-2 px-3 flex items-center justify-center gap-1 rounded-action-btn shadow-btn transition-all duration-300 select-none
|
||||
${
|
||||
type === "primary"
|
||||
? `${disabled ? "bg-blue-300" : "bg-blue-600"}`
|
||||
: `${disabled ? "bg-gray-100" : "bg-gray-300"}`
|
||||
}
|
||||
${disabled ? "cursor-not-allowed" : "cursor-pointer"}
|
||||
${type === "primary" ? `text-white` : `text-gray-500`}
|
||||
${btnClassName}
|
||||
`}
|
||||
onClick={onClick}
|
||||
title={title}
|
||||
|
32
app/components/Card/index.tsx
Normal file
32
app/components/Card/index.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { ReactNode } from "react";
|
||||
|
||||
export interface CardProps {
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
title?: ReactNode;
|
||||
inMobile?: boolean;
|
||||
}
|
||||
|
||||
export default function Card(props: CardProps) {
|
||||
const { className, children, title, inMobile } = props;
|
||||
|
||||
let titleClassName = "ml-4 mb-3";
|
||||
if (inMobile) {
|
||||
titleClassName = "ml-3 mb-3";
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{title && (
|
||||
<div
|
||||
className={`capitalize font-black font-setting-card-title text-sm-mobile font-weight-setting-card-title ${titleClassName}`}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
)}
|
||||
<div className={`px-4 py-1 rounded-lg bg-white ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
82
app/components/Input/index.tsx
Normal file
82
app/components/Input/index.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import PasswordVisible from "@/app/icons/passwordVisible.svg";
|
||||
import PasswordInvisible from "@/app/icons/passwordInvisible.svg";
|
||||
import {
|
||||
DetailedHTMLProps,
|
||||
InputHTMLAttributes,
|
||||
useContext,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import List from "@/app/components/List";
|
||||
|
||||
export interface CommonInputProps
|
||||
extends Omit<
|
||||
DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>,
|
||||
"onChange" | "type" | "value"
|
||||
> {
|
||||
className?: string;
|
||||
}
|
||||
export interface NumberInputProps {
|
||||
onChange?: (v: number) => void;
|
||||
type?: "number";
|
||||
value?: number;
|
||||
}
|
||||
|
||||
export interface TextInputProps {
|
||||
onChange?: (v: string) => void;
|
||||
type?: "text" | "password";
|
||||
value?: string;
|
||||
}
|
||||
|
||||
export interface InputProps {
|
||||
onChange?: ((v: string) => void) | ((v: number) => void);
|
||||
type?: "text" | "password" | "number";
|
||||
value?: string | number;
|
||||
}
|
||||
|
||||
export default function Input(
|
||||
props: CommonInputProps & NumberInputProps,
|
||||
): JSX.Element;
|
||||
export default function Input(
|
||||
props: CommonInputProps & TextInputProps,
|
||||
): JSX.Element;
|
||||
export default function Input(props: CommonInputProps & InputProps) {
|
||||
const { value, type = "text", onChange, className, ...rest } = props;
|
||||
const [show, setShow] = useState(false);
|
||||
|
||||
const internalType = (show && "text") || type;
|
||||
|
||||
const { update } = useContext(List.ListContext);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
update?.({ type: "input" });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-[100%] rounded-chat-input bg-gray-100 flex gap-3 items-center px-3 py-2 ${className}`}
|
||||
>
|
||||
<input
|
||||
{...rest}
|
||||
className=" overflow-hidden text-black text-sm-title leading-input outline-none flex-1"
|
||||
type={internalType}
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
if (type === "number") {
|
||||
const v = e.currentTarget.valueAsNumber;
|
||||
(onChange as NumberInputProps["onChange"])?.(v);
|
||||
} else {
|
||||
const v = e.currentTarget.value;
|
||||
(onChange as TextInputProps["onChange"])?.(v);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{type == "password" && (
|
||||
<div onClick={() => setShow((pre) => !pre)}>
|
||||
{show ? <PasswordVisible /> : <PasswordInvisible />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
129
app/components/List/index.tsx
Normal file
129
app/components/List/index.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import {
|
||||
ReactNode,
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
interface WidgetStyle {
|
||||
selectClassName?: string;
|
||||
inputClassName?: string;
|
||||
rangeClassName?: string;
|
||||
switchClassName?: string;
|
||||
inputNextLine?: boolean;
|
||||
rangeNextLine?: boolean;
|
||||
}
|
||||
|
||||
interface ChildrenMeta {
|
||||
type?: "unknown" | "input" | "range";
|
||||
}
|
||||
|
||||
export interface ListProps {
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
id?: string;
|
||||
isMobileScreen?: boolean;
|
||||
widgetStyle?: WidgetStyle;
|
||||
}
|
||||
|
||||
export interface ListItemProps {
|
||||
title: string;
|
||||
subTitle?: string;
|
||||
children?: JSX.Element | JSX.Element[];
|
||||
className?: string;
|
||||
onClick?: () => void;
|
||||
nextline?: boolean;
|
||||
}
|
||||
|
||||
export const ListContext = createContext<
|
||||
{ isMobileScreen?: boolean; update?: (m: ChildrenMeta) => void } & WidgetStyle
|
||||
>({ isMobileScreen: false });
|
||||
|
||||
export function ListItem(props: ListItemProps) {
|
||||
const {
|
||||
className = "",
|
||||
onClick,
|
||||
title,
|
||||
subTitle,
|
||||
children,
|
||||
nextline,
|
||||
} = props;
|
||||
|
||||
const context = useContext(ListContext);
|
||||
|
||||
const [childrenMeta, setMeta] = useState<ChildrenMeta>({});
|
||||
|
||||
const { isMobileScreen, inputNextLine, rangeNextLine } = context;
|
||||
|
||||
let containerClassName = "py-3";
|
||||
let titleClassName = "";
|
||||
if (isMobileScreen) {
|
||||
containerClassName = "py-2";
|
||||
titleClassName = "";
|
||||
}
|
||||
|
||||
let internalNextLine;
|
||||
|
||||
switch (childrenMeta.type) {
|
||||
case "input":
|
||||
internalNextLine = !!(nextline || inputNextLine);
|
||||
break;
|
||||
case "range":
|
||||
internalNextLine = !!(nextline || rangeNextLine);
|
||||
break;
|
||||
default:
|
||||
internalNextLine = false;
|
||||
}
|
||||
if (childrenMeta.type === "input") {
|
||||
console.log("===============", internalNextLine, nextline, inputNextLine);
|
||||
}
|
||||
|
||||
const update = useCallback((m: ChildrenMeta) => {
|
||||
setMeta(m);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`relative after:h-[0.5px] after:bottom-0 after:w-[100%] after:left-0 after:absolute last:after:hidden after:bg-gray-100 ${
|
||||
internalNextLine ? "" : "flex gap-3"
|
||||
} justify-between items-center px-0 ${containerClassName} ${className}`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div
|
||||
className={`flex-1 flex flex-col justify-start gap-1 ${titleClassName}`}
|
||||
>
|
||||
<div className=" font-common text-sm-mobile font-weight-[500] line-clamp-1">
|
||||
{title}
|
||||
</div>
|
||||
{subTitle && <div className={` text-sm text-gray-300`}>{subTitle}</div>}
|
||||
</div>
|
||||
<ListContext.Provider value={{ ...context, update }}>
|
||||
<div
|
||||
className={`${
|
||||
internalNextLine ? "mt-[0.625rem]" : "max-w-[70%]"
|
||||
} flex items-center justify-center`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</ListContext.Provider>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function List(props: ListProps) {
|
||||
const { className, children, id, widgetStyle } = props;
|
||||
const { isMobileScreen } = useContext(ListContext);
|
||||
return (
|
||||
<ListContext.Provider value={{ isMobileScreen, ...widgetStyle }}>
|
||||
<div className={`flex flex-col w-[100%] ${className}`} id={id}>
|
||||
{children}
|
||||
</div>
|
||||
</ListContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
List.ListItem = ListItem;
|
||||
List.ListContext = ListContext;
|
||||
|
||||
export default List;
|
@@ -1,16 +1,17 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Path } from "@/app/constant";
|
||||
import useDragSideBar from "@/app/hooks/useDragSideBar";
|
||||
import useMobileScreen from "@/app/hooks/useMobileScreen";
|
||||
import {
|
||||
ComponentType,
|
||||
Context,
|
||||
createContext,
|
||||
useContext,
|
||||
useState,
|
||||
} from "react";
|
||||
DEFAULT_SIDEBAR_WIDTH,
|
||||
MAX_SIDEBAR_WIDTH,
|
||||
MIN_SIDEBAR_WIDTH,
|
||||
Path,
|
||||
} from "@/app/constant";
|
||||
import useDrag from "@/app/hooks/useDrag";
|
||||
import useMobileScreen from "@/app/hooks/useMobileScreen";
|
||||
import { updateGlobalCSSVars } from "@/app/utils/client";
|
||||
import { ComponentType, useRef, useState } from "react";
|
||||
|
||||
import DragIcon from "@/app/icons/drag.svg";
|
||||
import { useAppConfig } from "@/app/store/config";
|
||||
|
||||
export interface MenuWrapperInspectProps {
|
||||
setShowPanel?: (v: boolean) => void;
|
||||
@@ -28,10 +29,35 @@ export default function MenuLayout<
|
||||
const [showPanel, setShowPanel] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const config = useAppConfig();
|
||||
|
||||
const isMobileScreen = useMobileScreen();
|
||||
|
||||
const startDragWidth = useRef(config.sidebarWidth ?? DEFAULT_SIDEBAR_WIDTH);
|
||||
// drag side bar
|
||||
const { onDragStart } = useDragSideBar();
|
||||
const { onDragStart } = useDrag({
|
||||
customToggle: () => {
|
||||
config.update((config) => {
|
||||
config.sidebarWidth = DEFAULT_SIDEBAR_WIDTH;
|
||||
});
|
||||
},
|
||||
customDragMove: (nextWidth: number) => {
|
||||
const { menuWidth } = updateGlobalCSSVars(nextWidth);
|
||||
|
||||
document.documentElement.style.setProperty(
|
||||
"--menu-width",
|
||||
`${menuWidth}px`,
|
||||
);
|
||||
config.update((config) => {
|
||||
config.sidebarWidth = nextWidth;
|
||||
});
|
||||
},
|
||||
customLimit: (x: number) =>
|
||||
Math.max(
|
||||
MIN_SIDEBAR_WIDTH,
|
||||
Math.min(MAX_SIDEBAR_WIDTH, startDragWidth.current + x),
|
||||
),
|
||||
});
|
||||
|
||||
let containerClassName = "flex h-[100%] w-[100%]";
|
||||
let listClassName =
|
||||
@@ -64,7 +90,10 @@ export default function MenuLayout<
|
||||
{!isMobileScreen && (
|
||||
<div
|
||||
className={`group absolute right-0 h-[100%] flex items-center`}
|
||||
onPointerDown={(e) => onDragStart(e as any)}
|
||||
onPointerDown={(e) => {
|
||||
startDragWidth.current = config.sidebarWidth;
|
||||
onDragStart(e as any);
|
||||
}}
|
||||
>
|
||||
<div className="opacity-0 group-hover:bg-[rgba($color: #000000, $alpha: 0.01) group-hover:opacity-20">
|
||||
<DragIcon />
|
||||
|
@@ -1,5 +1,7 @@
|
||||
import useRelativePosition from "@/app/hooks/useRelativePosition";
|
||||
import { getCSSVar } from "@/app/utils";
|
||||
import { useMemo, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
const ArrowIcon = ({ color }: { color: string }) => {
|
||||
return (
|
||||
@@ -19,6 +21,20 @@ const ArrowIcon = ({ color }: { color: string }) => {
|
||||
};
|
||||
|
||||
const baseZIndex = 100;
|
||||
const popoverRootName = "popoverRoot";
|
||||
let popoverRoot = document.querySelector(
|
||||
`#${popoverRootName}`,
|
||||
) as HTMLDivElement;
|
||||
if (!popoverRoot) {
|
||||
popoverRoot = document.createElement("div");
|
||||
document.body.appendChild(popoverRoot);
|
||||
popoverRoot.style.height = "0px";
|
||||
popoverRoot.style.width = "100%";
|
||||
popoverRoot.style.position = "fixed";
|
||||
popoverRoot.style.bottom = "0";
|
||||
popoverRoot.style.zIndex = "100";
|
||||
popoverRoot.id = "popoverRootName";
|
||||
}
|
||||
|
||||
export default function Popover(props: {
|
||||
content?: JSX.Element | string;
|
||||
@@ -46,6 +62,21 @@ export default function Popover(props: {
|
||||
} = props;
|
||||
|
||||
const [internalShow, setShow] = useState(false);
|
||||
const { position, getRelativePosition } = useRelativePosition({
|
||||
delay: 0,
|
||||
});
|
||||
|
||||
const {
|
||||
distanceToBottomBoundary = 0,
|
||||
distanceToLeftBoundary = 0,
|
||||
distanceToRightBoundary = -10000,
|
||||
distanceToTopBoundary = 0,
|
||||
targetH = 0,
|
||||
targetW = 0,
|
||||
} = position?.poi || {};
|
||||
|
||||
let placementStyle: React.CSSProperties = {};
|
||||
const popoverCommonClass = `absolute p-2 box-border`;
|
||||
|
||||
const mergedShow = show ?? internalShow;
|
||||
|
||||
@@ -56,6 +87,13 @@ export default function Popover(props: {
|
||||
|
||||
switch (placement) {
|
||||
case "b":
|
||||
placementStyle = {
|
||||
top: `calc(-${distanceToBottomBoundary}px + 0.5rem)`,
|
||||
left: `calc(${distanceToLeftBoundary + targetW}px - ${
|
||||
targetW * 0.02
|
||||
}px)`,
|
||||
transform: "translateX(-50%)",
|
||||
};
|
||||
placementClassName =
|
||||
"top-[calc(100%+0.5rem)] left-[50%] translate-x-[calc(-50%)]";
|
||||
arrowClassName += "top-[calc(100%+0.5rem)] translate-y-[calc(-100%)]";
|
||||
@@ -67,32 +105,51 @@ export default function Popover(props: {
|
||||
// placementClassName = '';
|
||||
// break;
|
||||
case "rb":
|
||||
placementClassName = "top-[calc(100%+0.5rem)] translate-x-[calc(-2%)]";
|
||||
arrowClassName += "top-[calc(100%+0.5rem)] translate-y-[calc(-100%)]";
|
||||
break;
|
||||
case "lt":
|
||||
placementClassName =
|
||||
"bottom-[calc(100%+0.5rem)] left-[100%] translate-x-[calc(-98%)]";
|
||||
arrowClassName += "bottom-[calc(100%+0.5rem)] translate-y-[calc(100%)]";
|
||||
break;
|
||||
case "lb":
|
||||
placementClassName =
|
||||
"top-[calc(100%+0.5rem)] left-[100%] translate-x-[calc(-98%)]";
|
||||
placementStyle = {
|
||||
top: `calc(-${distanceToBottomBoundary}px + 0.5rem)`,
|
||||
right: `calc(${distanceToRightBoundary}px - ${targetW * 0.02}px)`,
|
||||
};
|
||||
placementClassName = "top-[calc(100%+0.5rem)] right-[calc(-2%)]";
|
||||
arrowClassName += "top-[calc(100%+0.5rem)] translate-y-[calc(-100%)]";
|
||||
break;
|
||||
case "rt":
|
||||
placementClassName = "bottom-[calc(100%+0.5rem)] translate-x-[calc(-2%)]";
|
||||
placementStyle = {
|
||||
bottom: `calc(${distanceToBottomBoundary + targetH}px + 0.5rem)`,
|
||||
right: `calc(${distanceToRightBoundary}px - ${targetW * 0.02}px)`,
|
||||
};
|
||||
placementClassName = "bottom-[calc(100%+0.5rem)] right-[calc(-2%)]";
|
||||
arrowClassName += "bottom-[calc(100%+0.5rem)] translate-y-[calc(100%)]";
|
||||
break;
|
||||
case "lt":
|
||||
placementStyle = {
|
||||
bottom: `calc(${distanceToBottomBoundary + targetH}px + 0.5rem)`,
|
||||
left: `calc(${distanceToLeftBoundary}px - ${targetW * 0.02}px)`,
|
||||
};
|
||||
placementClassName = "bottom-[calc(100%+0.5rem)] left-[calc(-2%)]";
|
||||
arrowClassName += "bottom-[calc(100%+0.5rem)] translate-y-[calc(100%)]";
|
||||
break;
|
||||
case "lb":
|
||||
placementStyle = {
|
||||
top: `calc(-${distanceToBottomBoundary}px + 0.5rem)`,
|
||||
left: `calc(${distanceToLeftBoundary}px - ${targetW * 0.02}px)`,
|
||||
};
|
||||
placementClassName = "top-[calc(100%+0.5rem)] left-[calc(-2%)]";
|
||||
arrowClassName += "top-[calc(100%+0.5rem)] translate-y-[calc(-100%)]";
|
||||
break;
|
||||
case "t":
|
||||
default:
|
||||
placementStyle = {
|
||||
bottom: `calc(${distanceToBottomBoundary + targetH}px + 0.5rem)`,
|
||||
left: `calc(${distanceToLeftBoundary + targetW}px - ${
|
||||
targetW * 0.02
|
||||
}px)`,
|
||||
transform: "translateX(-50%)",
|
||||
};
|
||||
placementClassName =
|
||||
"bottom-[calc(100%+0.5rem)] left-[50%] translate-x-[calc(-50%)]";
|
||||
arrowClassName += "bottom-[calc(100%+0.5rem)] translate-y-[calc(100%)]";
|
||||
}
|
||||
|
||||
const popoverCommonClass = "absolute p-2 box-border";
|
||||
|
||||
if (noArrow) {
|
||||
arrowClassName = "hidden";
|
||||
}
|
||||
@@ -109,6 +166,12 @@ export default function Popover(props: {
|
||||
e.preventDefault();
|
||||
onShow?.(!mergedShow);
|
||||
setShow(!mergedShow);
|
||||
if (!mergedShow) {
|
||||
getRelativePosition(e.currentTarget, "");
|
||||
window.document.documentElement.style.overflow = "hidden";
|
||||
} else {
|
||||
window.document.documentElement.style.overflow = "auto";
|
||||
}
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
@@ -119,12 +182,15 @@ export default function Popover(props: {
|
||||
<ArrowIcon color={internalBgColor} />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`${popoverCommonClass} ${placementClassName} ${popoverClassName}`}
|
||||
style={{ zIndex: baseZIndex + 1 }}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
{createPortal(
|
||||
<div
|
||||
className={`${popoverCommonClass} ${popoverClassName}`}
|
||||
style={{ zIndex: baseZIndex + 1, ...placementStyle }}
|
||||
>
|
||||
{content}
|
||||
</div>,
|
||||
popoverRoot,
|
||||
)}
|
||||
<div
|
||||
className=" fixed w-[100%] h-[100%] top-0 left-0 right-0 bottom-0"
|
||||
style={{ zIndex: baseZIndex }}
|
||||
|
104
app/components/Select/index.tsx
Normal file
104
app/components/Select/index.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import SelectIcon from "@/app/icons/downArrowIcon.svg";
|
||||
import Popover from "@/app/components/Popover";
|
||||
import React, { useContext, useMemo, useRef } from "react";
|
||||
import useRelativePosition, {
|
||||
Orientation,
|
||||
} from "@/app/hooks/useRelativePosition";
|
||||
import List from "@/app/components/List";
|
||||
|
||||
export type Option<Value> = {
|
||||
value: Value;
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
};
|
||||
|
||||
export interface SearchProps<Value> {
|
||||
value?: string;
|
||||
onSelect?: (v: Value) => void;
|
||||
options?: Option<Value>[];
|
||||
inMobile?: boolean;
|
||||
}
|
||||
|
||||
const Select = <Value extends number | string>(props: SearchProps<Value>) => {
|
||||
const { value, onSelect, options = [], inMobile } = props;
|
||||
|
||||
const { isMobileScreen, selectClassName } = useContext(List.ListContext);
|
||||
|
||||
const optionsRef = useRef<Option<Value>[]>([]);
|
||||
optionsRef.current = options;
|
||||
const selectedOption = useMemo(
|
||||
() => optionsRef.current.find((o) => o.value === value),
|
||||
[value],
|
||||
);
|
||||
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { position, getRelativePosition } = useRelativePosition({
|
||||
delay: 0,
|
||||
});
|
||||
|
||||
let headerH = 100;
|
||||
let baseH = position?.poi.distanceToBottomBoundary || 0;
|
||||
if (isMobileScreen) {
|
||||
headerH = 60;
|
||||
}
|
||||
if (position?.poi.relativePosition[1] === Orientation.bottom) {
|
||||
baseH = position?.poi.distanceToTopBoundary;
|
||||
}
|
||||
|
||||
const maxHeight = `${baseH - headerH}px`;
|
||||
|
||||
const content = (
|
||||
<div
|
||||
className={`px-2 py-2 flex flex-col gap-1 overflow-y-auto overflow-x-hidden`}
|
||||
style={{ maxHeight }}
|
||||
>
|
||||
{options?.map((o) => (
|
||||
<div
|
||||
key={o.value}
|
||||
className={`flex items-center p-3 gap-2 ${
|
||||
selectedOption?.value === o.value ? "bg-gray-100 rounded-md" : ""
|
||||
}`}
|
||||
onClick={() => {
|
||||
onSelect?.(o.value);
|
||||
}}
|
||||
>
|
||||
{!!o.icon && <div className="">{o.icon}</div>}
|
||||
<div className={`flex-1`}>{o.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
content={content}
|
||||
trigger="click"
|
||||
noArrow
|
||||
placement={
|
||||
position?.poi.relativePosition[1] !== Orientation.bottom ? "rb" : "rt"
|
||||
}
|
||||
popoverClassName="border-actions-popover border-gray-200 rounded-md shadow-actions-popover w-actions-popover bg-white"
|
||||
onShow={(e) => {
|
||||
getRelativePosition(contentRef.current!, "");
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center gap-3 py-2 px-3 bg-gray-100 rounded-action-btn font-time text-sm-title ${selectClassName}`}
|
||||
ref={contentRef}
|
||||
>
|
||||
<div className={`flex items-center gap-2 flex-1`}>
|
||||
{!!selectedOption?.icon && (
|
||||
<div className={``}>{selectedOption?.icon}</div>
|
||||
)}
|
||||
<div className={`flex-1`}>{selectedOption?.label}</div>
|
||||
</div>
|
||||
<div className={``}>
|
||||
<SelectIcon />
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default Select;
|
92
app/components/SlideRange/index.tsx
Normal file
92
app/components/SlideRange/index.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useContext, useEffect, useRef } from "react";
|
||||
import { ListContext } from "../List";
|
||||
|
||||
interface SlideRangeProps {
|
||||
className?: string;
|
||||
description?: string;
|
||||
range?: {
|
||||
start?: number;
|
||||
stroke?: number;
|
||||
};
|
||||
onSlide?: (v: number) => void;
|
||||
value?: number;
|
||||
step?: number;
|
||||
}
|
||||
|
||||
const margin = 15;
|
||||
|
||||
export default function SlideRange(props: SlideRangeProps) {
|
||||
const {
|
||||
className = "",
|
||||
description = "",
|
||||
range = {},
|
||||
value,
|
||||
onSlide,
|
||||
step,
|
||||
} = props;
|
||||
const { start = 0, stroke = 1 } = range;
|
||||
|
||||
const { rangeClassName, update } = useContext(ListContext);
|
||||
|
||||
const slideRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const transformToWidth = (x: number = start) => {
|
||||
const abs = x - start;
|
||||
const maxWidth = (slideRef.current?.clientWidth || 1) - margin * 2;
|
||||
const radio = stroke / maxWidth;
|
||||
return abs / radio;
|
||||
};
|
||||
|
||||
const setProperty = (value?: number) => {
|
||||
const initWidth = transformToWidth(value);
|
||||
slideRef.current?.style.setProperty(
|
||||
"--slide-value-size",
|
||||
`${initWidth + margin}px`,
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setProperty(value);
|
||||
update?.({ type: "range" });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col justify-center items-end gap-1 w-[100%] ${className} ${rangeClassName}`}
|
||||
>
|
||||
{!!description && (
|
||||
<div className="text-text-hint text-common text-sm">{description}</div>
|
||||
)}
|
||||
<div
|
||||
className="flex my-1.5 relative w-[100%] h-1.5 bg-gray-200 rounded-slide"
|
||||
ref={slideRef}
|
||||
>
|
||||
<div className="absolute top-0 h-[100%] w-[var(--slide-value-size)] pointer-events-none bg-gray-500 rounded-slide">
|
||||
|
||||
</div>
|
||||
<div
|
||||
className=" absolute w-[30px] top-[50%] translate-y-[-50%] left-[var(--slide-value-size)] translate-x-[-50%] pointer-events-none h-slide-btn leading-slide-btn text-sm-mobile text-center rounded-slide border-[1px] border-gray-300 bg-white"
|
||||
// onPointerDown={onPointerDown}
|
||||
>
|
||||
{value}
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
className="w-[100%] h-[100%] opacity-0"
|
||||
value={value}
|
||||
min={start}
|
||||
max={start + stroke}
|
||||
step={step}
|
||||
onChange={(e) => {
|
||||
setProperty(e.target.valueAsNumber);
|
||||
onSlide?.(e.target.valueAsNumber);
|
||||
}}
|
||||
style={{
|
||||
marginLeft: margin,
|
||||
marginRight: margin,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
27
app/components/Switch/index.tsx
Normal file
27
app/components/Switch/index.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import * as RadixSwitch from "@radix-ui/react-switch";
|
||||
import { useContext } from "react";
|
||||
import List from "../List";
|
||||
|
||||
interface SwitchProps {
|
||||
value: boolean;
|
||||
onChange: (v: boolean) => void;
|
||||
}
|
||||
|
||||
export default function Switch(props: SwitchProps) {
|
||||
const { value, onChange } = props;
|
||||
|
||||
const { switchClassName = "" } = useContext(List.ListContext);
|
||||
return (
|
||||
<RadixSwitch.Root
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
className={` flex w-switch h-switch bg-gray-200 p-0.5 box-content rounded-md ${switchClassName} ${
|
||||
value ? "bg-switch-checked justify-end" : "bg-gray-200 justify-start"
|
||||
}`}
|
||||
>
|
||||
<RadixSwitch.Thumb
|
||||
className={` bg-white block w-4 h-4 drop-shadow-sm rounded-md`}
|
||||
/>
|
||||
</RadixSwitch.Root>
|
||||
);
|
||||
}
|
@@ -6,6 +6,8 @@
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
|
||||
background-color: var(--white);
|
||||
|
||||
.auth-logo {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
@@ -33,4 +35,18 @@
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
||||
input[type="number"],
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
appearance: none;
|
||||
border-radius: 10px;
|
||||
border: var(--border-in-light);
|
||||
min-height: 36px;
|
||||
box-sizing: border-box;
|
||||
background: var(--white);
|
||||
color: var(--black);
|
||||
padding: 0 10px;
|
||||
max-width: 50%;
|
||||
font-family: inherit;
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user