mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-02 10:53:33 +00:00
Migrate to Mantine v8 and Design Changes (#961)
* mantine v8 migration * various design changes and improvements
This commit is contained in:
parent
bea55d48a8
commit
c1330d92b2
473 changed files with 12469 additions and 11607 deletions
12
src/shared/components/accordion/accordion.module.css
Normal file
12
src/shared/components/accordion/accordion.module.css
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
.panel {
|
||||
background: var(--theme-colors-background);
|
||||
}
|
||||
|
||||
.control {
|
||||
background: var(--theme-colors-background);
|
||||
}
|
||||
|
||||
.chevron {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
41
src/shared/components/accordion/accordion.tsx
Normal file
41
src/shared/components/accordion/accordion.tsx
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import {
|
||||
Accordion as MantineAccordion,
|
||||
AccordionProps as MantineAccordionProps,
|
||||
} from '@mantine/core';
|
||||
|
||||
import styles from './accordion.module.css';
|
||||
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
|
||||
export interface AccordionProps
|
||||
extends Omit<MantineAccordionProps, 'defaultValue' | 'multiple' | 'onChange'> {
|
||||
defaultValue?: string | string[];
|
||||
multiple?: boolean;
|
||||
onChange?: (value: null | string | string[]) => void;
|
||||
}
|
||||
|
||||
export const Accordion = ({ children, classNames, ...props }: AccordionProps) => {
|
||||
return (
|
||||
<MantineAccordion
|
||||
chevron={
|
||||
<Icon
|
||||
icon="arrowUpS"
|
||||
size="lg"
|
||||
/>
|
||||
}
|
||||
classNames={{
|
||||
chevron: styles.chevron,
|
||||
control: styles.control,
|
||||
panel: styles.panel,
|
||||
...classNames,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MantineAccordion>
|
||||
);
|
||||
};
|
||||
|
||||
Accordion.Control = MantineAccordion.Control;
|
||||
Accordion.Item = MantineAccordion.Item;
|
||||
Accordion.Panel = MantineAccordion.Panel;
|
||||
92
src/shared/components/action-icon/action-icon.module.css
Normal file
92
src/shared/components/action-icon/action-icon.module.css
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
.root {
|
||||
--ai-size-xs: calc(1.875rem * var(--mantine-scale));
|
||||
--ai-size-sm: calc(2.25rem * var(--mantine-scale));
|
||||
--ai-size-md: calc(2.625rem * var(--mantine-scale));
|
||||
--ai-size-lg: calc(3.125rem * var(--mantine-scale));
|
||||
--ai-size-xl: calc(3.75rem * var(--mantine-scale));
|
||||
|
||||
font-weight: 500;
|
||||
transition:
|
||||
background-color 0.2s ease-in-out,
|
||||
border-color 0.2s ease-in-out;
|
||||
|
||||
&[data-disabled='true'] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&[data-variant='default'] {
|
||||
color: var(--theme-colors-foreground);
|
||||
background: var(--theme-colors-surface);
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
background: lighten(var(--theme-colors-surface), 5%);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
background: lighten(var(--theme-colors-surface), 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant='outline'] {
|
||||
--button-border: var(--theme-colors-border);
|
||||
|
||||
color: var(--theme-colors-foreground);
|
||||
border: 1px solid var(--theme-colors-border);
|
||||
|
||||
&:hover {
|
||||
border: 1px solid lighten(var(--theme-colors-border), 10%);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
border: 1px solid lighten(var(--theme-colors-border), 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant='filled'] {
|
||||
color: var(--theme-colors-primary-contrast);
|
||||
background: var(--theme-colors-primary-filled);
|
||||
border: 1px solid transparent;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background: darken(var(--theme-colors-primary-filled), 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant='subtle'] {
|
||||
color: var(--theme-colors-foreground);
|
||||
background: transparent;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background: lighten(var(--theme-colors-surface), 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant='secondary'] {
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
background: darken(var(--theme-colors-surface), 5%);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
background: darken(var(--theme-colors-surface), 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant='transparent'] {
|
||||
color: var(--theme-colors-foreground);
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
border: 1px solid lighten(var(--theme-colors-border), 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
110
src/shared/components/action-icon/action-icon.tsx
Normal file
110
src/shared/components/action-icon/action-icon.tsx
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
import {
|
||||
ElementProps,
|
||||
ActionIcon as MantineActionIcon,
|
||||
ActionIconProps as MantineActionIconProps,
|
||||
} from '@mantine/core';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import styles from './action-icon.module.css';
|
||||
|
||||
import { AppIcon, Icon, IconProps } from '/@/shared/components/icon/icon';
|
||||
import { Tooltip, TooltipProps } from '/@/shared/components/tooltip/tooltip';
|
||||
import { createPolymorphicComponent } from '/@/shared/utils/create-polymorphic-component';
|
||||
|
||||
export interface ActionIconProps
|
||||
extends ElementProps<'button', keyof MantineActionIconProps>,
|
||||
MantineActionIconProps {
|
||||
icon?: keyof typeof AppIcon;
|
||||
iconProps?: Omit<IconProps, 'icon'>;
|
||||
tooltip?: Omit<TooltipProps, 'children'>;
|
||||
}
|
||||
|
||||
const _ActionIcon = forwardRef<HTMLButtonElement, ActionIconProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
classNames,
|
||||
icon,
|
||||
iconProps,
|
||||
size = 'sm',
|
||||
tooltip,
|
||||
variant = 'default',
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const actionIconProps: ActionIconProps = {
|
||||
classNames: {
|
||||
root: styles.root,
|
||||
...classNames,
|
||||
},
|
||||
size,
|
||||
variant,
|
||||
...props,
|
||||
};
|
||||
|
||||
if (tooltip && icon) {
|
||||
return (
|
||||
<Tooltip
|
||||
withinPortal
|
||||
{...tooltip}
|
||||
>
|
||||
<MantineActionIcon
|
||||
ref={ref}
|
||||
{...actionIconProps}
|
||||
>
|
||||
<Icon
|
||||
icon={icon}
|
||||
size={actionIconProps.size}
|
||||
{...iconProps}
|
||||
/>
|
||||
</MantineActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
if (icon) {
|
||||
return (
|
||||
<MantineActionIcon
|
||||
ref={ref}
|
||||
{...actionIconProps}
|
||||
>
|
||||
<Icon
|
||||
icon={icon}
|
||||
size={actionIconProps.size}
|
||||
{...iconProps}
|
||||
/>
|
||||
</MantineActionIcon>
|
||||
);
|
||||
}
|
||||
|
||||
if (tooltip) {
|
||||
return (
|
||||
<Tooltip
|
||||
withinPortal
|
||||
{...tooltip}
|
||||
>
|
||||
<MantineActionIcon
|
||||
ref={ref}
|
||||
{...actionIconProps}
|
||||
>
|
||||
{children}
|
||||
</MantineActionIcon>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MantineActionIcon
|
||||
ref={ref}
|
||||
{...actionIconProps}
|
||||
>
|
||||
{children}
|
||||
</MantineActionIcon>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const ActionIcon = createPolymorphicComponent<'button', ActionIconProps>(_ActionIcon);
|
||||
export const ActionIconGroup = MantineActionIcon.Group;
|
||||
export const ActionIconSection = MantineActionIcon.GroupSection;
|
||||
13
src/shared/components/badge/badge.module.css
Normal file
13
src/shared/components/badge/badge.module.css
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
.root[data-variant='filled'] {
|
||||
background: var(--theme-colors-primary-filled);
|
||||
}
|
||||
|
||||
.root[data-variant='outline'] {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.root {
|
||||
padding: var(--theme-spacing-xs) var(--theme-spacing-sm);
|
||||
font-weight: 500;
|
||||
background: var(--theme-colors-surface);
|
||||
}
|
||||
28
src/shared/components/badge/badge.tsx
Normal file
28
src/shared/components/badge/badge.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import {
|
||||
ElementProps,
|
||||
Badge as MantineBadge,
|
||||
BadgeProps as MantineBadgeProps,
|
||||
} from '@mantine/core';
|
||||
|
||||
import styles from './badge.module.css';
|
||||
|
||||
import { createPolymorphicComponent } from '/@/shared/utils/create-polymorphic-component';
|
||||
|
||||
export interface BadgeProps
|
||||
extends ElementProps<'div', keyof MantineBadgeProps>,
|
||||
MantineBadgeProps {}
|
||||
|
||||
const _Badge = ({ children, classNames, variant = 'default', ...props }: BadgeProps) => {
|
||||
return (
|
||||
<MantineBadge
|
||||
classNames={{ root: styles.root, ...classNames }}
|
||||
radius="md"
|
||||
variant={variant}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MantineBadge>
|
||||
);
|
||||
};
|
||||
|
||||
export const Badge = createPolymorphicComponent<'button', BadgeProps>(_Badge);
|
||||
7
src/shared/components/box/box.tsx
Normal file
7
src/shared/components/box/box.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { ElementProps, Box as MantineBox, BoxProps as MantineBoxProps } from '@mantine/core';
|
||||
|
||||
export interface BoxProps extends ElementProps<'div', keyof MantineBoxProps>, MantineBoxProps {}
|
||||
|
||||
export const Box = ({ children, ...props }: BoxProps) => {
|
||||
return <MantineBox {...props}>{children}</MantineBox>;
|
||||
};
|
||||
156
src/shared/components/button/button.module.css
Normal file
156
src/shared/components/button/button.module.css
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
.root {
|
||||
font-weight: 500;
|
||||
border: 1px solid transparent;
|
||||
transition:
|
||||
background-color 0.2s ease-in-out,
|
||||
border-color 0.2s ease-in-out;
|
||||
|
||||
&[data-disabled='true'] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
&[data-variant='default'] {
|
||||
color: var(--theme-colors-foreground);
|
||||
background-color: var(--theme-colors-surface);
|
||||
|
||||
&:hover {
|
||||
background: lighten(var(--theme-colors-surface), 5%);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
background: lighten(var(--theme-colors-surface), 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant='outline'] {
|
||||
--button-border: var(--theme-colors-border);
|
||||
|
||||
color: var(--theme-colors-foreground);
|
||||
border: 1px solid var(--theme-colors-border);
|
||||
|
||||
&:hover {
|
||||
border: 1px solid lighten(var(--theme-colors-border), 10%);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
border: 1px solid lighten(var(--theme-colors-border), 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant='filled'] {
|
||||
color: var(--theme-colors-primary-contrast);
|
||||
background: var(--theme-colors-primary-filled);
|
||||
border: 1px solid transparent;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background: darken(var(--theme-colors-primary-filled), 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant='state-error'] {
|
||||
background: var(--theme-colors-state-error);
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background: darken(var(--theme-colors-state-error), 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant='state-info'] {
|
||||
background: var(--theme-colors-state-info);
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background: darken(var(--theme-colors-state-info), 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant='state-success'] {
|
||||
background: var(--theme-colors-state-success);
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background: darken(var(--theme-colors-state-success), 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant='state-warning'] {
|
||||
background: var(--theme-colors-state-warning);
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background: darken(var(--theme-colors-state-warning), 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant='subtle'] {
|
||||
color: var(--theme-colors-foreground);
|
||||
background: transparent;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus-visible {
|
||||
background-color: lighten(var(--button-bg), 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant='secondary'] {
|
||||
border: 1px solid transparent;
|
||||
|
||||
&:hover {
|
||||
background-color: darken(var(--button-bg), 5%);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
background-color: darken(var(--button-bg), 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&[data-variant='transparent'] {
|
||||
color: var(--theme-colors-foreground);
|
||||
border: 1px solid transparent;
|
||||
transition: color 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent;
|
||||
|
||||
@mixin dark {
|
||||
color: lighten(var(--theme-colors-foreground), 10%);
|
||||
}
|
||||
|
||||
@mixin light {
|
||||
color: darken(var(--theme-colors-foreground), 10%);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
border: 1px solid lighten(var(--theme-colors-border), 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loader {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
margin-inline-end: var(--theme-spacing-sm);
|
||||
}
|
||||
|
||||
.button-inner.loading {
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate3d(-50%, -50%, 0);
|
||||
}
|
||||
|
||||
.uppercase {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
155
src/shared/components/button/button.tsx
Normal file
155
src/shared/components/button/button.tsx
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
import type { ButtonVariant, ButtonProps as MantineButtonProps } from '@mantine/core';
|
||||
|
||||
import { ElementProps, Button as MantineButton } from '@mantine/core';
|
||||
import { useTimeout } from '@mantine/hooks';
|
||||
import clsx from 'clsx';
|
||||
import { forwardRef, useCallback, useRef, useState } from 'react';
|
||||
|
||||
import styles from './button.module.css';
|
||||
|
||||
import { Spinner } from '/@/shared/components/spinner/spinner';
|
||||
import { Tooltip, TooltipProps } from '/@/shared/components/tooltip/tooltip';
|
||||
import { createPolymorphicComponent } from '/@/shared/utils/create-polymorphic-component';
|
||||
|
||||
export interface ButtonProps
|
||||
extends ElementProps<'button', keyof MantineButtonProps>,
|
||||
MantineButtonProps,
|
||||
MantineButtonProps {
|
||||
tooltip?: Omit<TooltipProps, 'children'>;
|
||||
uppercase?: boolean;
|
||||
variant?: ExtendedButtonVariant;
|
||||
}
|
||||
|
||||
type ExtendedButtonVariant =
|
||||
| 'state-error'
|
||||
| 'state-info'
|
||||
| 'state-success'
|
||||
| 'state-warning'
|
||||
| ButtonVariant;
|
||||
|
||||
export const _Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
classNames,
|
||||
loading,
|
||||
size = 'sm',
|
||||
style,
|
||||
tooltip,
|
||||
uppercase,
|
||||
variant = 'default',
|
||||
...props
|
||||
}: ButtonProps,
|
||||
ref,
|
||||
) => {
|
||||
if (tooltip) {
|
||||
return (
|
||||
<Tooltip
|
||||
withinPortal
|
||||
{...tooltip}
|
||||
>
|
||||
<MantineButton
|
||||
autoContrast
|
||||
classNames={{
|
||||
label: clsx(styles.label, {
|
||||
[styles.uppercase]: uppercase,
|
||||
}),
|
||||
loader: styles.loader,
|
||||
root: styles.root,
|
||||
section: styles.section,
|
||||
...classNames,
|
||||
}}
|
||||
ref={ref}
|
||||
size={size}
|
||||
style={style}
|
||||
variant={variant}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{loading && (
|
||||
<div className={styles.spinner}>
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</MantineButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MantineButton
|
||||
classNames={{
|
||||
label: clsx(styles.label, {
|
||||
[styles.uppercase]: uppercase,
|
||||
}),
|
||||
loader: styles.loader,
|
||||
root: styles.root,
|
||||
section: styles.section,
|
||||
...classNames,
|
||||
}}
|
||||
ref={ref}
|
||||
size={size}
|
||||
style={style}
|
||||
variant={variant}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{loading && (
|
||||
<div className={styles.spinner}>
|
||||
<Spinner />
|
||||
</div>
|
||||
)}
|
||||
</MantineButton>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export const Button = createPolymorphicComponent<'button', ButtonProps>(_Button);
|
||||
|
||||
interface TimeoutButtonProps extends ButtonProps {
|
||||
timeoutProps: {
|
||||
callback: () => void;
|
||||
duration: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const TimeoutButton = ({ timeoutProps, ...props }: TimeoutButtonProps) => {
|
||||
const [, setTimeoutRemaining] = useState(timeoutProps.duration);
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const intervalRef = useRef(0);
|
||||
|
||||
const callback = () => {
|
||||
timeoutProps.callback();
|
||||
setTimeoutRemaining(timeoutProps.duration);
|
||||
clearInterval(intervalRef.current);
|
||||
setIsRunning(false);
|
||||
};
|
||||
|
||||
const { clear, start } = useTimeout(callback, timeoutProps.duration);
|
||||
|
||||
const startTimeout = useCallback(() => {
|
||||
if (isRunning) {
|
||||
clearInterval(intervalRef.current);
|
||||
setIsRunning(false);
|
||||
clear();
|
||||
} else {
|
||||
setIsRunning(true);
|
||||
start();
|
||||
|
||||
const intervalId = window.setInterval(() => {
|
||||
setTimeoutRemaining((prev) => prev - 100);
|
||||
}, 100);
|
||||
|
||||
intervalRef.current = intervalId;
|
||||
}
|
||||
}, [clear, isRunning, start]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={startTimeout}
|
||||
{...props}
|
||||
>
|
||||
{isRunning ? 'Cancel' : props.children}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
22
src/shared/components/center/center.tsx
Normal file
22
src/shared/components/center/center.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { Center as MantineCenter, CenterProps as MantineCenterProps } from '@mantine/core';
|
||||
import { forwardRef, MouseEvent } from 'react';
|
||||
|
||||
export interface CenterProps extends MantineCenterProps {
|
||||
onClick?: (e: MouseEvent<HTMLDivElement>) => void;
|
||||
}
|
||||
|
||||
export const Center = forwardRef<HTMLDivElement, CenterProps>(
|
||||
({ children, classNames, onClick, style, ...props }, ref) => {
|
||||
return (
|
||||
<MantineCenter
|
||||
classNames={{ ...classNames }}
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
style={{ ...style }}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MantineCenter>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.root {
|
||||
padding: 0 var(--mantine-spacing-sm);
|
||||
}
|
||||
|
||||
.body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.label-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: var(--mantine-font-size-sm);
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: var(--mantine-spacing-xs);
|
||||
}
|
||||
|
||||
.dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.dragged-over-top {
|
||||
box-shadow: inset 0 2px 0 0 var(--mantine-color-secondary-7);
|
||||
}
|
||||
|
||||
.dragged-over-bottom {
|
||||
box-shadow: inset 0 -2px 0 0 var(--mantine-color-secondary-7);
|
||||
}
|
||||
167
src/shared/components/checkbox-select/checkbox-select.tsx
Normal file
167
src/shared/components/checkbox-select/checkbox-select.tsx
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
import type { Edge } from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
||||
|
||||
import {
|
||||
attachClosestEdge,
|
||||
extractClosestEdge,
|
||||
} from '@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge';
|
||||
import { combine } from '@atlaskit/pragmatic-drag-and-drop/combine';
|
||||
import {
|
||||
draggable,
|
||||
dropTargetForElements,
|
||||
} from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
|
||||
import { disableNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview';
|
||||
import clsx from 'clsx';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import styles from './checkbox-select.module.css';
|
||||
|
||||
import { ActionIcon } from '/@/shared/components/action-icon/action-icon';
|
||||
import { Checkbox } from '/@/shared/components/checkbox/checkbox';
|
||||
import { dndUtils, DragData, DragOperation, DragTarget } from '/@/shared/types/drag-and-drop';
|
||||
|
||||
interface CheckboxSelectProps {
|
||||
data: { label: string; value: string }[];
|
||||
enableDrag?: boolean;
|
||||
onChange: (value: string[]) => void;
|
||||
value: string[];
|
||||
}
|
||||
|
||||
export const CheckboxSelect = ({ data, enableDrag, onChange, value }: CheckboxSelectProps) => {
|
||||
const handleChange = (values: string[]) => {
|
||||
onChange(values);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{data.map((option) => (
|
||||
<CheckboxSelectItem
|
||||
enableDrag={enableDrag}
|
||||
key={option.value}
|
||||
onChange={handleChange}
|
||||
option={option}
|
||||
values={value}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface CheckboxSelectItemProps {
|
||||
enableDrag?: boolean;
|
||||
onChange: (values: string[]) => void;
|
||||
option: { label: string; value: string };
|
||||
values: string[];
|
||||
}
|
||||
|
||||
function CheckboxSelectItem({ enableDrag, onChange, option, values }: CheckboxSelectItemProps) {
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
const dragHandleRef = useRef<HTMLButtonElement | null>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [isDraggedOver, setIsDraggedOver] = useState<Edge | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref.current || !dragHandleRef.current || !enableDrag) {
|
||||
return;
|
||||
}
|
||||
|
||||
return combine(
|
||||
draggable({
|
||||
element: dragHandleRef.current,
|
||||
getInitialData: () => {
|
||||
const data = dndUtils.generateDragData({
|
||||
id: [option.value],
|
||||
operation: [DragOperation.REORDER],
|
||||
type: DragTarget.GENERIC,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onDragStart: () => {
|
||||
setIsDragging(true);
|
||||
},
|
||||
onDrop: () => {
|
||||
setIsDragging(false);
|
||||
},
|
||||
onGenerateDragPreview: (data) => {
|
||||
disableNativeDragPreview({ nativeSetDragImage: data.nativeSetDragImage });
|
||||
},
|
||||
}),
|
||||
dropTargetForElements({
|
||||
canDrop: (args) => {
|
||||
const data = args.source.data as unknown as DragData;
|
||||
const isSelf = (args.source.data.id as string[])[0] === option.value;
|
||||
return dndUtils.isDropTarget(data.type, [DragTarget.GENERIC]) && !isSelf;
|
||||
},
|
||||
element: ref.current,
|
||||
getData: ({ element, input }) => {
|
||||
const data = dndUtils.generateDragData({
|
||||
id: [option.value],
|
||||
operation: [DragOperation.REORDER],
|
||||
type: DragTarget.GENERIC,
|
||||
});
|
||||
|
||||
return attachClosestEdge(data, {
|
||||
allowedEdges: ['top', 'bottom'],
|
||||
element,
|
||||
input,
|
||||
});
|
||||
},
|
||||
onDrag: (args) => {
|
||||
const closestEdgeOfTarget: Edge | null = extractClosestEdge(args.self.data);
|
||||
setIsDraggedOver(closestEdgeOfTarget);
|
||||
},
|
||||
onDragLeave: () => {
|
||||
setIsDraggedOver(null);
|
||||
},
|
||||
onDrop: (args) => {
|
||||
const closestEdgeOfTarget: Edge | null = extractClosestEdge(args.self.data);
|
||||
|
||||
const from = args.source.data.id as string[];
|
||||
const to = args.self.data.id as string[];
|
||||
|
||||
const newOrder = dndUtils.reorderById({
|
||||
edge: closestEdgeOfTarget,
|
||||
idFrom: from[0],
|
||||
idTo: to[0],
|
||||
list: values,
|
||||
});
|
||||
|
||||
onChange(newOrder);
|
||||
setIsDraggedOver(null);
|
||||
},
|
||||
}),
|
||||
);
|
||||
}, [values, enableDrag, onChange, option.value]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.item, {
|
||||
[styles.draggedOverBottom]: isDraggedOver === 'bottom',
|
||||
[styles.draggedOverTop]: isDraggedOver === 'top',
|
||||
[styles.dragging]: isDragging,
|
||||
})}
|
||||
ref={ref}
|
||||
>
|
||||
{enableDrag && (
|
||||
<ActionIcon
|
||||
className={styles.dragHandle}
|
||||
icon="dragVertical"
|
||||
ref={dragHandleRef}
|
||||
size="xs"
|
||||
variant="default"
|
||||
/>
|
||||
)}
|
||||
<Checkbox
|
||||
checked={values.includes(option.value)}
|
||||
label={option.label}
|
||||
onChange={(e) => {
|
||||
onChange(
|
||||
e.target.checked
|
||||
? [...values, option.value]
|
||||
: values.filter((v) => v !== option.value),
|
||||
);
|
||||
}}
|
||||
variant="filled"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
src/shared/components/checkbox/checkbox.module.css
Normal file
13
src/shared/components/checkbox/checkbox.module.css
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
.input {
|
||||
background: var(--theme-colors-surface);
|
||||
border: 1px solid var(--theme-colors-border);
|
||||
|
||||
&[data-variant='filled'] {
|
||||
background: var(--theme-colors-background);
|
||||
}
|
||||
}
|
||||
|
||||
.input:disabled {
|
||||
border: 1px solid var(--theme-colors-border);
|
||||
opacity: 0.6;
|
||||
}
|
||||
22
src/shared/components/checkbox/checkbox.tsx
Normal file
22
src/shared/components/checkbox/checkbox.tsx
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { Checkbox as MantineCheckbox, CheckboxProps as MantineCheckboxProps } from '@mantine/core';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import styles from './checkbox.module.css';
|
||||
|
||||
interface CheckboxProps extends MantineCheckboxProps {}
|
||||
|
||||
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
({ classNames, ...props }: CheckboxProps, ref) => {
|
||||
return (
|
||||
<MantineCheckbox
|
||||
classNames={{
|
||||
input: styles.input,
|
||||
label: styles.label,
|
||||
...classNames,
|
||||
}}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
4
src/shared/components/code/code.module.css
Normal file
4
src/shared/components/code/code.module.css
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
.root {
|
||||
background: var(--theme-colors-surface);
|
||||
border-radius: var(--theme-radius-md);
|
||||
}
|
||||
18
src/shared/components/code/code.tsx
Normal file
18
src/shared/components/code/code.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { Code as MantineCode, CodeProps as MantineCodeProps } from '@mantine/core';
|
||||
|
||||
import styles from './code.module.css';
|
||||
|
||||
export interface CodeProps extends MantineCodeProps {}
|
||||
|
||||
export const Code = ({ classNames, ...props }: CodeProps) => {
|
||||
return (
|
||||
<MantineCode
|
||||
{...props}
|
||||
classNames={{
|
||||
...classNames,
|
||||
root: styles.root,
|
||||
}}
|
||||
spellCheck={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
37
src/shared/components/color-input/color-input.module.css
Normal file
37
src/shared/components/color-input/color-input.module.css
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
.root {
|
||||
& [data-disabled='true'] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&[data-variant='default'] {
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
background: var(--theme-colors-surface);
|
||||
}
|
||||
|
||||
&[data-variant='filled'] {
|
||||
color: var(--theme-colors-foreground);
|
||||
background: var(--theme-colors-background);
|
||||
}
|
||||
}
|
||||
|
||||
.input:focus,
|
||||
.input:focus-visible {
|
||||
border-color: lighten(var(--theme-colors-border), 10%);
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: var(--theme-spacing-sm);
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
background: var(--theme-colors-surface);
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--theme-radius-md);
|
||||
box-shadow: 2px 2px 10px 2px rgb(0 0 0 / 40%);
|
||||
}
|
||||
30
src/shared/components/color-input/color-input.tsx
Normal file
30
src/shared/components/color-input/color-input.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import {
|
||||
ColorInput as MantineColorInput,
|
||||
ColorInputProps as MantineColorInputProps,
|
||||
} from '@mantine/core';
|
||||
|
||||
import styles from './color-input.module.css';
|
||||
|
||||
export interface ColorInputProps extends MantineColorInputProps {}
|
||||
|
||||
export const ColorInput = ({
|
||||
classNames,
|
||||
size = 'sm',
|
||||
variant = 'default',
|
||||
...props
|
||||
}: ColorInputProps) => {
|
||||
return (
|
||||
<MantineColorInput
|
||||
classNames={{
|
||||
dropdown: styles.dropdown,
|
||||
input: styles.input,
|
||||
label: styles.label,
|
||||
root: styles.root,
|
||||
...classNames,
|
||||
}}
|
||||
size={size}
|
||||
variant={variant}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
10
src/shared/components/copy-button/copy-button.tsx
Normal file
10
src/shared/components/copy-button/copy-button.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import {
|
||||
CopyButton as MantineCopyButton,
|
||||
CopyButtonProps as MantineCopyButtonProps,
|
||||
} from '@mantine/core';
|
||||
|
||||
export interface CopyButtonProps extends MantineCopyButtonProps {}
|
||||
|
||||
export const CopyButton = ({ children, ...props }: CopyButtonProps) => {
|
||||
return <MantineCopyButton {...props}>{children}</MantineCopyButton>;
|
||||
};
|
||||
19
src/shared/components/date-picker/date-picker.module.css
Normal file
19
src/shared/components/date-picker/date-picker.module.css
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
.root {
|
||||
& [data-disabled='true'] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
background: var(--theme-colors-surface);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.section {
|
||||
color: var(--theme-colors-foreground-muted);
|
||||
}
|
||||
|
||||
.required {
|
||||
color: var(--theme-colors-state-error);
|
||||
}
|
||||
35
src/shared/components/date-picker/date-picker.tsx
Normal file
35
src/shared/components/date-picker/date-picker.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import type { DateInputProps as MantineDateInputProps } from '@mantine/dates';
|
||||
|
||||
import { DateInput as MantineDateInput } from '@mantine/dates';
|
||||
|
||||
import styles from './date-picker.module.css';
|
||||
|
||||
interface DateInputProps extends MantineDateInputProps {
|
||||
maxWidth?: number | string;
|
||||
width?: number | string;
|
||||
}
|
||||
|
||||
export const DateInput = ({
|
||||
classNames,
|
||||
maxWidth,
|
||||
size = 'sm',
|
||||
style,
|
||||
width,
|
||||
...props
|
||||
}: DateInputProps) => {
|
||||
return (
|
||||
<MantineDateInput
|
||||
classNames={{
|
||||
input: styles.input,
|
||||
label: styles.label,
|
||||
required: styles.required,
|
||||
root: styles.root,
|
||||
section: styles.section,
|
||||
...classNames,
|
||||
}}
|
||||
size={size}
|
||||
style={{ maxWidth, width, ...style }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
.root {
|
||||
& [data-disabled='true'] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
background: var(--theme-colors-surface);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.section {
|
||||
color: var(--theme-colors-foreground-muted);
|
||||
}
|
||||
|
||||
.required {
|
||||
color: var(--theme-colors-state-error);
|
||||
}
|
||||
37
src/shared/components/date-time-picker/date-time-picker.tsx
Normal file
37
src/shared/components/date-time-picker/date-time-picker.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import type { DateTimePickerProps as MantineDateTimePickerProps } from '@mantine/dates';
|
||||
|
||||
import { DateTimePicker as MantineDateTimePicker } from '@mantine/dates';
|
||||
|
||||
import styles from './date-time-picker.module.css';
|
||||
|
||||
interface DateTimePickerProps extends MantineDateTimePickerProps {
|
||||
maxWidth?: number | string;
|
||||
width?: number | string;
|
||||
}
|
||||
|
||||
export const DateTimePicker = ({
|
||||
classNames,
|
||||
maxWidth,
|
||||
popoverProps,
|
||||
size = 'sm',
|
||||
style,
|
||||
width,
|
||||
...props
|
||||
}: DateTimePickerProps) => {
|
||||
return (
|
||||
<MantineDateTimePicker
|
||||
classNames={{
|
||||
input: styles.input,
|
||||
label: styles.label,
|
||||
required: styles.required,
|
||||
root: styles.root,
|
||||
section: styles.section,
|
||||
...classNames,
|
||||
}}
|
||||
popoverProps={{ withinPortal: true, ...popoverProps }}
|
||||
size={size}
|
||||
style={{ maxWidth, width, ...style }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
5
src/shared/components/dialog/dialog.module.css
Normal file
5
src/shared/components/dialog/dialog.module.css
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
.root {
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
background: var(--theme-colors-surface);
|
||||
box-shadow: 2px 2px 10px 2px rgb(0 0 0 / 40%);
|
||||
}
|
||||
19
src/shared/components/dialog/dialog.tsx
Normal file
19
src/shared/components/dialog/dialog.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import type { DialogProps as MantineDialogProps } from '@mantine/core';
|
||||
|
||||
import { Dialog as MantineDialog } from '@mantine/core';
|
||||
|
||||
import styles from './dialog.module.css';
|
||||
|
||||
interface DialogProps extends MantineDialogProps {}
|
||||
|
||||
export const Dialog = ({ classNames, style, ...props }: DialogProps) => {
|
||||
return (
|
||||
<MantineDialog
|
||||
classNames={{ root: styles.root, ...classNames }}
|
||||
style={{
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
3
src/shared/components/divider/divider.module.css
Normal file
3
src/shared/components/divider/divider.module.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.root {
|
||||
--divider-color: var(--theme-colors-border);
|
||||
}
|
||||
19
src/shared/components/divider/divider.tsx
Normal file
19
src/shared/components/divider/divider.tsx
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { Divider as MantineDivider, DividerProps as MantineDividerProps } from '@mantine/core';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import styles from './divider.module.css';
|
||||
|
||||
export interface DividerProps extends MantineDividerProps {}
|
||||
|
||||
export const Divider = forwardRef<HTMLDivElement, DividerProps>(
|
||||
({ classNames, style, ...props }, ref) => {
|
||||
return (
|
||||
<MantineDivider
|
||||
classNames={{ root: styles.root, ...classNames }}
|
||||
ref={ref}
|
||||
style={{ ...style }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
58
src/shared/components/dropdown-menu/dropdown-menu.module.css
Normal file
58
src/shared/components/dropdown-menu/dropdown-menu.module.css
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
.menu-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--theme-spacing-sm) var(--theme-spacing-md);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.menu-item:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.menu-item-label {
|
||||
margin-right: var(--theme-spacing-md);
|
||||
margin-left: var(--theme-spacing-md);
|
||||
font-size: var(--theme-font-size-sm);
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
}
|
||||
|
||||
.selected {
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 2px;
|
||||
width: 4px;
|
||||
height: 50%;
|
||||
content: '';
|
||||
background-color: var(--theme-colors-primary-filled);
|
||||
border-radius: var(--theme-border-radius-xl);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item-label-danger {
|
||||
color: var(--theme-colors-state-error);
|
||||
}
|
||||
|
||||
.menu-item-right-section {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.menu-dropdown {
|
||||
padding: var(--theme-spacing-xs);
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
background: var(--theme-colors-surface);
|
||||
border: 1px solid var(--theme-colors-border);
|
||||
filter: drop-shadow(0 0 5px rgb(0 0 0 / 50%));
|
||||
}
|
||||
|
||||
.menu-divider {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border-color: var(--theme-colors-border);
|
||||
}
|
||||
|
||||
.menu-item-section svg {
|
||||
font-size: var(--theme-font-size-sm);
|
||||
}
|
||||
102
src/shared/components/dropdown-menu/dropdown-menu.tsx
Normal file
102
src/shared/components/dropdown-menu/dropdown-menu.tsx
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import type {
|
||||
MenuDividerProps as MantineMenuDividerProps,
|
||||
MenuDropdownProps as MantineMenuDropdownProps,
|
||||
MenuItemProps as MantineMenuItemProps,
|
||||
MenuLabelProps as MantineMenuLabelProps,
|
||||
MenuProps as MantineMenuProps,
|
||||
} from '@mantine/core';
|
||||
|
||||
import { Menu as MantineMenu } from '@mantine/core';
|
||||
import clsx from 'clsx';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import styles from './dropdown-menu.module.css';
|
||||
|
||||
import { createPolymorphicComponent } from '/@/shared/utils/create-polymorphic-component';
|
||||
|
||||
type MenuDividerProps = MantineMenuDividerProps;
|
||||
type MenuDropdownProps = MantineMenuDropdownProps;
|
||||
interface MenuItemProps extends MantineMenuItemProps {
|
||||
children: ReactNode;
|
||||
isDanger?: boolean;
|
||||
isSelected?: boolean;
|
||||
}
|
||||
type MenuLabelProps = MantineMenuLabelProps;
|
||||
type MenuProps = MantineMenuProps;
|
||||
|
||||
export const DropdownMenu = ({ children, ...props }: MenuProps) => {
|
||||
return (
|
||||
<MantineMenu
|
||||
classNames={{
|
||||
dropdown: styles['menu-dropdown'],
|
||||
itemSection: styles['menu-item-section'],
|
||||
}}
|
||||
transitionProps={{
|
||||
transition: 'fade',
|
||||
}}
|
||||
withinPortal
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MantineMenu>
|
||||
);
|
||||
};
|
||||
|
||||
const MenuLabel = ({ children, ...props }: MenuLabelProps) => {
|
||||
return (
|
||||
<MantineMenu.Label
|
||||
className={styles['menu-label']}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MantineMenu.Label>
|
||||
);
|
||||
};
|
||||
|
||||
const pMenuItem = ({ children, isDanger, isSelected, ...props }: MenuItemProps) => {
|
||||
return (
|
||||
<MantineMenu.Item
|
||||
className={clsx(styles['menu-item'], {
|
||||
[styles.selected]: isSelected,
|
||||
})}
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
className={clsx(styles['menu-item-label'], {
|
||||
[styles['menu-item-label-danger']]: isDanger,
|
||||
[styles['menu-item-label-normal']]: !isDanger,
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</span>
|
||||
</MantineMenu.Item>
|
||||
);
|
||||
};
|
||||
|
||||
const MenuDropdown = ({ children, ...props }: MenuDropdownProps) => {
|
||||
return (
|
||||
<MantineMenu.Dropdown
|
||||
className={styles['menu-dropdown']}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MantineMenu.Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
const MenuItem = createPolymorphicComponent<'button', MenuItemProps>(pMenuItem);
|
||||
|
||||
const MenuDivider = ({ ...props }: MenuDividerProps) => {
|
||||
return (
|
||||
<MantineMenu.Divider
|
||||
className={styles['menu-divider']}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
DropdownMenu.Label = MenuLabel;
|
||||
DropdownMenu.Item = MenuItem;
|
||||
DropdownMenu.Target = MantineMenu.Target;
|
||||
DropdownMenu.Dropdown = MenuDropdown;
|
||||
DropdownMenu.Divider = MenuDivider;
|
||||
40
src/shared/components/file-input/file-input.module.css
Normal file
40
src/shared/components/file-input/file-input.module.css
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
.root {
|
||||
transition: width 0.3s ease-in-out;
|
||||
|
||||
&[data-disabled='true'] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&[data-variant='default'] {
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
background: var(--theme-colors-surface);
|
||||
}
|
||||
|
||||
&[data-variant='filled'] {
|
||||
color: var(--theme-colors-foreground);
|
||||
background: var(--theme-colors-background);
|
||||
}
|
||||
}
|
||||
|
||||
.input:focus,
|
||||
.input:focus-visible {
|
||||
border-color: lighten(var(--theme-colors-border), 10%);
|
||||
}
|
||||
|
||||
.section {
|
||||
color: var(--theme-colors-foreground-muted);
|
||||
}
|
||||
|
||||
.required {
|
||||
color: var(--theme-colors-state-error);
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: var(--theme-spacing-sm);
|
||||
font-family: var(--theme-label-font-family);
|
||||
}
|
||||
49
src/shared/components/file-input/file-input.tsx
Normal file
49
src/shared/components/file-input/file-input.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import {
|
||||
FileInput as MantineFileInput,
|
||||
FileInputProps as MantineFileInputProps,
|
||||
} from '@mantine/core';
|
||||
import { CSSProperties, forwardRef } from 'react';
|
||||
|
||||
import styles from './file-input.module.css';
|
||||
|
||||
export interface FileInputProps extends MantineFileInputProps {
|
||||
maxWidth?: CSSProperties['maxWidth'];
|
||||
width?: CSSProperties['width'];
|
||||
}
|
||||
|
||||
export const FileInput = forwardRef<HTMLButtonElement, FileInputProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
classNames,
|
||||
maxWidth,
|
||||
size = 'sm',
|
||||
style,
|
||||
variant = 'default',
|
||||
width,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<MantineFileInput
|
||||
classNames={{
|
||||
input: styles.input,
|
||||
label: styles.label,
|
||||
required: styles.required,
|
||||
root: styles.root,
|
||||
section: styles.section,
|
||||
wrapper: styles.wrapper,
|
||||
...classNames,
|
||||
}}
|
||||
ref={ref}
|
||||
size={size}
|
||||
style={{ maxWidth, width, ...style }}
|
||||
variant={variant}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MantineFileInput>
|
||||
);
|
||||
},
|
||||
);
|
||||
17
src/shared/components/flex/flex.tsx
Normal file
17
src/shared/components/flex/flex.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { Flex as MantineFlex, FlexProps as MantineFlexProps } from '@mantine/core';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
export interface FlexProps extends MantineFlexProps {}
|
||||
|
||||
export const Flex = forwardRef<HTMLDivElement, FlexProps>(({ children, ...props }, ref) => {
|
||||
return (
|
||||
<MantineFlex
|
||||
classNames={{ ...props.classNames }}
|
||||
ref={ref}
|
||||
style={{ ...props.style }}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MantineFlex>
|
||||
);
|
||||
});
|
||||
15
src/shared/components/grid/grid.tsx
Normal file
15
src/shared/components/grid/grid.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import { Grid as MantineGrid, GridProps as MantineGridProps } from '@mantine/core';
|
||||
|
||||
export interface GridProps extends MantineGridProps {}
|
||||
|
||||
export const Grid = ({ classNames, style, ...props }: GridProps) => {
|
||||
return (
|
||||
<MantineGrid
|
||||
classNames={{ ...classNames }}
|
||||
style={{ ...style }}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Grid.Col = MantineGrid.Col;
|
||||
17
src/shared/components/group/group.tsx
Normal file
17
src/shared/components/group/group.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { Group as MantineGroup, GroupProps as MantineGroupProps } from '@mantine/core';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
export interface GroupProps extends MantineGroupProps {}
|
||||
|
||||
export const Group = forwardRef<HTMLDivElement, GroupProps>(({ children, ...props }, ref) => {
|
||||
return (
|
||||
<MantineGroup
|
||||
classNames={{ ...props.classNames }}
|
||||
ref={ref}
|
||||
style={{ ...props.style }}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MantineGroup>
|
||||
);
|
||||
});
|
||||
7
src/shared/components/hover-card/hover-card.module.css
Normal file
7
src/shared/components/hover-card/hover-card.module.css
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
.dropdown {
|
||||
padding: var(--theme-spacing-xs);
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
background: var(--theme-colors-surface);
|
||||
border: 1px solid var(--theme-colors-border);
|
||||
filter: drop-shadow(0 0 5px rgb(0 0 0 / 50%));
|
||||
}
|
||||
25
src/shared/components/hover-card/hover-card.tsx
Normal file
25
src/shared/components/hover-card/hover-card.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import {
|
||||
HoverCard as MantineHoverCard,
|
||||
HoverCardProps as MantineHoverCardProps,
|
||||
} from '@mantine/core';
|
||||
|
||||
import styles from './hover-card.module.css';
|
||||
|
||||
interface HoverCardProps extends MantineHoverCardProps {}
|
||||
|
||||
export const HoverCard = ({ children, classNames, ...props }: HoverCardProps) => {
|
||||
return (
|
||||
<MantineHoverCard
|
||||
classNames={{
|
||||
dropdown: styles.dropdown,
|
||||
...classNames,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MantineHoverCard>
|
||||
);
|
||||
};
|
||||
|
||||
HoverCard.Target = MantineHoverCard.Target;
|
||||
HoverCard.Dropdown = MantineHoverCard.Dropdown;
|
||||
123
src/shared/components/icon/icon.module.css
Normal file
123
src/shared/components/icon/icon.module.css
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
.size-xs {
|
||||
font-size: var(--theme-font-size-xs);
|
||||
}
|
||||
|
||||
.size-sm {
|
||||
font-size: var(--theme-font-size-sm);
|
||||
}
|
||||
|
||||
.size-md {
|
||||
font-size: var(--theme-font-size-md);
|
||||
}
|
||||
|
||||
.size-lg {
|
||||
font-size: var(--theme-font-size-lg);
|
||||
}
|
||||
|
||||
.size-xl {
|
||||
font-size: var(--theme-font-size-xl);
|
||||
}
|
||||
|
||||
.size-2xl {
|
||||
font-size: var(--theme-font-size-2xl);
|
||||
}
|
||||
|
||||
.size-3xl {
|
||||
font-size: var(--theme-font-size-3xl);
|
||||
}
|
||||
|
||||
.size-4xl {
|
||||
font-size: var(--theme-font-size-4xl);
|
||||
}
|
||||
|
||||
.size-5xl {
|
||||
font-size: var(--theme-font-size-5xl);
|
||||
}
|
||||
|
||||
.color-default {
|
||||
color: var(--theme-colors-foreground);
|
||||
}
|
||||
|
||||
.color-primary {
|
||||
color: var(--theme-colors-primary-filled);
|
||||
}
|
||||
|
||||
.color-muted {
|
||||
color: var(--theme-colors-foreground-muted);
|
||||
}
|
||||
|
||||
.color-success {
|
||||
color: var(--theme-colors-state-success);
|
||||
}
|
||||
|
||||
.color-error {
|
||||
color: var(--theme-colors-state-error);
|
||||
}
|
||||
|
||||
.color-info {
|
||||
color: var(--theme-colors-state-info);
|
||||
}
|
||||
|
||||
.color-warn {
|
||||
color: var(--theme-colors-state-warn);
|
||||
}
|
||||
|
||||
.fill {
|
||||
fill: transparent;
|
||||
}
|
||||
|
||||
.fill-default {
|
||||
fill: var(--theme-colors-foreground);
|
||||
}
|
||||
|
||||
.fill-inherit {
|
||||
fill: inherit;
|
||||
}
|
||||
|
||||
.fill-primary {
|
||||
fill: var(--theme-colors-primary-filled);
|
||||
}
|
||||
|
||||
.fill-muted {
|
||||
fill: var(--theme-colors-foreground-muted);
|
||||
}
|
||||
|
||||
.fill-success {
|
||||
fill: var(--theme-colors-state-success);
|
||||
}
|
||||
|
||||
.fill-error {
|
||||
fill: var(--theme-colors-state-error);
|
||||
}
|
||||
|
||||
.fill-info {
|
||||
fill: var(--theme-colors-state-info);
|
||||
}
|
||||
|
||||
.fill-warn {
|
||||
fill: var(--theme-colors-state-warn);
|
||||
}
|
||||
|
||||
.spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
269
src/shared/components/icon/icon.tsx
Normal file
269
src/shared/components/icon/icon.tsx
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
import clsx from 'clsx';
|
||||
import { motion } from 'motion/react';
|
||||
import { type ComponentType, forwardRef } from 'react';
|
||||
import { IconBaseProps } from 'react-icons';
|
||||
import { FaLastfmSquare } from 'react-icons/fa';
|
||||
import {
|
||||
LuAppWindow,
|
||||
LuArrowDown,
|
||||
LuArrowDownToLine,
|
||||
LuArrowDownWideNarrow,
|
||||
LuArrowLeft,
|
||||
LuArrowLeftToLine,
|
||||
LuArrowRight,
|
||||
LuArrowRightToLine,
|
||||
LuArrowUp,
|
||||
LuArrowUpDown,
|
||||
LuArrowUpNarrowWide,
|
||||
LuArrowUpToLine,
|
||||
LuBookOpen,
|
||||
LuCheck,
|
||||
LuChevronDown,
|
||||
LuChevronLast,
|
||||
LuChevronLeft,
|
||||
LuChevronRight,
|
||||
LuChevronUp,
|
||||
LuCircleCheck,
|
||||
LuCircleX,
|
||||
LuClipboardCopy,
|
||||
LuClock3,
|
||||
LuCloudDownload,
|
||||
LuCornerUpRight,
|
||||
LuDelete,
|
||||
LuDisc3,
|
||||
LuDownload,
|
||||
LuEllipsis,
|
||||
LuEllipsisVertical,
|
||||
LuExternalLink,
|
||||
LuFlag,
|
||||
LuFolderOpen,
|
||||
LuGauge,
|
||||
LuGithub,
|
||||
LuGripHorizontal,
|
||||
LuGripVertical,
|
||||
LuHardDrive,
|
||||
LuHash,
|
||||
LuHeart,
|
||||
LuHeartCrack,
|
||||
LuImage,
|
||||
LuImageOff,
|
||||
LuInfinity,
|
||||
LuInfo,
|
||||
LuKeyboard,
|
||||
LuLayoutGrid,
|
||||
LuLibrary,
|
||||
LuList,
|
||||
LuListFilter,
|
||||
LuListMinus,
|
||||
LuListMusic,
|
||||
LuListPlus,
|
||||
LuLoader,
|
||||
LuLock,
|
||||
LuLogIn,
|
||||
LuLogOut,
|
||||
LuMenu,
|
||||
LuMinus,
|
||||
LuMusic,
|
||||
LuMusic2,
|
||||
LuPanelRightClose,
|
||||
LuPanelRightOpen,
|
||||
LuPause,
|
||||
LuPencilLine,
|
||||
LuPlay,
|
||||
LuPlus,
|
||||
LuRadio,
|
||||
LuRotateCw,
|
||||
LuSave,
|
||||
LuSearch,
|
||||
LuSettings2,
|
||||
LuShare2,
|
||||
LuShieldAlert,
|
||||
LuShuffle,
|
||||
LuSkipBack,
|
||||
LuSkipForward,
|
||||
LuSlidersHorizontal,
|
||||
LuSquare,
|
||||
LuSquareCheck,
|
||||
LuSquareMenu,
|
||||
LuStar,
|
||||
LuStepBack,
|
||||
LuStepForward,
|
||||
LuTable,
|
||||
LuTriangleAlert,
|
||||
LuUser,
|
||||
LuUserPen,
|
||||
LuUserRoundCog,
|
||||
LuVolume1,
|
||||
LuVolume2,
|
||||
LuVolumeX,
|
||||
LuX,
|
||||
} from 'react-icons/lu';
|
||||
import { MdOutlineVisibility, MdOutlineVisibilityOff } from 'react-icons/md';
|
||||
import { RiPlayListAddLine, RiRepeat2Line, RiRepeatOneLine } from 'react-icons/ri';
|
||||
import { SiMusicbrainz } from 'react-icons/si';
|
||||
|
||||
import styles from './icon.module.css';
|
||||
|
||||
export type AppIconSelection = keyof typeof AppIcon;
|
||||
|
||||
export const AppIcon = {
|
||||
add: LuPlus,
|
||||
album: LuDisc3,
|
||||
appWindow: LuAppWindow,
|
||||
arrowDown: LuArrowDown,
|
||||
arrowDownS: LuChevronDown,
|
||||
arrowDownToLine: LuArrowDownToLine,
|
||||
arrowLeft: LuArrowLeft,
|
||||
arrowLeftS: LuChevronLeft,
|
||||
arrowLeftToLine: LuArrowLeftToLine,
|
||||
arrowRight: LuArrowRight,
|
||||
arrowRightLast: LuChevronLast,
|
||||
arrowRightS: LuChevronRight,
|
||||
arrowRightToLine: LuArrowRightToLine,
|
||||
arrowUp: LuArrowUp,
|
||||
arrowUpS: LuChevronUp,
|
||||
arrowUpToLine: LuArrowUpToLine,
|
||||
artist: LuUserPen,
|
||||
brandGitHub: LuGithub,
|
||||
brandLastfm: FaLastfmSquare,
|
||||
brandMusicBrainz: SiMusicbrainz,
|
||||
cache: LuCloudDownload,
|
||||
check: LuCheck,
|
||||
clipboardCopy: LuClipboardCopy,
|
||||
delete: LuDelete,
|
||||
download: LuDownload,
|
||||
dragHorizontal: LuGripHorizontal,
|
||||
dragVertical: LuGripVertical,
|
||||
dropdown: LuChevronDown,
|
||||
duration: LuClock3,
|
||||
edit: LuPencilLine,
|
||||
ellipsisHorizontal: LuEllipsis,
|
||||
ellipsisVertical: LuEllipsisVertical,
|
||||
emptyImage: LuImageOff,
|
||||
error: LuShieldAlert,
|
||||
externalLink: LuExternalLink,
|
||||
favorite: LuHeart,
|
||||
filter: LuListFilter,
|
||||
folder: LuFolderOpen,
|
||||
genre: LuFlag,
|
||||
hash: LuHash,
|
||||
home: LuSquareMenu,
|
||||
image: LuImage,
|
||||
info: LuInfo,
|
||||
itemAlbum: LuDisc3,
|
||||
itemSong: LuMusic,
|
||||
keyboard: LuKeyboard,
|
||||
layoutGrid: LuLayoutGrid,
|
||||
layoutList: LuList,
|
||||
layoutTable: LuTable,
|
||||
library: LuLibrary,
|
||||
list: LuList,
|
||||
listInfinite: LuInfinity,
|
||||
listPaginated: LuArrowRightToLine,
|
||||
lock: LuLock,
|
||||
mediaNext: LuSkipForward,
|
||||
mediaPause: LuPause,
|
||||
mediaPlay: LuPlay,
|
||||
mediaPlayLast: LuChevronLast,
|
||||
mediaPlayNext: LuCornerUpRight,
|
||||
mediaPrevious: LuSkipBack,
|
||||
mediaRandom: RiPlayListAddLine,
|
||||
mediaRepeat: RiRepeat2Line,
|
||||
mediaRepeatOne: RiRepeatOneLine,
|
||||
mediaSettings: LuSlidersHorizontal,
|
||||
mediaShuffle: LuShuffle,
|
||||
mediaSpeed: LuGauge,
|
||||
mediaStepBackward: LuStepBack,
|
||||
mediaStepForward: LuStepForward,
|
||||
mediaStop: LuSquare,
|
||||
menu: LuMenu,
|
||||
metadata: LuBookOpen,
|
||||
minus: LuMinus,
|
||||
panelRightClose: LuPanelRightClose,
|
||||
panelRightOpen: LuPanelRightOpen,
|
||||
playlist: LuListMusic,
|
||||
playlistAdd: LuListPlus,
|
||||
playlistDelete: LuListMinus,
|
||||
plus: LuPlus,
|
||||
queue: LuList,
|
||||
radio: LuRadio,
|
||||
refresh: LuRotateCw,
|
||||
remove: LuMinus,
|
||||
save: LuSave,
|
||||
search: LuSearch,
|
||||
server: LuHardDrive,
|
||||
settings: LuSettings2,
|
||||
share: LuShare2,
|
||||
signIn: LuLogIn,
|
||||
signOut: LuLogOut,
|
||||
sort: LuArrowUpDown,
|
||||
sortAsc: LuArrowUpNarrowWide,
|
||||
sortDesc: LuArrowDownWideNarrow,
|
||||
spinner: LuLoader,
|
||||
square: LuSquare,
|
||||
squareCheck: LuSquareCheck,
|
||||
star: LuStar,
|
||||
success: LuCircleCheck,
|
||||
track: LuMusic2,
|
||||
unfavorite: LuHeartCrack,
|
||||
user: LuUser,
|
||||
userManage: LuUserRoundCog,
|
||||
visibility: MdOutlineVisibility,
|
||||
visibilityOff: MdOutlineVisibilityOff,
|
||||
volumeMax: LuVolume2,
|
||||
volumeMute: LuVolumeX,
|
||||
volumeNormal: LuVolume1,
|
||||
warn: LuTriangleAlert,
|
||||
x: LuX,
|
||||
xCircle: LuCircleX,
|
||||
} as const;
|
||||
|
||||
export interface IconProps extends Omit<IconBaseProps, 'color' | 'fill' | 'size'> {
|
||||
animate?: 'pulse' | 'spin';
|
||||
color?: 'default' | 'error' | 'info' | 'inherit' | 'muted' | 'primary' | 'success' | 'warn';
|
||||
fill?: 'default' | 'error' | 'info' | 'inherit' | 'muted' | 'primary' | 'success' | 'warn';
|
||||
icon: keyof typeof AppIcon;
|
||||
size?: '2xl' | '3xl' | '4xl' | '5xl' | 'lg' | 'md' | 'sm' | 'xl' | 'xs' | number | string;
|
||||
}
|
||||
|
||||
export const Icon = forwardRef<HTMLDivElement, IconProps>((props, ref) => {
|
||||
const { animate, className, color, fill, icon, size = 'md' } = props;
|
||||
|
||||
const IconComponent: ComponentType<any> = AppIcon[icon];
|
||||
|
||||
const classNames = clsx(className, {
|
||||
[styles.fill]: true,
|
||||
[styles.pulse]: animate === 'pulse',
|
||||
[styles.spin]: animate === 'spin',
|
||||
[styles[`color-${color || fill}`]]: color || fill,
|
||||
[styles[`fill-${fill}`]]: fill,
|
||||
[styles[`size-${size}`]]: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<IconComponent
|
||||
className={classNames}
|
||||
fill={fill}
|
||||
ref={ref}
|
||||
size={isPredefinedSize(size) ? undefined : size}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
Icon.displayName = 'Icon';
|
||||
|
||||
export const MotionIcon: ComponentType = motion.create(Icon);
|
||||
|
||||
function isPredefinedSize(size: IconProps['size']) {
|
||||
return (
|
||||
size === '2xl' ||
|
||||
size === '3xl' ||
|
||||
size === '4xl' ||
|
||||
size === '5xl' ||
|
||||
size === 'lg' ||
|
||||
size === 'md' ||
|
||||
size === 'sm' ||
|
||||
size === 'xl' ||
|
||||
size === 'xs'
|
||||
);
|
||||
}
|
||||
34
src/shared/components/image/image.module.css
Normal file
34
src/shared/components/image/image.module.css
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
.image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: var(--theme-image-fit);
|
||||
border-radius: var(--theme-radius-md);
|
||||
}
|
||||
|
||||
.loader {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.image-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.unloader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
background-color: darken(var(--theme-colors-foreground), 40%);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
93
src/shared/components/image/image.tsx
Normal file
93
src/shared/components/image/image.tsx
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
import type { ImgHTMLAttributes } from 'react';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import { Img } from 'react-image';
|
||||
|
||||
import styles from './image.module.css';
|
||||
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { Skeleton } from '/@/shared/components/skeleton/skeleton';
|
||||
|
||||
interface ImageContainerProps {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface ImageLoaderProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface ImageProps extends Omit<ImgHTMLAttributes<HTMLImageElement>, 'src'> {
|
||||
containerClassName?: string;
|
||||
includeLoader?: boolean;
|
||||
includeUnloader?: boolean;
|
||||
src: string | string[] | undefined;
|
||||
thumbHash?: string;
|
||||
}
|
||||
|
||||
interface ImageUnloaderProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Image({
|
||||
className,
|
||||
containerClassName,
|
||||
includeLoader = true,
|
||||
includeUnloader = true,
|
||||
src,
|
||||
}: ImageProps) {
|
||||
if (src) {
|
||||
return (
|
||||
<Img
|
||||
className={clsx(styles.image, className)}
|
||||
container={(children) => (
|
||||
<ImageContainer className={containerClassName}>{children}</ImageContainer>
|
||||
)}
|
||||
loader={
|
||||
includeLoader ? (
|
||||
<ImageContainer className={containerClassName}>
|
||||
<ImageLoader className={className} />
|
||||
</ImageContainer>
|
||||
) : null
|
||||
}
|
||||
loading="eager"
|
||||
src={src}
|
||||
unloader={
|
||||
includeUnloader ? (
|
||||
<ImageContainer className={containerClassName}>
|
||||
<ImageUnloader className={className} />
|
||||
</ImageContainer>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <ImageUnloader />;
|
||||
}
|
||||
|
||||
function ImageContainer({ children, className }: ImageContainerProps) {
|
||||
return <div className={clsx(styles.imageContainer, className)}>{children}</div>;
|
||||
}
|
||||
|
||||
function ImageLoader({ className }: ImageLoaderProps) {
|
||||
return (
|
||||
<div className={clsx(styles.loader, className)}>
|
||||
<Skeleton
|
||||
className={clsx(styles.skeleton, className)}
|
||||
enableAnimation={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ImageUnloader({ className }: ImageUnloaderProps) {
|
||||
return (
|
||||
<div className={clsx(styles.unloader, className)}>
|
||||
<Icon
|
||||
icon="emptyImage"
|
||||
size="xl"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
39
src/shared/components/json-input/json-input.module.css
Normal file
39
src/shared/components/json-input/json-input.module.css
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
.root {
|
||||
transition: width 0.3s ease-in-out;
|
||||
|
||||
&[data-disabled='true'] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&[data-variant='default'] {
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
background: var(--theme-colors-surface);
|
||||
}
|
||||
|
||||
&[data-variant='filled'] {
|
||||
color: var(--theme-colors-foreground);
|
||||
background: var(--theme-colors-background);
|
||||
}
|
||||
}
|
||||
|
||||
.input:focus,
|
||||
.input:focus-visible {
|
||||
border-color: lighten(var(--theme-colors-border), 10%);
|
||||
}
|
||||
|
||||
.section {
|
||||
color: var(--theme-colors-foreground-muted);
|
||||
}
|
||||
|
||||
.required {
|
||||
color: var(--theme-colors-state-error);
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: var(--theme-spacing-sm);
|
||||
}
|
||||
49
src/shared/components/json-input/json-input.tsx
Normal file
49
src/shared/components/json-input/json-input.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import {
|
||||
JsonInput as MantineJsonInput,
|
||||
JsonInputProps as MantineJsonInputProps,
|
||||
} from '@mantine/core';
|
||||
import { CSSProperties, forwardRef } from 'react';
|
||||
|
||||
import styles from './json-input.module.css';
|
||||
|
||||
export interface JsonInputProps extends MantineJsonInputProps {
|
||||
maxWidth?: CSSProperties['maxWidth'];
|
||||
width?: CSSProperties['width'];
|
||||
}
|
||||
|
||||
export const JsonInput = forwardRef<HTMLTextAreaElement, JsonInputProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
classNames,
|
||||
maxWidth,
|
||||
size = 'sm',
|
||||
style,
|
||||
variant = 'default',
|
||||
width,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<MantineJsonInput
|
||||
classNames={{
|
||||
input: styles.input,
|
||||
label: styles.label,
|
||||
required: styles.required,
|
||||
root: styles.root,
|
||||
section: styles.section,
|
||||
wrapper: styles.wrapper,
|
||||
...classNames,
|
||||
}}
|
||||
ref={ref}
|
||||
size={size}
|
||||
style={{ maxWidth, width, ...style }}
|
||||
variant={variant}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MantineJsonInput>
|
||||
);
|
||||
},
|
||||
);
|
||||
7
src/shared/components/kbd/kbd.tsx
Normal file
7
src/shared/components/kbd/kbd.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { Kbd as MantineKbd, KbdProps as MantineKbdProps } from '@mantine/core';
|
||||
|
||||
export interface KbdProps extends MantineKbdProps {}
|
||||
|
||||
export const Kbd = (props: KbdProps) => {
|
||||
return <MantineKbd {...props} />;
|
||||
};
|
||||
17
src/shared/components/modal/modal.module.css
Normal file
17
src/shared/components/modal/modal.module.css
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
.title {
|
||||
font-size: var(--theme-font-size-lg);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: var(--theme-spacing-sm) var(--theme-spacing-md);
|
||||
}
|
||||
|
||||
.header {
|
||||
background: var(--theme-colors-background);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
background: var(--theme-colors-background);
|
||||
}
|
||||
145
src/shared/components/modal/modal.tsx
Normal file
145
src/shared/components/modal/modal.tsx
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import { Modal as MantineModal, ModalProps as MantineModalProps } from '@mantine/core';
|
||||
import { closeAllModals, ContextModalProps } from '@mantine/modals';
|
||||
import {
|
||||
ModalsProvider as MantineModalsProvider,
|
||||
ModalsProviderProps as MantineModalsProviderProps,
|
||||
} from '@mantine/modals';
|
||||
import React, { ReactNode } from 'react';
|
||||
|
||||
import styles from './modal.module.css';
|
||||
|
||||
import { Button } from '/@/shared/components/button/button';
|
||||
import { Flex } from '/@/shared/components/flex/flex';
|
||||
import { Group } from '/@/shared/components/group/group';
|
||||
import { Icon } from '/@/shared/components/icon/icon';
|
||||
import { Stack } from '/@/shared/components/stack/stack';
|
||||
|
||||
export interface ModalProps extends Omit<MantineModalProps, 'onClose'> {
|
||||
children?: ReactNode;
|
||||
handlers: {
|
||||
close: () => void;
|
||||
open: () => void;
|
||||
toggle: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export const Modal = ({ children, classNames, handlers, ...rest }: ModalProps) => {
|
||||
return (
|
||||
<MantineModal
|
||||
{...rest}
|
||||
classNames={{
|
||||
body: styles.body,
|
||||
content: styles.content,
|
||||
header: styles.header,
|
||||
root: styles.root,
|
||||
title: styles.title,
|
||||
...classNames,
|
||||
}}
|
||||
onClose={handlers.close}
|
||||
radius="lg"
|
||||
transitionProps={{
|
||||
duration: 300,
|
||||
exitDuration: 300,
|
||||
transition: 'fade',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MantineModal>
|
||||
);
|
||||
};
|
||||
|
||||
export type ContextModalVars = {
|
||||
context: ContextModalProps['context'];
|
||||
id: ContextModalProps['id'];
|
||||
};
|
||||
|
||||
export const BaseContextModal = ({
|
||||
context,
|
||||
id,
|
||||
innerProps,
|
||||
}: ContextModalProps<{
|
||||
modalBody: (vars: ContextModalVars) => React.ReactNode;
|
||||
}>) => <>{innerProps.modalBody({ context, id })}</>;
|
||||
|
||||
interface ConfirmModalProps {
|
||||
children: ReactNode;
|
||||
disabled?: boolean;
|
||||
labels?: {
|
||||
cancel?: string;
|
||||
confirm?: string;
|
||||
};
|
||||
loading?: boolean;
|
||||
onCancel?: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
export const ConfirmModal = ({
|
||||
children,
|
||||
disabled,
|
||||
labels,
|
||||
loading,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: ConfirmModalProps) => {
|
||||
const handleCancel = () => {
|
||||
if (onCancel) {
|
||||
onCancel();
|
||||
} else {
|
||||
closeAllModals();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
<Flex>{children}</Flex>
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
data-focus
|
||||
onClick={handleCancel}
|
||||
variant="default"
|
||||
>
|
||||
{labels?.cancel ? labels.cancel : 'Cancel'}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
loading={loading}
|
||||
onClick={onConfirm}
|
||||
variant="filled"
|
||||
>
|
||||
{labels?.confirm ? labels.confirm : 'Confirm'}
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export interface ModalsProviderProps extends MantineModalsProviderProps {}
|
||||
|
||||
export const ModalsProvider = ({ children, ...rest }: ModalsProviderProps) => {
|
||||
return (
|
||||
<MantineModalsProvider
|
||||
modalProps={{
|
||||
centered: true,
|
||||
classNames: {
|
||||
body: styles.body,
|
||||
content: styles.content,
|
||||
header: styles.header,
|
||||
root: styles.root,
|
||||
title: styles.title,
|
||||
},
|
||||
closeButtonProps: {
|
||||
icon: <Icon icon="x" />,
|
||||
},
|
||||
radius: 'lg',
|
||||
transitionProps: {
|
||||
duration: 300,
|
||||
exitDuration: 300,
|
||||
transition: 'fade',
|
||||
},
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</MantineModalsProvider>
|
||||
);
|
||||
};
|
||||
59
src/shared/components/multi-select/multi-select.module.css
Normal file
59
src/shared/components/multi-select/multi-select.module.css
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
.root {
|
||||
& [data-disabled='true'] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: var(--theme-spacing-sm);
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
padding: var(--theme-spacing-xs);
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
background: var(--theme-colors-surface);
|
||||
border: 1px solid var(--theme-colors-border);
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&[data-variant='default'] {
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
background: var(--theme-colors-surface);
|
||||
}
|
||||
|
||||
&[data-variant='filled'] {
|
||||
color: var(--theme-colors-foreground);
|
||||
background: var(--theme-colors-background);
|
||||
}
|
||||
}
|
||||
|
||||
.input:focus,
|
||||
.input:focus-visible {
|
||||
border-color: lighten(var(--theme-colors-border), 10%);
|
||||
}
|
||||
|
||||
.option {
|
||||
position: relative;
|
||||
padding: var(--theme-spacing-sm) var(--theme-spacing-md);
|
||||
|
||||
&[data-checked='true'] {
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 2px;
|
||||
width: 4px;
|
||||
height: 50%;
|
||||
content: '';
|
||||
background-color: var(--theme-colors-primary-filled);
|
||||
border-radius: var(--theme-border-radius-xl);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background: lighten(var(--theme-colors-surface), 5%);
|
||||
}
|
||||
36
src/shared/components/multi-select/multi-select.tsx
Normal file
36
src/shared/components/multi-select/multi-select.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import {
|
||||
MultiSelect as MantineMultiSelect,
|
||||
MultiSelectProps as MantineMultiSelectProps,
|
||||
} from '@mantine/core';
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
import styles from './multi-select.module.css';
|
||||
|
||||
export interface MultiSelectProps extends MantineMultiSelectProps {
|
||||
maxWidth?: CSSProperties['maxWidth'];
|
||||
width?: CSSProperties['width'];
|
||||
}
|
||||
|
||||
export const MultiSelect = ({
|
||||
classNames,
|
||||
maxWidth,
|
||||
variant = 'default',
|
||||
width,
|
||||
...props
|
||||
}: MultiSelectProps) => {
|
||||
return (
|
||||
<MantineMultiSelect
|
||||
classNames={{
|
||||
dropdown: styles.dropdown,
|
||||
input: styles.input,
|
||||
option: styles.option,
|
||||
root: styles.root,
|
||||
...classNames,
|
||||
}}
|
||||
style={{ maxWidth, width }}
|
||||
variant={variant}
|
||||
withCheckIcon={false}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
46
src/shared/components/number-input/number-input.module.css
Normal file
46
src/shared/components/number-input/number-input.module.css
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
.root {
|
||||
transition: width 0.3s ease-in-out;
|
||||
|
||||
&[data-disabled='true'] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&[data-variant='default'] {
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
background: var(--theme-colors-surface);
|
||||
}
|
||||
|
||||
&[data-variant='filled'] {
|
||||
color: var(--theme-colors-foreground);
|
||||
background: var(--theme-colors-background);
|
||||
}
|
||||
}
|
||||
|
||||
.input:focus,
|
||||
.input:focus-visible {
|
||||
border-color: lighten(var(--theme-colors-border), 10%);
|
||||
}
|
||||
|
||||
.control {
|
||||
svg {
|
||||
color: var(--theme-btn-default-fg);
|
||||
fill: var(--theme-btn-default-fg);
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
color: var(--theme-colors-foreground-muted);
|
||||
}
|
||||
|
||||
.required {
|
||||
color: var(--theme-colors-state-error);
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: var(--theme-spacing-sm);
|
||||
}
|
||||
51
src/shared/components/number-input/number-input.tsx
Normal file
51
src/shared/components/number-input/number-input.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import {
|
||||
NumberInput as MantineNumberInput,
|
||||
NumberInputProps as MantineNumberInputProps,
|
||||
} from '@mantine/core';
|
||||
import { CSSProperties, forwardRef } from 'react';
|
||||
|
||||
import styles from './number-input.module.css';
|
||||
|
||||
export interface NumberInputProps extends MantineNumberInputProps {
|
||||
maxWidth?: CSSProperties['maxWidth'];
|
||||
width?: CSSProperties['width'];
|
||||
}
|
||||
|
||||
export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
classNames,
|
||||
maxWidth,
|
||||
size = 'sm',
|
||||
style,
|
||||
variant = 'default',
|
||||
width,
|
||||
...props
|
||||
}: NumberInputProps,
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<MantineNumberInput
|
||||
classNames={{
|
||||
control: styles.control,
|
||||
input: styles.input,
|
||||
label: styles.label,
|
||||
required: styles.required,
|
||||
root: styles.root,
|
||||
section: styles.section,
|
||||
wrapper: styles.wrapper,
|
||||
...classNames,
|
||||
}}
|
||||
hideControls
|
||||
ref={ref}
|
||||
size={size}
|
||||
style={{ maxWidth, width, ...style }}
|
||||
variant={variant}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MantineNumberInput>
|
||||
);
|
||||
},
|
||||
);
|
||||
3
src/shared/components/option/option.module.css
Normal file
3
src/shared/components/option/option.module.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.root {
|
||||
padding: var(--theme-spacing-sm);
|
||||
}
|
||||
42
src/shared/components/option/option.tsx
Normal file
42
src/shared/components/option/option.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { ReactNode } from 'react';
|
||||
|
||||
import styles from './option.module.css';
|
||||
|
||||
import { Flex } from '/@/shared/components/flex/flex';
|
||||
import { Group, GroupProps } from '/@/shared/components/group/group';
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
|
||||
interface OptionProps extends GroupProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const Option = ({ children, ...props }: OptionProps) => {
|
||||
return (
|
||||
<Group
|
||||
classNames={{ root: styles.root }}
|
||||
grow
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
|
||||
interface LabelProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const Label = ({ children }: LabelProps) => {
|
||||
return <Text>{children}</Text>;
|
||||
};
|
||||
|
||||
interface ControlProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
const Control = ({ children }: ControlProps) => {
|
||||
return <Flex justify="flex-end">{children}</Flex>;
|
||||
};
|
||||
|
||||
Option.Label = Label;
|
||||
Option.Control = Control;
|
||||
31
src/shared/components/pagination/pagination.module.css
Normal file
31
src/shared/components/pagination/pagination.module.css
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
.control {
|
||||
color: var(--theme-btn-default-fg);
|
||||
background-color: var(--theme-btn-default-bg);
|
||||
border: none;
|
||||
transition:
|
||||
background 0.2s ease-in-out,
|
||||
color 0.2s ease-in-out;
|
||||
|
||||
&[data-active] {
|
||||
color: var(--theme-btn-primary-fg);
|
||||
background-color: var(--theme-btn-primary-bg);
|
||||
}
|
||||
|
||||
&[data-dots] {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-btn-default-fg-hover);
|
||||
background-color: var(--theme-btn-default-bg-hover);
|
||||
|
||||
&[data-active] {
|
||||
color: var(--theme-btn-primary-fg-hover);
|
||||
background-color: var(--theme-btn-primary-bg-hover);
|
||||
}
|
||||
|
||||
&[data-dots] {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/shared/components/pagination/pagination.tsx
Normal file
24
src/shared/components/pagination/pagination.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import {
|
||||
Pagination as MantinePagination,
|
||||
PaginationProps as MantinePaginationProps,
|
||||
} from '@mantine/core';
|
||||
|
||||
import styles from './pagination.module.css';
|
||||
|
||||
interface PaginationProps extends MantinePaginationProps {}
|
||||
|
||||
export const Pagination = ({ classNames, style, ...props }: PaginationProps) => {
|
||||
return (
|
||||
<MantinePagination
|
||||
classNames={{
|
||||
control: styles.control,
|
||||
...classNames,
|
||||
}}
|
||||
radius="xl"
|
||||
style={{
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
4
src/shared/components/paper/paper.module.css
Normal file
4
src/shared/components/paper/paper.module.css
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
.root {
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
background: var(--theme-colors-surface);
|
||||
}
|
||||
27
src/shared/components/paper/paper.tsx
Normal file
27
src/shared/components/paper/paper.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import type { PaperProps as MantinePaperProps } from '@mantine/core';
|
||||
|
||||
import { Paper as MantinePaper } from '@mantine/core';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
import styles from './paper.module.css';
|
||||
|
||||
export interface PaperProps extends MantinePaperProps {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const Paper = ({ children, classNames, style, ...props }: PaperProps) => {
|
||||
return (
|
||||
<MantinePaper
|
||||
classNames={{
|
||||
root: styles.root,
|
||||
...classNames,
|
||||
}}
|
||||
style={{
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MantinePaper>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
.root {
|
||||
&[data-disabled='true'] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
transition: width 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&[data-variant='default'] {
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
background: var(--theme-colors-surface);
|
||||
}
|
||||
|
||||
&[data-variant='filled'] {
|
||||
color: var(--theme-colors-foreground);
|
||||
background: var(--theme-colors-background);
|
||||
}
|
||||
}
|
||||
|
||||
.input:focus,
|
||||
.input:focus-visible {
|
||||
border-color: lighten(var(--theme-colors-border), 10%);
|
||||
}
|
||||
|
||||
.section {
|
||||
color: var(--theme-colors-foreground-muted);
|
||||
}
|
||||
|
||||
.required {
|
||||
color: var(--theme-colors-state-error);
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: var(--theme-spacing-sm);
|
||||
}
|
||||
35
src/shared/components/password-input/password-input.tsx
Normal file
35
src/shared/components/password-input/password-input.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import {
|
||||
PasswordInput as MantinePasswordInput,
|
||||
PasswordInputProps as MantinePasswordInputProps,
|
||||
} from '@mantine/core';
|
||||
import { CSSProperties, forwardRef } from 'react';
|
||||
|
||||
import styles from './password-input.module.css';
|
||||
|
||||
export interface PasswordInputProps extends MantinePasswordInputProps {
|
||||
maxWidth?: CSSProperties['maxWidth'];
|
||||
width?: CSSProperties['width'];
|
||||
}
|
||||
|
||||
export const PasswordInput = forwardRef<HTMLInputElement, PasswordInputProps>(
|
||||
({ children, classNames, maxWidth, style, variant = 'default', width, ...props }, ref) => {
|
||||
return (
|
||||
<MantinePasswordInput
|
||||
classNames={{
|
||||
input: styles.input,
|
||||
label: styles.label,
|
||||
required: styles.required,
|
||||
root: styles.root,
|
||||
section: styles.section,
|
||||
...classNames,
|
||||
}}
|
||||
ref={ref}
|
||||
style={{ maxWidth, width, ...style }}
|
||||
variant={variant}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MantinePasswordInput>
|
||||
);
|
||||
},
|
||||
);
|
||||
7
src/shared/components/popover/popover.module.css
Normal file
7
src/shared/components/popover/popover.module.css
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
.dropdown {
|
||||
padding: var(--theme-spacing-xs);
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
background: var(--theme-colors-surface);
|
||||
border: 1px solid var(--theme-colors-border);
|
||||
filter: drop-shadow(0 0 5px rgb(0 0 0 / 50%));
|
||||
}
|
||||
29
src/shared/components/popover/popover.tsx
Normal file
29
src/shared/components/popover/popover.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import type {
|
||||
PopoverDropdownProps as MantinePopoverDropdownProps,
|
||||
PopoverProps as MantinePopoverProps,
|
||||
} from '@mantine/core';
|
||||
|
||||
import { Popover as MantinePopover } from '@mantine/core';
|
||||
|
||||
import styles from './popover.module.css';
|
||||
|
||||
export interface PopoverDropdownProps extends MantinePopoverDropdownProps {}
|
||||
export interface PopoverProps extends MantinePopoverProps {}
|
||||
|
||||
export const Popover = ({ children, ...props }: PopoverProps) => {
|
||||
return (
|
||||
<MantinePopover
|
||||
classNames={{
|
||||
dropdown: styles.dropdown,
|
||||
}}
|
||||
transitionProps={{ transition: 'fade' }}
|
||||
withinPortal
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MantinePopover>
|
||||
);
|
||||
};
|
||||
|
||||
Popover.Target = MantinePopover.Target;
|
||||
Popover.Dropdown = MantinePopover.Dropdown;
|
||||
7
src/shared/components/portal/portal.tsx
Normal file
7
src/shared/components/portal/portal.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import { Portal as MantinePortal, PortalProps as MantinePortalProps } from '@mantine/core';
|
||||
|
||||
export interface PortalProps extends MantinePortalProps {}
|
||||
|
||||
export const Portal = ({ children, ...props }: PortalProps) => {
|
||||
return <MantinePortal {...props}>{children}</MantinePortal>;
|
||||
};
|
||||
5
src/shared/components/rating/rating.module.css
Normal file
5
src/shared/components/rating/rating.module.css
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
.symbol-body {
|
||||
svg {
|
||||
stroke: var(--theme-colors-foreground-muted);
|
||||
}
|
||||
}
|
||||
44
src/shared/components/rating/rating.tsx
Normal file
44
src/shared/components/rating/rating.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { Rating as MantineRating, RatingProps as MantineRatingProps } from '@mantine/core';
|
||||
import debounce from 'lodash/debounce';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import styles from './rating.module.css';
|
||||
|
||||
interface RatingProps extends MantineRatingProps {}
|
||||
|
||||
export const Rating = ({ classNames, onChange, style, ...props }: RatingProps) => {
|
||||
const valueChange = useCallback(
|
||||
(rating: number) => {
|
||||
if (onChange) {
|
||||
if (rating === props.value) {
|
||||
onChange(0);
|
||||
} else {
|
||||
onChange(rating);
|
||||
}
|
||||
}
|
||||
},
|
||||
[onChange, props.value],
|
||||
);
|
||||
|
||||
const debouncedOnChange = debounce(valueChange, 100);
|
||||
|
||||
return (
|
||||
<MantineRating
|
||||
classNames={{
|
||||
symbolBody: styles.symbolBody,
|
||||
...classNames,
|
||||
}}
|
||||
style={{
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
onChange={(e) => {
|
||||
debouncedOnChange(e);
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
11
src/shared/components/scroll-area/scroll-area.css
Normal file
11
src/shared/components/scroll-area/scroll-area.css
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
.feishin-os-scrollbar {
|
||||
--os-size: var(--theme-scrollbar-size);
|
||||
--os-track-bg: var(--theme-scrollbar-track-background);
|
||||
--os-track-bg-hover: var(--theme-scrollbar-track-hover-background);
|
||||
--os-track-border-radius: var(--theme-scrollbar-track-border-radius);
|
||||
--os-handle-bg: var(--theme-scrollbar-handle-background);
|
||||
--os-handle-bg-hover: var(--theme-scrollbar-handle-hover-background);
|
||||
--os-handle-bg-active: var(--theme-scrollbar-handle-active-background);
|
||||
--os-handle-border-radius: var(--theme-scrollbar-handle-border-radius);
|
||||
--os-handle-max-size: 200px;
|
||||
}
|
||||
35
src/shared/components/scroll-area/scroll-area.module.css
Normal file
35
src/shared/components/scroll-area/scroll-area.module.css
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/* .thumb {
|
||||
background: var(--theme-scrollbar-handle-background);
|
||||
border-radius: var(--theme-scrollbar-handle-border-radius);
|
||||
|
||||
&:hover {
|
||||
background: var(--theme-scrollbar-handle-hover-background);
|
||||
}
|
||||
|
||||
&[data-state='visible'] {
|
||||
animation: fade-in 0.3s forwards;
|
||||
}
|
||||
|
||||
&[data-state='hidden'] {
|
||||
animation: fade-out 0.2s forwards;
|
||||
}
|
||||
}
|
||||
|
||||
.scrollbar {
|
||||
padding: 0;
|
||||
background: var(--theme-scrollbar-track-background);
|
||||
border-radius: var(--theme-scrollbar-track-border-radius);
|
||||
|
||||
&:hover {
|
||||
background: var(--theme-scrollbar-track-hover-background);
|
||||
}
|
||||
}
|
||||
|
||||
.viewport > div {
|
||||
display: block !important;
|
||||
} */
|
||||
|
||||
.scroll-area {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
78
src/shared/components/scroll-area/scroll-area.tsx
Normal file
78
src/shared/components/scroll-area/scroll-area.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import { autoScrollForElements } from '@atlaskit/pragmatic-drag-and-drop-auto-scroll/element';
|
||||
import { useMergedRef } from '@mantine/hooks';
|
||||
import clsx from 'clsx';
|
||||
import { useOverlayScrollbars } from 'overlayscrollbars-react';
|
||||
import { forwardRef, Ref, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import styles from './scroll-area.module.css';
|
||||
import './scroll-area.css';
|
||||
|
||||
import { DragData, DragTarget } from '/@/shared/types/drag-and-drop';
|
||||
|
||||
interface ScrollAreaProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||
allowDragScroll?: boolean;
|
||||
debugScrollPosition?: boolean;
|
||||
scrollHideDelay?: number;
|
||||
}
|
||||
|
||||
export const ScrollArea = forwardRef((props: ScrollAreaProps, ref: Ref<HTMLDivElement>) => {
|
||||
const { allowDragScroll, children, className, scrollHideDelay, ...htmlProps } = props;
|
||||
|
||||
const containerRef = useRef(null);
|
||||
const [scroller, setScroller] = useState<HTMLElement | null | Window>(null);
|
||||
|
||||
const [initialize, osInstance] = useOverlayScrollbars({
|
||||
defer: false,
|
||||
options: {
|
||||
overflow: { x: 'hidden', y: 'scroll' },
|
||||
scrollbars: {
|
||||
autoHide: 'leave',
|
||||
autoHideDelay: scrollHideDelay || 500,
|
||||
pointers: ['mouse', 'pen', 'touch'],
|
||||
theme: 'feishin-os-scrollbar',
|
||||
visibility: 'visible',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const { current: root } = containerRef;
|
||||
|
||||
if (scroller && root) {
|
||||
initialize({
|
||||
elements: { viewport: scroller as HTMLElement },
|
||||
target: root,
|
||||
});
|
||||
|
||||
if (allowDragScroll) {
|
||||
autoScrollForElements({
|
||||
canScroll: (args) => {
|
||||
const data = args.source.data as unknown as DragData<unknown>;
|
||||
if (data.type === DragTarget.TABLE_COLUMN) return false;
|
||||
return true;
|
||||
},
|
||||
element: scroller as HTMLElement,
|
||||
getAllowedAxis: () => 'vertical',
|
||||
getConfiguration: () => ({ maxScrollSpeed: 'standard' }),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return () => osInstance()?.destroy();
|
||||
}, [allowDragScroll, initialize, osInstance, scroller]);
|
||||
|
||||
const mergedRef = useMergedRef(ref, containerRef);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(styles.scrollArea, className)}
|
||||
ref={(el) => {
|
||||
setScroller(el);
|
||||
mergedRef(el);
|
||||
}}
|
||||
{...htmlProps}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
.root {
|
||||
background: var(--theme-colors-surface);
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import type { SegmentedControlProps as MantineSegmentedControlProps } from '@mantine/core';
|
||||
|
||||
import { SegmentedControl as MantineSegmentedControl } from '@mantine/core';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
import styles from './segmented-control.module.css';
|
||||
|
||||
type SegmentedControlProps = MantineSegmentedControlProps;
|
||||
|
||||
export const SegmentedControl = forwardRef<HTMLDivElement, SegmentedControlProps>(
|
||||
({ classNames, size = 'sm', ...props }: SegmentedControlProps, ref) => {
|
||||
return (
|
||||
<MantineSegmentedControl
|
||||
classNames={{
|
||||
control: styles.control,
|
||||
indicator: styles.indicator,
|
||||
label: styles.label,
|
||||
root: styles.root,
|
||||
...classNames,
|
||||
}}
|
||||
ref={ref}
|
||||
size={size}
|
||||
transitionDuration={250}
|
||||
transitionTimingFunction="linear"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
59
src/shared/components/select/select.module.css
Normal file
59
src/shared/components/select/select.module.css
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
.root {
|
||||
& [data-disabled='true'] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&[data-variant='default'] {
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
background: var(--theme-colors-surface);
|
||||
}
|
||||
|
||||
&[data-variant='filled'] {
|
||||
color: var(--theme-colors-foreground);
|
||||
background: var(--theme-colors-background);
|
||||
}
|
||||
}
|
||||
|
||||
.input:focus,
|
||||
.input:focus-visible {
|
||||
border-color: lighten(var(--theme-colors-border), 10%);
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: var(--theme-spacing-sm);
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
padding: var(--theme-spacing-xs);
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
background: var(--theme-colors-surface);
|
||||
border: 1px solid var(--theme-colors-border);
|
||||
}
|
||||
|
||||
.option {
|
||||
position: relative;
|
||||
padding: var(--theme-spacing-sm) var(--theme-spacing-lg);
|
||||
|
||||
&[data-checked='true'] {
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 2px;
|
||||
width: 4px;
|
||||
height: 50%;
|
||||
content: '';
|
||||
background-color: var(--theme-colors-primary-filled);
|
||||
border-radius: var(--theme-border-radius-xl);
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background: lighten(var(--theme-colors-surface), 5%);
|
||||
}
|
||||
36
src/shared/components/select/select.tsx
Normal file
36
src/shared/components/select/select.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import type { SelectProps as MantineSelectProps } from '@mantine/core';
|
||||
|
||||
import { Select as MantineSelect } from '@mantine/core';
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
import styles from './select.module.css';
|
||||
|
||||
export interface SelectProps extends MantineSelectProps {
|
||||
maxWidth?: CSSProperties['maxWidth'];
|
||||
width?: CSSProperties['width'];
|
||||
}
|
||||
|
||||
export const Select = ({
|
||||
classNames,
|
||||
maxWidth,
|
||||
variant = 'default',
|
||||
width,
|
||||
...props
|
||||
}: SelectProps) => {
|
||||
return (
|
||||
<MantineSelect
|
||||
classNames={{
|
||||
dropdown: styles.dropdown,
|
||||
input: styles.input,
|
||||
label: styles.label,
|
||||
option: styles.option,
|
||||
root: styles.root,
|
||||
...classNames,
|
||||
}}
|
||||
style={{ maxWidth, width }}
|
||||
variant={variant}
|
||||
withCheckIcon={false}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
0
src/shared/components/separator/separator.module.css
Normal file
0
src/shared/components/separator/separator.module.css
Normal file
5
src/shared/components/separator/separator.tsx
Normal file
5
src/shared/components/separator/separator.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { SEPARATOR_STRING } from '/@/shared/api/utils';
|
||||
|
||||
export const Separator = () => {
|
||||
return <>{SEPARATOR_STRING}</>;
|
||||
};
|
||||
27
src/shared/components/skeleton/skeleton.module.css
Normal file
27
src/shared/components/skeleton/skeleton.module.css
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
.skeleton {
|
||||
@mixin dark {
|
||||
--base-color: lighten(var(--theme-colors-surface), 10%) !important;
|
||||
--highlight-color: lighten(var(--theme-colors-surface), 15%) !important;
|
||||
}
|
||||
|
||||
@mixin light {
|
||||
--base-color: var(--theme-colors-foreground-muted) !important;
|
||||
--highlight-color: darken(var(--theme-colors-foreground-muted), 40%) !important;
|
||||
}
|
||||
|
||||
--animation-duration: 1.5s !important;
|
||||
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.skeleton-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.centered {
|
||||
justify-content: center;
|
||||
}
|
||||
56
src/shared/components/skeleton/skeleton.tsx
Normal file
56
src/shared/components/skeleton/skeleton.tsx
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import type { CSSProperties } from 'react';
|
||||
|
||||
import clsx from 'clsx';
|
||||
import RSkeleton from 'react-loading-skeleton';
|
||||
|
||||
import styles from './skeleton.module.css';
|
||||
|
||||
import 'react-loading-skeleton/dist/skeleton.css';
|
||||
|
||||
interface SkeletonProps {
|
||||
baseColor?: string;
|
||||
borderRadius?: string;
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
count?: number;
|
||||
direction?: 'ltr' | 'rtl';
|
||||
enableAnimation?: boolean;
|
||||
height?: number | string;
|
||||
inline?: boolean;
|
||||
isCentered?: boolean;
|
||||
style?: CSSProperties;
|
||||
width?: number | string;
|
||||
}
|
||||
|
||||
export function Skeleton({
|
||||
baseColor,
|
||||
borderRadius,
|
||||
className,
|
||||
containerClassName,
|
||||
count,
|
||||
direction,
|
||||
enableAnimation = true,
|
||||
height,
|
||||
inline,
|
||||
isCentered,
|
||||
style,
|
||||
width,
|
||||
}: SkeletonProps) {
|
||||
return (
|
||||
<RSkeleton
|
||||
baseColor={baseColor}
|
||||
borderRadius={borderRadius}
|
||||
className={clsx(styles.skeleton, className)}
|
||||
containerClassName={clsx(styles.skeletonContainer, containerClassName, {
|
||||
[styles.centered]: isCentered,
|
||||
})}
|
||||
count={count}
|
||||
direction={direction}
|
||||
enableAnimation={enableAnimation}
|
||||
height={height}
|
||||
inline={inline}
|
||||
style={style}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
}
|
||||
17
src/shared/components/slider/slider.module.css
Normal file
17
src/shared/components/slider/slider.module.css
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
.track {
|
||||
height: 0.5rem;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
background: var(--theme-colors-foreground);
|
||||
}
|
||||
|
||||
.label {
|
||||
max-width: 200px;
|
||||
padding: var(--theme-spacing-sm) var(--theme-spacing-md);
|
||||
font-size: var(--theme-font-size-md);
|
||||
font-weight: 550;
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
background: var(--theme-colors-surface);
|
||||
box-shadow: 4px 4px 10px 0 rgb(0 0 0 / 20%);
|
||||
}
|
||||
25
src/shared/components/slider/slider.tsx
Normal file
25
src/shared/components/slider/slider.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import type { SliderProps as MantineSliderProps } from '@mantine/core';
|
||||
|
||||
import { Slider as MantineSlider } from '@mantine/core';
|
||||
|
||||
import styles from './slider.module.css';
|
||||
|
||||
export interface SliderProps extends MantineSliderProps {}
|
||||
|
||||
export const Slider = ({ classNames, style, ...props }: SliderProps) => {
|
||||
return (
|
||||
<MantineSlider
|
||||
classNames={{
|
||||
bar: styles.bar,
|
||||
label: styles.label,
|
||||
thumb: styles.thumb,
|
||||
track: styles.track,
|
||||
...classNames,
|
||||
}}
|
||||
style={{
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
18
src/shared/components/spinner/spinner.module.css
Normal file
18
src/shared/components/spinner/spinner.module.css
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.icon {
|
||||
animation: rotating 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
35
src/shared/components/spinner/spinner.tsx
Normal file
35
src/shared/components/spinner/spinner.tsx
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { Center } from '@mantine/core';
|
||||
import { IconBaseProps } from 'react-icons';
|
||||
import { RiLoader5Fill } from 'react-icons/ri';
|
||||
|
||||
import styles from './spinner.module.css';
|
||||
|
||||
interface SpinnerProps extends IconBaseProps {
|
||||
color?: string;
|
||||
container?: boolean;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export const SpinnerIcon = RiLoader5Fill;
|
||||
|
||||
export const Spinner = ({ ...props }: SpinnerProps) => {
|
||||
if (props.container) {
|
||||
return (
|
||||
<Center className={styles.container}>
|
||||
<SpinnerIcon
|
||||
className={styles.icon}
|
||||
color={props.color}
|
||||
size={props.size}
|
||||
/>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SpinnerIcon
|
||||
className={styles.icon}
|
||||
color={props.color}
|
||||
size={props.size}
|
||||
/>
|
||||
);
|
||||
};
|
||||
31
src/shared/components/spoiler/spoiler.module.css
Normal file
31
src/shared/components/spoiler/spoiler.module.css
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
.control:hover {
|
||||
color: var(--theme-btn-subtle-fg-hover);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.spoiler {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.spoiler:not(.is-expanded).can-expand::after {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
content: '';
|
||||
background: linear-gradient(to top, var(--theme-colors-background) 10%, transparent 60%);
|
||||
}
|
||||
|
||||
.spoiler.can-expand {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.spoiler.is-expanded {
|
||||
max-height: 2500px !important;
|
||||
}
|
||||
42
src/shared/components/spoiler/spoiler.tsx
Normal file
42
src/shared/components/spoiler/spoiler.tsx
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import clsx from 'clsx';
|
||||
import { HTMLAttributes, ReactNode, useRef, useState } from 'react';
|
||||
|
||||
import styles from './spoiler.module.css';
|
||||
|
||||
import { Text } from '/@/shared/components/text/text';
|
||||
import { useIsOverflow } from '/@/shared/hooks/use-is-overflow';
|
||||
|
||||
interface SpoilerProps extends HTMLAttributes<HTMLDivElement> {
|
||||
children?: ReactNode;
|
||||
defaultOpened?: boolean;
|
||||
maxHeight?: number;
|
||||
}
|
||||
|
||||
export const Spoiler = ({ children, defaultOpened, maxHeight, ...props }: SpoilerProps) => {
|
||||
const ref = useRef(null);
|
||||
const isOverflow = useIsOverflow(ref);
|
||||
const [isExpanded, setIsExpanded] = useState(!!defaultOpened);
|
||||
|
||||
const spoilerClassNames = clsx(styles.spoiler, {
|
||||
[styles.canExpand]: isOverflow,
|
||||
[styles.isExpanded]: isExpanded,
|
||||
});
|
||||
|
||||
const handleToggleExpand = () => {
|
||||
setIsExpanded((val) => !val);
|
||||
};
|
||||
|
||||
return (
|
||||
<Text
|
||||
className={spoilerClassNames}
|
||||
onClick={handleToggleExpand}
|
||||
ref={ref}
|
||||
role="button"
|
||||
style={{ maxHeight: maxHeight ?? '100px', whiteSpace: 'pre-wrap' }}
|
||||
tabIndex={-1}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Text>
|
||||
);
|
||||
};
|
||||
17
src/shared/components/stack/stack.tsx
Normal file
17
src/shared/components/stack/stack.tsx
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { Stack as MantineStack, StackProps as MantineStackProps } from '@mantine/core';
|
||||
import { forwardRef } from 'react';
|
||||
|
||||
export interface StackProps extends MantineStackProps {}
|
||||
|
||||
export const Stack = forwardRef<HTMLDivElement, StackProps>(({ children, ...props }, ref) => {
|
||||
return (
|
||||
<MantineStack
|
||||
classNames={{ ...props.classNames }}
|
||||
ref={ref}
|
||||
style={{ ...props.style }}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MantineStack>
|
||||
);
|
||||
});
|
||||
16
src/shared/components/switch/switch.module.css
Normal file
16
src/shared/components/switch/switch.module.css
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
.thumb {
|
||||
background: var(--theme-colors-foreground);
|
||||
}
|
||||
|
||||
.track {
|
||||
background-color: var(--theme-colors-surface);
|
||||
border: 1px solid var(--theme-colors-border);
|
||||
|
||||
input:checked + & {
|
||||
background-color: var(--theme-colors-primary-filled);
|
||||
|
||||
& > .thumb {
|
||||
background: var(--theme-colors-primary-contrast);
|
||||
}
|
||||
}
|
||||
}
|
||||
23
src/shared/components/switch/switch.tsx
Normal file
23
src/shared/components/switch/switch.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import type { SwitchProps as MantineSwitchProps } from '@mantine/core';
|
||||
|
||||
import { Switch as MantineSwitch } from '@mantine/core';
|
||||
|
||||
import styles from './switch.module.css';
|
||||
|
||||
type SwitchProps = MantineSwitchProps;
|
||||
|
||||
export const Switch = ({ classNames, ...props }: SwitchProps) => {
|
||||
return (
|
||||
<MantineSwitch
|
||||
classNames={{
|
||||
input: styles.input,
|
||||
root: styles.root,
|
||||
thumb: styles.thumb,
|
||||
track: styles.track,
|
||||
...classNames,
|
||||
}}
|
||||
withThumbIndicator={false}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
7
src/shared/components/table/table.module.css
Normal file
7
src/shared/components/table/table.module.css
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
.td {
|
||||
padding: var(--theme-spacing-xs) var(--theme-spacing-sm);
|
||||
}
|
||||
|
||||
.th {
|
||||
padding: var(--theme-spacing-xs) var(--theme-spacing-sm);
|
||||
}
|
||||
24
src/shared/components/table/table.tsx
Normal file
24
src/shared/components/table/table.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { Table as MantineTable, TableProps as MantineTableProps } from '@mantine/core';
|
||||
|
||||
import styles from './table.module.css';
|
||||
|
||||
export interface TableProps extends MantineTableProps {}
|
||||
|
||||
export const Table = ({ classNames, ...props }: TableProps) => {
|
||||
return (
|
||||
<MantineTable
|
||||
classNames={{
|
||||
td: styles.td,
|
||||
th: styles.th,
|
||||
...classNames,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Table.Thead = MantineTable.Thead;
|
||||
Table.Tr = MantineTable.Tr;
|
||||
Table.Td = MantineTable.Td;
|
||||
Table.Th = MantineTable.Th;
|
||||
Table.Tbody = MantineTable.Tbody;
|
||||
37
src/shared/components/tabs/tabs.module.css
Normal file
37
src/shared/components/tabs/tabs.module.css
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
.root {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.list {
|
||||
padding-right: var(--theme-spacing-md);
|
||||
|
||||
&::before {
|
||||
border: 1px solid var(--theme-colors-border);
|
||||
}
|
||||
}
|
||||
|
||||
.tab {
|
||||
padding: var(--theme-spacing-md);
|
||||
font-weight: 500;
|
||||
color: var(--theme-btn-subtle-fg);
|
||||
transition: color 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
color: var(--theme-btn-subtle-fg-hover);
|
||||
background: var(--theme-btn-subtle-bg-hover);
|
||||
}
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding: var(--theme-spacing-lg) var(--theme-spacing-sm);
|
||||
}
|
||||
|
||||
.tab[data-active] {
|
||||
color: var(--theme-btn-subtle-fg);
|
||||
background: none;
|
||||
border-color: var(--theme-colors-primary-filled);
|
||||
|
||||
&:hover {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
34
src/shared/components/tabs/tabs.tsx
Normal file
34
src/shared/components/tabs/tabs.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import { Tabs as MantineTabs, TabsProps as MantineTabsProps, TabsPanelProps } from '@mantine/core';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import styles from './tabs.module.css';
|
||||
|
||||
type TabsProps = MantineTabsProps;
|
||||
|
||||
export const Tabs = ({ children, ...props }: TabsProps) => {
|
||||
return (
|
||||
<MantineTabs
|
||||
classNames={{
|
||||
list: styles.list,
|
||||
panel: styles.panel,
|
||||
root: styles.root,
|
||||
tab: styles.tab,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MantineTabs>
|
||||
);
|
||||
};
|
||||
|
||||
const Panel = ({ children, ...props }: TabsPanelProps) => {
|
||||
return (
|
||||
<MantineTabs.Panel {...props}>
|
||||
<Suspense fallback={<></>}>{children}</Suspense>
|
||||
</MantineTabs.Panel>
|
||||
);
|
||||
};
|
||||
|
||||
Tabs.List = MantineTabs.List;
|
||||
Tabs.Panel = Panel;
|
||||
Tabs.Tab = MantineTabs.Tab;
|
||||
39
src/shared/components/text-input/text-input.module.css
Normal file
39
src/shared/components/text-input/text-input.module.css
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
.root {
|
||||
transition: width 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&[data-variant='default'] {
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
background: var(--theme-colors-surface);
|
||||
}
|
||||
|
||||
&[data-variant='filled'] {
|
||||
color: var(--theme-colors-foreground);
|
||||
background: var(--theme-colors-background);
|
||||
}
|
||||
}
|
||||
|
||||
.input:focus,
|
||||
.input:focus-visible {
|
||||
border-color: lighten(var(--theme-colors-border), 10%);
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: var(--theme-spacing-sm);
|
||||
}
|
||||
|
||||
.section {
|
||||
color: var(--theme-colors-foreground-muted);
|
||||
}
|
||||
|
||||
.required {
|
||||
color: var(--theme-colors-state-error);
|
||||
}
|
||||
|
||||
.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
50
src/shared/components/text-input/text-input.tsx
Normal file
50
src/shared/components/text-input/text-input.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import {
|
||||
TextInput as MantineTextInput,
|
||||
TextInputProps as MantineTextInputProps,
|
||||
} from '@mantine/core';
|
||||
import { CSSProperties, forwardRef } from 'react';
|
||||
|
||||
import styles from './text-input.module.css';
|
||||
|
||||
export interface TextInputProps extends MantineTextInputProps {
|
||||
maxWidth?: CSSProperties['maxWidth'];
|
||||
width?: CSSProperties['width'];
|
||||
}
|
||||
|
||||
export const TextInput = forwardRef<HTMLInputElement, TextInputProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
classNames,
|
||||
maxWidth,
|
||||
size = 'sm',
|
||||
style,
|
||||
variant = 'default',
|
||||
width,
|
||||
...props
|
||||
}: TextInputProps,
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<MantineTextInput
|
||||
classNames={{
|
||||
input: styles.input,
|
||||
label: styles.label,
|
||||
required: styles.required,
|
||||
root: styles.root,
|
||||
section: styles.section,
|
||||
wrapper: styles.wrapper,
|
||||
...classNames,
|
||||
}}
|
||||
ref={ref}
|
||||
size={size}
|
||||
spellCheck={false}
|
||||
style={{ maxWidth, width, ...style }}
|
||||
variant={variant}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MantineTextInput>
|
||||
);
|
||||
},
|
||||
);
|
||||
27
src/shared/components/text-title/text-title.module.css
Normal file
27
src/shared/components/text-title/text-title.module.css
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
.root {
|
||||
color: var(--theme-colors-foreground);
|
||||
transition: color 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: var(--theme-colors-foreground-muted);
|
||||
}
|
||||
|
||||
.link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.link:hover {
|
||||
color: var(--theme-colors-foreground);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.no-select {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.overflow-hidden {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
49
src/shared/components/text-title/text-title.tsx
Normal file
49
src/shared/components/text-title/text-title.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import type { TitleProps as MantineTitleProps } from '@mantine/core';
|
||||
import type { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||
|
||||
import { createPolymorphicComponent, Title as MantineHeader } from '@mantine/core';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import styles from './text-title.module.css';
|
||||
|
||||
type MantineTextTitleDivProps = ComponentPropsWithoutRef<'div'> & MantineTitleProps;
|
||||
|
||||
interface TextTitleProps extends MantineTextTitleDivProps {
|
||||
children?: ReactNode;
|
||||
isLink?: boolean;
|
||||
isMuted?: boolean;
|
||||
isNoSelect?: boolean;
|
||||
overflow?: 'hidden' | 'visible';
|
||||
to?: string;
|
||||
weight?: number;
|
||||
}
|
||||
|
||||
const _TextTitle = ({
|
||||
children,
|
||||
className,
|
||||
isLink,
|
||||
isMuted,
|
||||
isNoSelect,
|
||||
overflow,
|
||||
...rest
|
||||
}: TextTitleProps) => {
|
||||
return (
|
||||
<MantineHeader
|
||||
className={clsx(
|
||||
styles.root,
|
||||
{
|
||||
[styles.link]: isLink,
|
||||
[styles.muted]: isMuted,
|
||||
[styles.noSelect]: isNoSelect,
|
||||
[styles.overflowHidden]: overflow === 'hidden' && !rest.lineClamp,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</MantineHeader>
|
||||
);
|
||||
};
|
||||
|
||||
export const TextTitle = createPolymorphicComponent<'div', TextTitleProps>(_TextTitle);
|
||||
28
src/shared/components/text/text.module.css
Normal file
28
src/shared/components/text/text.module.css
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
.root {
|
||||
font-family: var(--font-family);
|
||||
color: var(--theme-colors-foreground);
|
||||
user-select: auto;
|
||||
}
|
||||
|
||||
.root.muted {
|
||||
color: var(--theme-colors-foreground-muted);
|
||||
}
|
||||
|
||||
.root.link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.root.link:hover {
|
||||
color: var(--theme-colors-foreground);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.root.overflow-hidden {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.root.no-select {
|
||||
user-select: none;
|
||||
}
|
||||
54
src/shared/components/text/text.tsx
Normal file
54
src/shared/components/text/text.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { Text as MantineText, TextProps as MantineTextProps } from '@mantine/core';
|
||||
import clsx from 'clsx';
|
||||
import { ComponentPropsWithoutRef, ReactNode } from 'react';
|
||||
|
||||
import styles from './text.module.css';
|
||||
|
||||
import { createPolymorphicComponent } from '/@/shared/utils/create-polymorphic-component';
|
||||
|
||||
export interface TextProps extends MantineTextDivProps {
|
||||
children?: ReactNode;
|
||||
font?: Font;
|
||||
isLink?: boolean;
|
||||
isMuted?: boolean;
|
||||
isNoSelect?: boolean;
|
||||
overflow?: 'hidden' | 'visible';
|
||||
to?: string;
|
||||
weight?: number;
|
||||
}
|
||||
|
||||
type Font = 'Epilogue' | 'Gotham' | 'Inter' | 'Poppins';
|
||||
|
||||
type MantineTextDivProps = ComponentPropsWithoutRef<'div'> & MantineTextProps;
|
||||
|
||||
export const _Text = ({
|
||||
children,
|
||||
font,
|
||||
isLink,
|
||||
isMuted,
|
||||
isNoSelect,
|
||||
overflow,
|
||||
...rest
|
||||
}: TextProps) => {
|
||||
return (
|
||||
<MantineText
|
||||
className={clsx(styles.root, {
|
||||
[styles.link]: isLink,
|
||||
[styles.muted]: isMuted,
|
||||
[styles.noSelect]: isNoSelect,
|
||||
[styles.overflowHidden]: overflow === 'hidden',
|
||||
})}
|
||||
component="div"
|
||||
style={
|
||||
{
|
||||
'--font-family': font,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</MantineText>
|
||||
);
|
||||
};
|
||||
|
||||
export const Text = createPolymorphicComponent<'div', TextProps>(_Text);
|
||||
22
src/shared/components/textarea/textarea.module.css
Normal file
22
src/shared/components/textarea/textarea.module.css
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
.root {
|
||||
transition: width 0.3s ease-in-out;
|
||||
|
||||
&[data-disabled='true'] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
background: var(--theme-colors-surface);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
|
||||
.input:focus,
|
||||
.input:focus-visible {
|
||||
border-color: lighten(var(--theme-colors-border), 10%);
|
||||
}
|
||||
|
||||
.label {
|
||||
margin-bottom: var(--theme-spacing-sm);
|
||||
}
|
||||
31
src/shared/components/textarea/textarea.tsx
Normal file
31
src/shared/components/textarea/textarea.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { Textarea as MantineTextarea, TextareaProps as MantineTextareaProps } from '@mantine/core';
|
||||
import { CSSProperties, forwardRef } from 'react';
|
||||
|
||||
import styles from './textarea.module.css';
|
||||
|
||||
export interface TextareaProps extends MantineTextareaProps {
|
||||
maxWidth?: CSSProperties['maxWidth'];
|
||||
width?: CSSProperties['width'];
|
||||
}
|
||||
|
||||
export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
({ children, classNames, maxWidth, style, width, ...props }: TextareaProps, ref) => {
|
||||
return (
|
||||
<MantineTextarea
|
||||
classNames={{
|
||||
input: styles.input,
|
||||
label: styles.label,
|
||||
required: styles.required,
|
||||
root: styles.root,
|
||||
wrapper: styles.wrapper,
|
||||
...classNames,
|
||||
}}
|
||||
ref={ref}
|
||||
style={{ maxWidth, width, ...style }}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MantineTextarea>
|
||||
);
|
||||
},
|
||||
);
|
||||
40
src/shared/components/toast/toast.module.css
Normal file
40
src/shared/components/toast/toast.module.css
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
.root {
|
||||
bottom: 90px;
|
||||
background-color: var(--theme-colors-surface);
|
||||
}
|
||||
|
||||
.root.error {
|
||||
--notification-color: var(--theme-colors-state-error);
|
||||
}
|
||||
|
||||
.root.info {
|
||||
--notification-color: var(--theme-colors-state-info);
|
||||
}
|
||||
|
||||
.root.success {
|
||||
--notification-color: var(--theme-colors-state-success);
|
||||
}
|
||||
|
||||
.root.warning {
|
||||
--notification-color: var(--theme-colors-state-warning);
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: var(--theme-font-size-md);
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: var(--theme-spacing-md);
|
||||
}
|
||||
|
||||
.loader {
|
||||
margin: var(--theme-spacing-md);
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: var(--theme-font-size-md);
|
||||
}
|
||||
|
||||
.close-button {
|
||||
background-color: var(--theme-colors-surface);
|
||||
}
|
||||
61
src/shared/components/toast/toast.tsx
Normal file
61
src/shared/components/toast/toast.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import type { NotificationsProps as MantineNotificationProps } from '@mantine/notifications';
|
||||
|
||||
import {
|
||||
cleanNotifications,
|
||||
cleanNotificationsQueue,
|
||||
hideNotification,
|
||||
notifications,
|
||||
updateNotification,
|
||||
} from '@mantine/notifications';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import styles from './toast.module.css';
|
||||
|
||||
interface NotificationProps extends MantineNotificationProps {
|
||||
message?: string;
|
||||
onClose?: () => void;
|
||||
type?: 'error' | 'info' | 'success' | 'warning';
|
||||
}
|
||||
|
||||
const getTitle = (type: NotificationProps['type']) => {
|
||||
if (type === 'success') return 'Success';
|
||||
if (type === 'warning') return 'Warning';
|
||||
if (type === 'error') return 'Error';
|
||||
return 'Info';
|
||||
};
|
||||
|
||||
const showToast = ({ message, onClose, type, ...props }: NotificationProps) => {
|
||||
return notifications.show({
|
||||
autoClose: props.autoClose,
|
||||
classNames: {
|
||||
body: styles.body,
|
||||
closeButton: styles.closeButton,
|
||||
description: styles.description,
|
||||
loader: styles.loader,
|
||||
root: clsx(styles.root, {
|
||||
[styles.error]: type === 'error',
|
||||
[styles.info]: type === 'info',
|
||||
[styles.success]: type === 'success',
|
||||
[styles.warning]: type === 'warning',
|
||||
}),
|
||||
title: styles.title,
|
||||
},
|
||||
message: message ?? '',
|
||||
onClose,
|
||||
title: getTitle(type),
|
||||
withBorder: true,
|
||||
withCloseButton: true,
|
||||
});
|
||||
};
|
||||
|
||||
export const toast = {
|
||||
clean: cleanNotifications,
|
||||
cleanQueue: cleanNotificationsQueue,
|
||||
error: (props: NotificationProps) => showToast({ type: 'error', ...props }),
|
||||
hide: hideNotification,
|
||||
info: (props: NotificationProps) => showToast({ type: 'info', ...props }),
|
||||
show: showToast,
|
||||
success: (props: NotificationProps) => showToast({ type: 'success', ...props }),
|
||||
update: updateNotification,
|
||||
warn: (props: NotificationProps) => showToast({ type: 'warning', ...props }),
|
||||
};
|
||||
9
src/shared/components/tooltip/tooltip.module.css
Normal file
9
src/shared/components/tooltip/tooltip.module.css
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
.tooltip {
|
||||
max-width: 200px;
|
||||
padding: var(--theme-spacing-sm) var(--theme-spacing-md);
|
||||
font-size: var(--theme-font-size-md);
|
||||
font-weight: 550;
|
||||
color: var(--theme-colors-surface-foreground);
|
||||
background: var(--theme-colors-surface);
|
||||
box-shadow: 4px 4px 10px 0 rgb(0 0 0 / 20%);
|
||||
}
|
||||
31
src/shared/components/tooltip/tooltip.tsx
Normal file
31
src/shared/components/tooltip/tooltip.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { Tooltip as MantineTooltip, TooltipProps as MantineTooltipProps } from '@mantine/core';
|
||||
|
||||
import styles from './tooltip.module.css';
|
||||
|
||||
export interface TooltipProps extends MantineTooltipProps {}
|
||||
|
||||
export const Tooltip = ({
|
||||
children,
|
||||
openDelay = 500,
|
||||
transitionProps = {
|
||||
duration: 250,
|
||||
transition: 'fade',
|
||||
},
|
||||
withinPortal = true,
|
||||
...props
|
||||
}: TooltipProps) => {
|
||||
return (
|
||||
<MantineTooltip
|
||||
classNames={{
|
||||
tooltip: styles.tooltip,
|
||||
}}
|
||||
multiline
|
||||
openDelay={openDelay}
|
||||
transitionProps={transitionProps}
|
||||
withinPortal={withinPortal}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</MantineTooltip>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue