mirror of
https://github.com/harivansh-afk/asap.it.git
synced 2026-04-16 14:01:05 +00:00
first commit
This commit is contained in:
commit
1cdbffff09
200 changed files with 30007 additions and 0 deletions
18
app/components/ui/BackgroundRays/index.tsx
Normal file
18
app/components/ui/BackgroundRays/index.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import styles from './styles.module.scss';
|
||||
|
||||
const BackgroundRays = () => {
|
||||
return (
|
||||
<div className={`${styles.rayContainer} `}>
|
||||
<div className={`${styles.lightRay} ${styles.ray1}`}></div>
|
||||
<div className={`${styles.lightRay} ${styles.ray2}`}></div>
|
||||
<div className={`${styles.lightRay} ${styles.ray3}`}></div>
|
||||
<div className={`${styles.lightRay} ${styles.ray4}`}></div>
|
||||
<div className={`${styles.lightRay} ${styles.ray5}`}></div>
|
||||
<div className={`${styles.lightRay} ${styles.ray6}`}></div>
|
||||
<div className={`${styles.lightRay} ${styles.ray7}`}></div>
|
||||
<div className={`${styles.lightRay} ${styles.ray8}`}></div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default BackgroundRays;
|
||||
246
app/components/ui/BackgroundRays/styles.module.scss
Normal file
246
app/components/ui/BackgroundRays/styles.module.scss
Normal file
|
|
@ -0,0 +1,246 @@
|
|||
.rayContainer {
|
||||
// Theme-specific colors
|
||||
--ray-color-primary: color-mix(in srgb, var(--primary-color), transparent 30%);
|
||||
--ray-color-secondary: color-mix(in srgb, var(--secondary-color), transparent 30%);
|
||||
--ray-color-accent: color-mix(in srgb, var(--accent-color), transparent 30%);
|
||||
|
||||
// Theme-specific gradients
|
||||
--ray-gradient-primary: radial-gradient(var(--ray-color-primary) 0%, transparent 70%);
|
||||
--ray-gradient-secondary: radial-gradient(var(--ray-color-secondary) 0%, transparent 70%);
|
||||
--ray-gradient-accent: radial-gradient(var(--ray-color-accent) 0%, transparent 70%);
|
||||
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
animation: fadeIn 1.5s ease-out;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
// background-color: transparent;
|
||||
|
||||
:global(html[data-theme='dark']) & {
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
:global(html[data-theme='light']) & {
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
}
|
||||
|
||||
.lightRay {
|
||||
position: absolute;
|
||||
border-radius: 100%;
|
||||
|
||||
:global(html[data-theme='dark']) & {
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
:global(html[data-theme='light']) & {
|
||||
mix-blend-mode: multiply;
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.ray1 {
|
||||
width: 600px;
|
||||
height: 800px;
|
||||
background: var(--ray-gradient-primary);
|
||||
transform: rotate(65deg);
|
||||
top: -500px;
|
||||
left: -100px;
|
||||
filter: blur(80px);
|
||||
opacity: 0.6;
|
||||
animation: float1 15s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.ray2 {
|
||||
width: 400px;
|
||||
height: 600px;
|
||||
background: var(--ray-gradient-secondary);
|
||||
transform: rotate(-30deg);
|
||||
top: -300px;
|
||||
left: 200px;
|
||||
filter: blur(60px);
|
||||
opacity: 0.6;
|
||||
animation: float2 18s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.ray3 {
|
||||
width: 500px;
|
||||
height: 400px;
|
||||
background: var(--ray-gradient-accent);
|
||||
top: -320px;
|
||||
left: 500px;
|
||||
filter: blur(65px);
|
||||
opacity: 0.5;
|
||||
animation: float3 20s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.ray4 {
|
||||
width: 400px;
|
||||
height: 450px;
|
||||
background: var(--ray-gradient-secondary);
|
||||
top: -350px;
|
||||
left: 800px;
|
||||
filter: blur(55px);
|
||||
opacity: 0.55;
|
||||
animation: float4 17s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.ray5 {
|
||||
width: 350px;
|
||||
height: 500px;
|
||||
background: var(--ray-gradient-primary);
|
||||
transform: rotate(-45deg);
|
||||
top: -250px;
|
||||
left: 1000px;
|
||||
filter: blur(45px);
|
||||
opacity: 0.6;
|
||||
animation: float5 16s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.ray6 {
|
||||
width: 300px;
|
||||
height: 700px;
|
||||
background: var(--ray-gradient-accent);
|
||||
transform: rotate(75deg);
|
||||
top: -400px;
|
||||
left: 600px;
|
||||
filter: blur(75px);
|
||||
opacity: 0.45;
|
||||
animation: float6 19s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.ray7 {
|
||||
width: 450px;
|
||||
height: 600px;
|
||||
background: var(--ray-gradient-primary);
|
||||
transform: rotate(45deg);
|
||||
top: -450px;
|
||||
left: 350px;
|
||||
filter: blur(65px);
|
||||
opacity: 0.55;
|
||||
animation: float7 21s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.ray8 {
|
||||
width: 380px;
|
||||
height: 550px;
|
||||
background: var(--ray-gradient-secondary);
|
||||
transform: rotate(-60deg);
|
||||
top: -380px;
|
||||
left: 750px;
|
||||
filter: blur(58px);
|
||||
opacity: 0.6;
|
||||
animation: float8 14s infinite ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes float1 {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(65deg) translate(0, 0);
|
||||
}
|
||||
25% {
|
||||
transform: rotate(70deg) translate(30px, 20px);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(60deg) translate(-20px, 40px);
|
||||
}
|
||||
75% {
|
||||
transform: rotate(68deg) translate(-40px, 10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float2 {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(-30deg) scale(1);
|
||||
}
|
||||
33% {
|
||||
transform: rotate(-25deg) scale(1.1);
|
||||
}
|
||||
66% {
|
||||
transform: rotate(-35deg) scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float3 {
|
||||
0%,
|
||||
100% {
|
||||
transform: translate(0, 0) rotate(0deg);
|
||||
}
|
||||
25% {
|
||||
transform: translate(40px, 20px) rotate(5deg);
|
||||
}
|
||||
75% {
|
||||
transform: translate(-30px, 40px) rotate(-5deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float4 {
|
||||
0%,
|
||||
100% {
|
||||
transform: scale(1) rotate(0deg);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.15) rotate(10deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float5 {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(-45deg) translate(0, 0);
|
||||
}
|
||||
33% {
|
||||
transform: rotate(-40deg) translate(25px, -20px);
|
||||
}
|
||||
66% {
|
||||
transform: rotate(-50deg) translate(-25px, 20px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float6 {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(75deg) scale(1);
|
||||
filter: blur(75px);
|
||||
}
|
||||
50% {
|
||||
transform: rotate(85deg) scale(1.1);
|
||||
filter: blur(65px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float7 {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(45deg) translate(0, 0);
|
||||
opacity: 0.55;
|
||||
}
|
||||
50% {
|
||||
transform: rotate(40deg) translate(-30px, 30px);
|
||||
opacity: 0.65;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float8 {
|
||||
0%,
|
||||
100% {
|
||||
transform: rotate(-60deg) scale(1);
|
||||
}
|
||||
25% {
|
||||
transform: rotate(-55deg) scale(1.05);
|
||||
}
|
||||
75% {
|
||||
transform: rotate(-65deg) scale(0.95);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
133
app/components/ui/Dialog.tsx
Normal file
133
app/components/ui/Dialog.tsx
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import * as RadixDialog from '@radix-ui/react-dialog';
|
||||
import { motion, type Variants } from 'framer-motion';
|
||||
import React, { memo, type ReactNode } from 'react';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { cubicEasingFn } from '~/utils/easings';
|
||||
import { IconButton } from './IconButton';
|
||||
|
||||
export { Close as DialogClose, Root as DialogRoot } from '@radix-ui/react-dialog';
|
||||
|
||||
const transition = {
|
||||
duration: 0.15,
|
||||
ease: cubicEasingFn,
|
||||
};
|
||||
|
||||
export const dialogBackdropVariants = {
|
||||
closed: {
|
||||
opacity: 0,
|
||||
transition,
|
||||
},
|
||||
open: {
|
||||
opacity: 1,
|
||||
transition,
|
||||
},
|
||||
} satisfies Variants;
|
||||
|
||||
export const dialogVariants = {
|
||||
closed: {
|
||||
x: '-50%',
|
||||
y: '-40%',
|
||||
scale: 0.96,
|
||||
opacity: 0,
|
||||
transition,
|
||||
},
|
||||
open: {
|
||||
x: '-50%',
|
||||
y: '-50%',
|
||||
scale: 1,
|
||||
opacity: 1,
|
||||
transition,
|
||||
},
|
||||
} satisfies Variants;
|
||||
|
||||
interface DialogButtonProps {
|
||||
type: 'primary' | 'secondary' | 'danger';
|
||||
children: ReactNode;
|
||||
onClick?: (event: React.UIEvent) => void;
|
||||
}
|
||||
|
||||
export const DialogButton = memo(({ type, children, onClick }: DialogButtonProps) => {
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
'inline-flex h-[35px] items-center justify-center rounded-lg px-4 text-sm leading-none focus:outline-none',
|
||||
{
|
||||
'bg-bolt-elements-button-primary-background text-bolt-elements-button-primary-text hover:bg-bolt-elements-button-primary-backgroundHover':
|
||||
type === 'primary',
|
||||
'bg-bolt-elements-button-secondary-background text-bolt-elements-button-secondary-text hover:bg-bolt-elements-button-secondary-backgroundHover':
|
||||
type === 'secondary',
|
||||
'bg-bolt-elements-button-danger-background text-bolt-elements-button-danger-text hover:bg-bolt-elements-button-danger-backgroundHover':
|
||||
type === 'danger',
|
||||
},
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
export const DialogTitle = memo(({ className, children, ...props }: RadixDialog.DialogTitleProps) => {
|
||||
return (
|
||||
<RadixDialog.Title
|
||||
className={classNames(
|
||||
'px-5 py-4 flex items-center justify-between border-b border-bolt-elements-borderColor text-lg font-semibold leading-6 text-bolt-elements-textPrimary',
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</RadixDialog.Title>
|
||||
);
|
||||
});
|
||||
|
||||
export const DialogDescription = memo(({ className, children, ...props }: RadixDialog.DialogDescriptionProps) => {
|
||||
return (
|
||||
<RadixDialog.Description
|
||||
className={classNames('px-5 py-4 text-bolt-elements-textPrimary text-md', className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</RadixDialog.Description>
|
||||
);
|
||||
});
|
||||
|
||||
interface DialogProps {
|
||||
children: ReactNode | ReactNode[];
|
||||
className?: string;
|
||||
onBackdrop?: (event: React.UIEvent) => void;
|
||||
onClose?: (event: React.UIEvent) => void;
|
||||
}
|
||||
|
||||
export const Dialog = memo(({ className, children, onBackdrop, onClose }: DialogProps) => {
|
||||
return (
|
||||
<RadixDialog.Portal>
|
||||
<RadixDialog.Overlay onClick={onBackdrop} asChild>
|
||||
<motion.div
|
||||
className="bg-black/50 fixed inset-0 z-max"
|
||||
initial="closed"
|
||||
animate="open"
|
||||
exit="closed"
|
||||
variants={dialogBackdropVariants}
|
||||
/>
|
||||
</RadixDialog.Overlay>
|
||||
<RadixDialog.Content asChild>
|
||||
<motion.div
|
||||
className={classNames(
|
||||
'fixed top-[50%] left-[50%] z-max max-h-[85vh] w-[90vw] max-w-[450px] translate-x-[-50%] translate-y-[-50%] border border-bolt-elements-borderColor rounded-lg bg-bolt-elements-background-depth-2 shadow-lg focus:outline-none overflow-hidden',
|
||||
className,
|
||||
)}
|
||||
initial="closed"
|
||||
animate="open"
|
||||
exit="closed"
|
||||
variants={dialogVariants}
|
||||
>
|
||||
{children}
|
||||
<RadixDialog.Close asChild onClick={onClose}>
|
||||
<IconButton icon="i-ph:x" className="absolute top-[10px] right-[10px]" />
|
||||
</RadixDialog.Close>
|
||||
</motion.div>
|
||||
</RadixDialog.Content>
|
||||
</RadixDialog.Portal>
|
||||
);
|
||||
});
|
||||
77
app/components/ui/IconButton.tsx
Normal file
77
app/components/ui/IconButton.tsx
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { memo } from 'react';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
|
||||
type IconSize = 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
|
||||
|
||||
interface BaseIconButtonProps {
|
||||
size?: IconSize;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
disabledClassName?: string;
|
||||
title?: string;
|
||||
disabled?: boolean;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
}
|
||||
|
||||
type IconButtonWithoutChildrenProps = {
|
||||
icon: string;
|
||||
children?: undefined;
|
||||
} & BaseIconButtonProps;
|
||||
|
||||
type IconButtonWithChildrenProps = {
|
||||
icon?: undefined;
|
||||
children: string | JSX.Element | JSX.Element[];
|
||||
} & BaseIconButtonProps;
|
||||
|
||||
type IconButtonProps = IconButtonWithoutChildrenProps | IconButtonWithChildrenProps;
|
||||
|
||||
export const IconButton = memo(
|
||||
({
|
||||
icon,
|
||||
size = 'xl',
|
||||
className,
|
||||
iconClassName,
|
||||
disabledClassName,
|
||||
disabled = false,
|
||||
title,
|
||||
onClick,
|
||||
children,
|
||||
}: IconButtonProps) => {
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
'flex items-center text-bolt-elements-item-contentDefault bg-transparent enabled:hover:text-bolt-elements-item-contentActive rounded-md p-1 enabled:hover:bg-bolt-elements-item-backgroundActive disabled:cursor-not-allowed',
|
||||
{
|
||||
[classNames('opacity-30', disabledClassName)]: disabled,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
title={title}
|
||||
disabled={disabled}
|
||||
onClick={(event) => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClick?.(event);
|
||||
}}
|
||||
>
|
||||
{children ? children : <div className={classNames(icon, getIconSize(size), iconClassName)}></div>}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function getIconSize(size: IconSize) {
|
||||
if (size === 'sm') {
|
||||
return 'text-sm';
|
||||
} else if (size === 'md') {
|
||||
return 'text-md';
|
||||
} else if (size === 'lg') {
|
||||
return 'text-lg';
|
||||
} else if (size === 'xl') {
|
||||
return 'text-xl';
|
||||
} else {
|
||||
return 'text-2xl';
|
||||
}
|
||||
}
|
||||
27
app/components/ui/LoadingDots.tsx
Normal file
27
app/components/ui/LoadingDots.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { memo, useEffect, useState } from 'react';
|
||||
|
||||
interface LoadingDotsProps {
|
||||
text: string;
|
||||
}
|
||||
|
||||
export const LoadingDots = memo(({ text }: LoadingDotsProps) => {
|
||||
const [dotCount, setDotCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setDotCount((prevDotCount) => (prevDotCount + 1) % 4);
|
||||
}, 500);
|
||||
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center h-full">
|
||||
<div className="relative">
|
||||
<span>{text}</span>
|
||||
<span className="absolute left-[calc(100%-12px)]">{'.'.repeat(dotCount)}</span>
|
||||
<span className="invisible">...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
20
app/components/ui/PanelHeader.tsx
Normal file
20
app/components/ui/PanelHeader.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { memo } from 'react';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
|
||||
interface PanelHeaderProps {
|
||||
className?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const PanelHeader = memo(({ className, children }: PanelHeaderProps) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'flex items-center gap-2 bg-bolt-elements-background-depth-2 text-bolt-elements-textSecondary border-b border-bolt-elements-borderColor px-4 py-1 min-h-[34px] text-sm',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
36
app/components/ui/PanelHeaderButton.tsx
Normal file
36
app/components/ui/PanelHeaderButton.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { memo } from 'react';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
|
||||
interface PanelHeaderButtonProps {
|
||||
className?: string;
|
||||
disabledClassName?: string;
|
||||
disabled?: boolean;
|
||||
children: string | JSX.Element | Array<JSX.Element | string>;
|
||||
onClick?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
|
||||
}
|
||||
|
||||
export const PanelHeaderButton = memo(
|
||||
({ className, disabledClassName, disabled = false, children, onClick }: PanelHeaderButtonProps) => {
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
'flex items-center shrink-0 gap-1.5 px-1.5 rounded-md py-0.5 text-bolt-elements-item-contentDefault bg-transparent enabled:hover:text-bolt-elements-item-contentActive enabled:hover:bg-bolt-elements-item-backgroundActive disabled:cursor-not-allowed',
|
||||
{
|
||||
[classNames('opacity-30', disabledClassName)]: disabled,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={(event) => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClick?.(event);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
},
|
||||
);
|
||||
17
app/components/ui/SettingsButton.tsx
Normal file
17
app/components/ui/SettingsButton.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { memo } from 'react';
|
||||
import { IconButton } from '~/components/ui/IconButton';
|
||||
interface SettingsButtonProps {
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
export const SettingsButton = memo(({ onClick }: SettingsButtonProps) => {
|
||||
return (
|
||||
<IconButton
|
||||
onClick={onClick}
|
||||
icon="i-ph:gear"
|
||||
size="xl"
|
||||
title="Settings"
|
||||
className="text-[#666] hover:text-bolt-elements-textPrimary hover:bg-bolt-elements-item-backgroundActive/10 transition-colors"
|
||||
/>
|
||||
);
|
||||
});
|
||||
65
app/components/ui/Slider.tsx
Normal file
65
app/components/ui/Slider.tsx
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { motion } from 'framer-motion';
|
||||
import { memo } from 'react';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
import { cubicEasingFn } from '~/utils/easings';
|
||||
import { genericMemo } from '~/utils/react';
|
||||
|
||||
interface SliderOption<T> {
|
||||
value: T;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface SliderOptions<T> {
|
||||
left: SliderOption<T>;
|
||||
right: SliderOption<T>;
|
||||
}
|
||||
|
||||
interface SliderProps<T> {
|
||||
selected: T;
|
||||
options: SliderOptions<T>;
|
||||
setSelected?: (selected: T) => void;
|
||||
}
|
||||
|
||||
export const Slider = genericMemo(<T,>({ selected, options, setSelected }: SliderProps<T>) => {
|
||||
const isLeftSelected = selected === options.left.value;
|
||||
|
||||
return (
|
||||
<div className="flex items-center flex-wrap shrink-0 gap-1 bg-bolt-elements-background-depth-1 overflow-hidden rounded-full p-1">
|
||||
<SliderButton selected={isLeftSelected} setSelected={() => setSelected?.(options.left.value)}>
|
||||
{options.left.text}
|
||||
</SliderButton>
|
||||
<SliderButton selected={!isLeftSelected} setSelected={() => setSelected?.(options.right.value)}>
|
||||
{options.right.text}
|
||||
</SliderButton>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
interface SliderButtonProps {
|
||||
selected: boolean;
|
||||
children: string | JSX.Element | Array<JSX.Element | string>;
|
||||
setSelected: () => void;
|
||||
}
|
||||
|
||||
const SliderButton = memo(({ selected, children, setSelected }: SliderButtonProps) => {
|
||||
return (
|
||||
<button
|
||||
onClick={setSelected}
|
||||
className={classNames(
|
||||
'bg-transparent text-sm px-2.5 py-0.5 rounded-full relative',
|
||||
selected
|
||||
? 'text-bolt-elements-item-contentAccent'
|
||||
: 'text-bolt-elements-item-contentDefault hover:text-bolt-elements-item-contentActive',
|
||||
)}
|
||||
>
|
||||
<span className="relative z-10">{children}</span>
|
||||
{selected && (
|
||||
<motion.span
|
||||
layoutId="pill-tab"
|
||||
transition={{ duration: 0.2, ease: cubicEasingFn }}
|
||||
className="absolute inset-0 z-0 bg-bolt-elements-item-backgroundAccent rounded-full"
|
||||
></motion.span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
});
|
||||
37
app/components/ui/Switch.tsx
Normal file
37
app/components/ui/Switch.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { memo } from 'react';
|
||||
import * as SwitchPrimitive from '@radix-ui/react-switch';
|
||||
import { classNames } from '~/utils/classNames';
|
||||
|
||||
interface SwitchProps {
|
||||
className?: string;
|
||||
checked?: boolean;
|
||||
onCheckedChange?: (event: boolean) => void;
|
||||
}
|
||||
|
||||
export const Switch = memo(({ className, onCheckedChange, checked }: SwitchProps) => {
|
||||
return (
|
||||
<SwitchPrimitive.Root
|
||||
className={classNames(
|
||||
'relative h-6 w-11 cursor-pointer rounded-full bg-bolt-elements-button-primary-background',
|
||||
'transition-colors duration-200 ease-in-out',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
'data-[state=checked]:bg-bolt-elements-item-contentAccent',
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
onCheckedChange={(e) => onCheckedChange?.(e)}
|
||||
>
|
||||
<SwitchPrimitive.Thumb
|
||||
className={classNames(
|
||||
'block h-5 w-5 rounded-full bg-white',
|
||||
'shadow-lg shadow-black/20',
|
||||
'transition-transform duration-200 ease-in-out',
|
||||
'translate-x-0.5',
|
||||
'data-[state=checked]:translate-x-[1.375rem]',
|
||||
'will-change-transform',
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitive.Root>
|
||||
);
|
||||
});
|
||||
29
app/components/ui/ThemeSwitch.tsx
Normal file
29
app/components/ui/ThemeSwitch.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { useStore } from '@nanostores/react';
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { themeStore, toggleTheme } from '~/lib/stores/theme';
|
||||
import { IconButton } from './IconButton';
|
||||
|
||||
interface ThemeSwitchProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ThemeSwitch = memo(({ className }: ThemeSwitchProps) => {
|
||||
const theme = useStore(themeStore);
|
||||
const [domLoaded, setDomLoaded] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setDomLoaded(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
domLoaded && (
|
||||
<IconButton
|
||||
className={className}
|
||||
icon={theme === 'dark' ? 'i-ph-sun-dim-duotone' : 'i-ph-moon-stars-duotone'}
|
||||
size="xl"
|
||||
title="Toggle Theme"
|
||||
onClick={toggleTheme}
|
||||
/>
|
||||
)
|
||||
);
|
||||
});
|
||||
73
app/components/ui/Tooltip.tsx
Normal file
73
app/components/ui/Tooltip.tsx
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import * as Tooltip from '@radix-ui/react-tooltip';
|
||||
|
||||
interface TooltipProps {
|
||||
tooltip: React.ReactNode;
|
||||
children: React.ReactNode;
|
||||
sideOffset?: number;
|
||||
className?: string;
|
||||
arrowClassName?: string;
|
||||
tooltipStyle?: React.CSSProperties;
|
||||
position?: 'top' | 'bottom' | 'left' | 'right';
|
||||
maxWidth?: number;
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
const WithTooltip = ({
|
||||
tooltip,
|
||||
children,
|
||||
sideOffset = 5,
|
||||
className = '',
|
||||
arrowClassName = '',
|
||||
tooltipStyle = {},
|
||||
position = 'top',
|
||||
maxWidth = 250,
|
||||
delay = 0,
|
||||
}: TooltipProps) => {
|
||||
return (
|
||||
<Tooltip.Root delayDuration={delay}>
|
||||
<Tooltip.Trigger asChild>{children}</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
side={position}
|
||||
className={`
|
||||
z-[2000]
|
||||
px-2.5
|
||||
py-1.5
|
||||
max-h-[300px]
|
||||
select-none
|
||||
rounded-md
|
||||
bg-bolt-elements-background-depth-3
|
||||
text-bolt-elements-textPrimary
|
||||
text-sm
|
||||
leading-tight
|
||||
shadow-lg
|
||||
animate-in
|
||||
fade-in-0
|
||||
zoom-in-95
|
||||
data-[state=closed]:animate-out
|
||||
data-[state=closed]:fade-out-0
|
||||
data-[state=closed]:zoom-out-95
|
||||
${className}
|
||||
`}
|
||||
sideOffset={sideOffset}
|
||||
style={{
|
||||
maxWidth,
|
||||
...tooltipStyle,
|
||||
}}
|
||||
>
|
||||
<div className="break-words">{tooltip}</div>
|
||||
<Tooltip.Arrow
|
||||
className={`
|
||||
fill-bolt-elements-background-depth-3
|
||||
${arrowClassName}
|
||||
`}
|
||||
width={12}
|
||||
height={6}
|
||||
/>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
);
|
||||
};
|
||||
|
||||
export default WithTooltip;
|
||||
Loading…
Add table
Add a link
Reference in a new issue