migrate to radix ui, make header draft

This commit is contained in:
Daniil
2026-01-20 00:50:12 +03:00
parent 4688f65c5a
commit 3dfb9453ec
62 changed files with 1757 additions and 165 deletions
+26
View File
@@ -0,0 +1,26 @@
"use client"
import type { JSX, ReactNode } from "react"
import { Theme } from "@radix-ui/themes"
import { Provider as ReduxProvider } from "react-redux"
import { store } from "@shared/store"
import { QueryClientProvider } from "./QueryClientProvider"
export const AppProviders = ({
children,
}: {
children: ReactNode
}): JSX.Element => {
return (
<ReduxProvider store={store}>
<QueryClientProvider>
<Theme accentColor="violet" grayColor="slate" radius="medium">
{children}
</Theme>
</QueryClientProvider>
</ReduxProvider>
)
}
+3 -2
View File
@@ -1,14 +1,15 @@
"use client"
import type { JSX, ReactNode } from "react"
import { QueryClientProvider as QueryClientProviderTanstack } from "@tanstack/react-query"
import { JSX, Suspense } from "react"
import { queryClient } from "@shared/lib/query_client"
export const QueryClientProvider = ({
children,
}: {
children: React.ReactNode
children: ReactNode
}): JSX.Element => {
return (
<QueryClientProviderTanstack client={queryClient}>
+5
View File
@@ -0,0 +1,5 @@
import type { AppDispatch } from "@shared/store"
import { useDispatch } from "react-redux"
export const useAppDispatch = useDispatch.withTypes<AppDispatch>()
+5
View File
@@ -0,0 +1,5 @@
import type { RootState } from "@shared/store"
import { useSelector } from "react-redux"
export const useAppSelector = useSelector.withTypes<RootState>()
+27
View File
@@ -0,0 +1,27 @@
import type { PayloadAction } from "@reduxjs/toolkit"
import { createSlice } from "@reduxjs/toolkit"
export interface AppState {
currentScreenName: string
}
const initialState: AppState = {
currentScreenName: "",
}
const appStateSlice = createSlice({
name: "appState",
initialState,
reducers: {
setCurrentScreenName(state, action: PayloadAction<string>) {
state.currentScreenName = action.payload
},
resetAppState(state) {
state.currentScreenName = initialState.currentScreenName
},
},
})
export const { resetAppState, setCurrentScreenName } = appStateSlice.actions
export const appStateReducer = appStateSlice.reducer
+14
View File
@@ -0,0 +1,14 @@
import { configureStore } from "@reduxjs/toolkit"
import { appStateReducer } from "./appStateStore"
import { userReducer } from "./userStore"
export const store = configureStore({
reducer: {
appState: appStateReducer,
user: userReducer,
},
})
export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch
+38
View File
@@ -0,0 +1,38 @@
import type { PayloadAction } from "@reduxjs/toolkit"
import type { components } from "@shared/api/__generated__/openapi.types"
import { createSlice } from "@reduxjs/toolkit"
export type UserEntity = components["schemas"]["UserRead"]
export interface UserState {
user: UserEntity | null
}
const initialState: UserState = {
user: null,
}
const userSlice = createSlice({
name: "user",
initialState,
reducers: {
setAuthData(
state,
action: PayloadAction<{
user: UserEntity
}>,
) {
state.user = action.payload.user
},
setUser(state, action: PayloadAction<UserEntity | null>) {
state.user = action.payload
},
reset(state) {
state.user = initialState.user
},
},
})
export const { reset: resetUser, setAuthData, setUser } = userSlice.actions
export const userReducer = userSlice.reducer
+29 -23
View File
@@ -1,37 +1,43 @@
@mixin font-header-l {
@mixin font-numeric {
font-variant-numeric: lining-nums proportional-nums;
}
@mixin font-body-16($weight) {
font-weight: $weight;
font-size: 16px;
line-height: 22px;
letter-spacing: 0px;
}
@mixin font-body-14($weight) {
font-weight: $weight;
font-size: 14px;
line-height: 20px;
letter-spacing: 0px;
}
@mixin font-display {
@include font-numeric;
font-weight: 600;
font-size: 32px;
line-height: 42px;
letter-spacing: -0.65px;
}
@mixin font-header-l {
@include font-numeric;
font-weight: 500;
font-size: 20px;
line-height: 26px;
letter-spacing: -0.41px;
}
@mixin font-subheader-l {
font-weight: 500;
font-size: 16px;
line-height: 22px;
letter-spacing: 0px;
}
@mixin font-body-m {
font-weight: 500;
font-size: 16px;
line-height: 22px;
letter-spacing: 0px;
}
@mixin font-body-mm {
font-weight: 500;
font-size: 16px;
line-height: 22px;
letter-spacing: 0px;
@include font-body-16(500);
}
@mixin font-body-mr {
font-weight: 400;
font-size: 16px;
line-height: 22px;
letter-spacing: 0px;
@include font-body-16(400);
}
@mixin font-body-s {
+2
View File
@@ -30,4 +30,6 @@ $color-white: var(--color-white);
$color-black: var(--color-black);
$header-height: var(--header-height);
$text-primary: var(--text-primary);
$text-secondary: var(--text-secondary);
+12 -9
View File
@@ -1,11 +1,12 @@
@import "normalize.css";
@import "@radix-ui/themes/styles.css";
* {
box-sizing: border-box;
margin: 0;
padding: 0;
border: 0;
font-family: var(--font-roboto);
font-family: var(--font-open-sans);
font-variant-numeric: lining-nums proportional-nums;
-moz-osx-font-smoothing: grayscale;
-webkit-font-smoothing: antialiased;
@@ -14,10 +15,10 @@
body {
background-color: #f8f8f8;
@media (prefers-color-scheme: dark) {
background-color: #121212;
}
color: var(--text-primary);
// @media (prefers-color-scheme: dark) {
// background-color: #121212;
// }
}
:root {
@@ -54,10 +55,12 @@ body {
--color-white: #ffffff;
--color-black: #000000;
--text-primary: #111827;
--text-secondary: #6b7280;
--header-height: 56px;
}
.radix-themes {
--default-font-family: var(--font-open-sans);
}
+9 -2
View File
@@ -1,3 +1,10 @@
import type { AlertProps } from "react-bootstrap/Alert"
import type { Callout } from "@radix-ui/themes"
import type { ComponentProps, ReactNode } from "react"
export interface IAlertProps extends AlertProps {}
export type AlertVariant = "info" | "success" | "warning" | "danger"
export interface IAlertProps
extends Omit<ComponentProps<typeof Callout.Root>, "color"> {
variant?: AlertVariant
children?: ReactNode
}
+30
View File
@@ -0,0 +1,30 @@
.alert {
padding: 1rem;
border-radius: 0.5rem;
font-size: 0.875rem;
line-height: 1.5;
}
.info {
background-color: #e0f2fe;
color: #0369a1;
border: 1px solid #7dd3fc;
}
.success {
background-color: #dcfce7;
color: #166534;
border: 1px solid #86efac;
}
.warning {
background-color: #fef9c3;
color: #854d0e;
border: 1px solid #fde047;
}
.danger {
background-color: #fee2e2;
color: #991b1b;
border: 1px solid #fca5a5;
}
+21 -2
View File
@@ -3,11 +3,30 @@
import type { IAlertProps } from "../model/Alert.d"
import type { JSX } from "react"
import { Callout } from "@radix-ui/themes"
import { AlertCircle, CheckCircle, Info, TriangleAlert } from "lucide-react"
import { forwardRef } from "react"
import BootstrapAlert from "react-bootstrap/Alert"
const variantMap = {
info: { color: "blue", Icon: Info },
success: { color: "green", Icon: CheckCircle },
warning: { color: "yellow", Icon: TriangleAlert },
danger: { color: "red", Icon: AlertCircle },
} as const
export const Alert = forwardRef<HTMLDivElement, IAlertProps>(
(props, ref): JSX.Element => <BootstrapAlert ref={ref} {...props} />,
({ variant = "info", children, ...props }, ref): JSX.Element => {
const { color, Icon } = variantMap[variant]
return (
<Callout.Root ref={ref} color={color} role="alert" {...props}>
<Callout.Icon>
<Icon size={16} />
</Callout.Icon>
<Callout.Text>{children}</Callout.Text>
</Callout.Root>
)
},
)
Alert.displayName = "Alert"
+15 -2
View File
@@ -1,3 +1,16 @@
import type { BadgeProps } from "react-bootstrap/Badge"
import type { Badge } from "@radix-ui/themes"
import type { ComponentProps, ReactNode } from "react"
export interface IBadgeProps extends BadgeProps {}
export type BadgeVariant =
| "primary"
| "secondary"
| "success"
| "danger"
| "warning"
| "info"
export interface IBadgeProps
extends Omit<ComponentProps<typeof Badge>, "color"> {
variant?: BadgeVariant
children?: ReactNode
}
+39
View File
@@ -0,0 +1,39 @@
.badge {
display: inline-flex;
align-items: center;
padding: 0.25rem 0.5rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 500;
line-height: 1;
}
.primary {
background-color: var(--purple-400);
color: white;
}
.secondary {
background-color: var(--green-600);
color: white;
}
.success {
background-color: var(--color-success);
color: white;
}
.danger {
background-color: var(--color-danger);
color: white;
}
.warning {
background-color: var(--color-warning);
color: #1f2937;
}
.info {
background-color: #3b82f6;
color: white;
}
+15 -2
View File
@@ -3,11 +3,24 @@
import type { IBadgeProps } from "../model/Badge.d"
import type { JSX } from "react"
import { Badge as RadixBadge } from "@radix-ui/themes"
import { forwardRef } from "react"
import BootstrapBadge from "react-bootstrap/Badge"
const variantMap = {
primary: "violet",
secondary: "gray",
success: "green",
danger: "red",
warning: "yellow",
info: "blue",
} as const
export const Badge = forwardRef<HTMLSpanElement, IBadgeProps>(
(props, ref): JSX.Element => <BootstrapBadge ref={ref} {...props} />,
({ variant = "primary", children, ...props }, ref): JSX.Element => (
<RadixBadge ref={ref} color={variantMap[variant]} {...props}>
{children}
</RadixBadge>
),
)
Badge.displayName = "Badge"
-1
View File
@@ -1 +0,0 @@
export * from "./model/Button.d"
-1
View File
@@ -1 +0,0 @@
export * from "./ui/Button"
+19 -2
View File
@@ -1,3 +1,20 @@
import type { ButtonProps } from "react-bootstrap/Button"
import type { Button as RadixButton } from "@radix-ui/themes"
import type { ComponentProps, ReactNode } from "react"
export interface IButtonProps extends ButtonProps {}
export type ButtonVariant =
| "primary"
| "secondary"
| "outline"
| "ghost"
| "danger"
| "icon"
export type ButtonSize = "sm" | "md" | "lg"
export interface IButtonProps extends Omit<
ComponentProps<typeof RadixButton>,
"size" | "variant"
> {
variant?: ButtonVariant
size?: ButtonSize
children?: ReactNode
}
+53 -2
View File
@@ -4,10 +4,61 @@ import type { IButtonProps } from "../model/Button.d"
import type { JSX } from "react"
import { forwardRef } from "react"
import BootstrapButton from "react-bootstrap/Button"
import {
Button as RadixButton,
IconButton as RadixIconButton,
} from "@radix-ui/themes"
const sizeMap = {
sm: "1",
md: "2",
lg: "3",
} as const
const variantMap = {
primary: { variant: "solid", color: "indigo" },
secondary: { variant: "soft", color: "grass" },
outline: { variant: "outline", color: "indigo" },
ghost: { variant: "ghost", color: "gray" },
danger: { variant: "solid", color: "ruby" },
icon: { variant: "ghost", color: "gray" },
} as const
export const Button = forwardRef<HTMLButtonElement, IButtonProps>(
(props, ref): JSX.Element => <BootstrapButton ref={ref} {...props} />,
(
{ variant = "primary", size = "md", children, ...props },
ref,
): JSX.Element => {
const visual = variantMap[variant]
const radixSize = sizeMap[size]
if (variant === "icon") {
return (
<RadixIconButton
ref={ref}
size={radixSize}
variant={visual.variant}
color={visual.color}
{...props}
>
{children}
</RadixIconButton>
)
}
return (
<RadixButton
ref={ref}
size={radixSize}
variant={visual.variant}
color={visual.color}
{...props}
>
{children}
</RadixButton>
)
},
)
Button.displayName = "Button"
+5 -2
View File
@@ -1,3 +1,6 @@
import type { CardProps } from "react-bootstrap/Card"
import type { Card } from "@radix-ui/themes"
import type { ComponentProps, ReactNode } from "react"
export interface ICardProps extends CardProps {}
export interface ICardProps extends ComponentProps<typeof Card> {
children?: ReactNode
}
+10
View File
@@ -0,0 +1,10 @@
.card {
background-color: white;
border-radius: 0.75rem;
box-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
padding: 1.5rem;
@media (prefers-color-scheme: dark) {
background-color: #1f1f1f;
}
}
+6 -2
View File
@@ -3,11 +3,15 @@
import type { ICardProps } from "../model/Card.d"
import type { JSX } from "react"
import { Card as RadixCard } from "@radix-ui/themes"
import { forwardRef } from "react"
import BootstrapCard from "react-bootstrap/Card"
export const Card = forwardRef<HTMLDivElement, ICardProps>(
(props, ref): JSX.Element => <BootstrapCard ref={ref} {...props} />,
({ children, ...props }, ref): JSX.Element => (
<RadixCard ref={ref} {...props}>
{children}
</RadixCard>
),
)
Card.displayName = "Card"
+5 -2
View File
@@ -1,3 +1,6 @@
import type { FormCheckProps } from "react-bootstrap/FormCheck"
import type { Checkbox } from "@radix-ui/themes"
import type { ComponentProps, ReactNode } from "react"
export interface ICheckboxProps extends Omit<FormCheckProps, "type"> {}
export interface ICheckboxProps extends ComponentProps<typeof Checkbox> {
label?: ReactNode
}
@@ -0,0 +1,50 @@
.wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
}
.checkbox {
width: 1.25rem;
height: 1.25rem;
background-color: white;
border: 2px solid #d1d5db;
border-radius: 0.25rem;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
&:hover {
border-color: var(--purple-400);
}
&:focus-visible {
outline: 2px solid var(--purple-400);
outline-offset: 2px;
}
&[data-state="checked"] {
background-color: var(--purple-400);
border-color: var(--purple-400);
}
&[data-disabled] {
opacity: 0.5;
cursor: not-allowed;
}
}
.indicator {
color: white;
display: flex;
align-items: center;
justify-content: center;
}
.label {
font-size: 0.875rem;
cursor: pointer;
user-select: none;
}
+16 -6
View File
@@ -3,13 +3,23 @@
import type { ICheckboxProps } from "../model/Checkbox.d"
import type { JSX } from "react"
import { forwardRef } from "react"
import BootstrapFormCheck from "react-bootstrap/FormCheck"
import { Checkbox as RadixCheckbox, Flex, Text } from "@radix-ui/themes"
import { forwardRef, useId } from "react"
export const Checkbox = forwardRef<HTMLInputElement, ICheckboxProps>(
(props, ref): JSX.Element => (
<BootstrapFormCheck ref={ref} type="checkbox" {...props} />
),
export const Checkbox = forwardRef<HTMLButtonElement, ICheckboxProps>(
({ label, id: propId, ...props }, ref): JSX.Element => {
const generatedId = useId()
const id = propId ?? generatedId
return (
<Text as="label" size="2">
<Flex gap="2" align="center">
<RadixCheckbox ref={ref} id={id} {...props} />
{label}
</Flex>
</Text>
)
},
)
Checkbox.displayName = "Checkbox"
+4 -2
View File
@@ -1,3 +1,5 @@
import type { FormProps } from "react-bootstrap/Form"
import type { FormHTMLAttributes, ReactNode } from "react"
export interface IFormProps extends FormProps {}
export interface IFormProps extends FormHTMLAttributes<HTMLFormElement> {
children?: ReactNode
}
+5
View File
@@ -0,0 +1,5 @@
.form {
display: flex;
flex-direction: column;
gap: 1rem;
}
+9 -2
View File
@@ -4,10 +4,17 @@ import type { IFormProps } from "../model/Form.d"
import type { JSX } from "react"
import { forwardRef } from "react"
import BootstrapForm from "react-bootstrap/Form"
import classNames from "classnames"
import styles from "./Form.module.scss"
export const Form = forwardRef<HTMLFormElement, IFormProps>(
(props, ref): JSX.Element => <BootstrapForm ref={ref} {...props} />,
({ className, children, ...props }, ref): JSX.Element => (
<form ref={ref} className={classNames(styles.form, className)} {...props}>
{children}
</form>
),
)
Form.displayName = "Form"
+7 -2
View File
@@ -1,3 +1,8 @@
import type { ModalProps } from "react-bootstrap/Modal"
import type { Dialog } from "@radix-ui/themes"
import type { ComponentProps, ReactNode } from "react"
export interface IModalProps extends ModalProps {}
export interface IModalProps extends ComponentProps<typeof Dialog.Root> {
title?: ReactNode
description?: ReactNode
children?: ReactNode
}
+86
View File
@@ -0,0 +1,86 @@
.overlay {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.5);
z-index: 100;
animation: overlayShow 0.15s ease;
}
.content {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
max-width: 32rem;
width: calc(100% - 2rem);
max-height: 85vh;
padding: 1.5rem;
background-color: white;
border-radius: 0.75rem;
box-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
z-index: 101;
animation: contentShow 0.15s ease;
&:focus {
outline: none;
}
@media (prefers-color-scheme: dark) {
background-color: #1f1f1f;
}
}
.title {
margin-bottom: 0.5rem;
font-size: 1.125rem;
font-weight: 600;
}
.description {
margin-bottom: 1rem;
font-size: 0.875rem;
color: #6b7280;
}
.close {
position: absolute;
top: 1rem;
right: 1rem;
display: flex;
align-items: center;
justify-content: center;
padding: 0.25rem;
background: transparent;
border-radius: 0.25rem;
color: #6b7280;
cursor: pointer;
&:hover {
background-color: #f3f4f6;
}
&:focus-visible {
outline: 2px solid var(--purple-400);
outline-offset: 2px;
}
}
@keyframes overlayShow {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes contentShow {
from {
opacity: 0;
transform: translate(-50%, -48%) scale(0.96);
}
to {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
}
+17 -2
View File
@@ -3,11 +3,26 @@
import type { IModalProps } from "../model/Modal.d"
import type { JSX } from "react"
import { Dialog } from "@radix-ui/themes"
import { forwardRef } from "react"
import BootstrapModal from "react-bootstrap/Modal"
export const Modal = forwardRef<HTMLDivElement, IModalProps>(
(props, ref): JSX.Element => <BootstrapModal ref={ref} {...props} />,
({ title, description, children, ...props }, ref): JSX.Element => (
<Dialog.Root {...props}>
<Dialog.Content ref={ref}>
{title && <Dialog.Title>{title}</Dialog.Title>}
{description && (
<Dialog.Description size="2" mb="4">
{description}
</Dialog.Description>
)}
{children}
</Dialog.Content>
</Dialog.Root>
),
)
Modal.displayName = "Modal"
export const ModalTrigger = Dialog.Trigger
ModalTrigger.displayName = "ModalTrigger"
+8 -2
View File
@@ -1,3 +1,9 @@
import type { PaginationProps } from "react-bootstrap/Pagination"
import type { Flex } from "@radix-ui/themes"
import type { ComponentProps } from "react"
export interface IPaginationProps extends PaginationProps {}
export interface IPaginationProps extends ComponentProps<typeof Flex> {
currentPage: number
totalPages: number
onPageChange: (page: number) => void
showFirstLast?: boolean
}
@@ -0,0 +1,56 @@
.pagination {
display: flex;
align-items: center;
gap: 0.25rem;
}
.button {
display: flex;
align-items: center;
justify-content: center;
min-width: 2rem;
height: 2rem;
padding: 0 0.5rem;
background: transparent;
border-radius: 0.375rem;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.15s ease;
&:hover:not(:disabled) {
background-color: #f3f4f6;
}
&:focus-visible {
outline: 2px solid var(--purple-400);
outline-offset: 2px;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.page {
font-weight: 500;
}
.active {
background-color: var(--purple-400);
color: white;
&:hover:not(:disabled) {
background-color: var(--purple-500);
}
}
.ellipsis {
display: flex;
align-items: center;
justify-content: center;
min-width: 2rem;
height: 2rem;
font-size: 0.875rem;
color: #6b7280;
}
+106 -4
View File
@@ -3,11 +3,113 @@
import type { IPaginationProps } from "../model/Pagination.d"
import type { JSX } from "react"
import { forwardRef } from "react"
import BootstrapPagination from "react-bootstrap/Pagination"
import { Flex, IconButton, Text } from "@radix-ui/themes"
import {
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
} from "lucide-react"
import { forwardRef, useMemo } from "react"
export const Pagination = forwardRef<HTMLUListElement, IPaginationProps>(
(props, ref): JSX.Element => <BootstrapPagination ref={ref} {...props} />,
export const Pagination = forwardRef<HTMLDivElement, IPaginationProps>(
(
{ currentPage, totalPages, onPageChange, showFirstLast = true, ...props },
ref,
): JSX.Element => {
const pages = useMemo(() => {
const items: (number | "ellipsis")[] = []
const maxVisible = 5
if (totalPages <= maxVisible) {
for (let i = 1; i <= totalPages; i++) items.push(i)
} else {
items.push(1)
if (currentPage > 3) items.push("ellipsis")
const start = Math.max(2, currentPage - 1)
const end = Math.min(totalPages - 1, currentPage + 1)
for (let i = start; i <= end; i++) items.push(i)
if (currentPage < totalPages - 2) items.push("ellipsis")
items.push(totalPages)
}
return items
}, [currentPage, totalPages])
return (
<Flex
ref={ref}
role="navigation"
align="center"
gap="1"
aria-label="Pagination"
{...props}
>
{showFirstLast && (
<IconButton
variant="soft"
size="1"
onClick={() => onPageChange(1)}
disabled={currentPage === 1}
aria-label="First page"
>
<ChevronsLeft size={16} />
</IconButton>
)}
<IconButton
variant="soft"
size="1"
onClick={() => onPageChange(currentPage - 1)}
disabled={currentPage === 1}
aria-label="Previous page"
>
<ChevronLeft size={16} />
</IconButton>
{pages.map((page, index) =>
page === "ellipsis" ? (
<Text key={`ellipsis-${index}`} size="2" color="gray" mx="1">
...
</Text>
) : (
<IconButton
key={page}
variant={page === currentPage ? "solid" : "soft"}
size="1"
onClick={() => onPageChange(page)}
aria-current={page === currentPage ? "page" : undefined}
>
{page}
</IconButton>
),
)}
<IconButton
variant="soft"
size="1"
onClick={() => onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
aria-label="Next page"
>
<ChevronRight size={16} />
</IconButton>
{showFirstLast && (
<IconButton
variant="soft"
size="1"
onClick={() => onPageChange(totalPages)}
disabled={currentPage === totalPages}
aria-label="Last page"
>
<ChevronsRight size={16} />
</IconButton>
)}
</Flex>
)
},
)
Pagination.displayName = "Pagination"
+8 -2
View File
@@ -1,3 +1,9 @@
import type { FormCheckProps } from "react-bootstrap/FormCheck"
import type { RadioGroup } from "@radix-ui/themes"
import type { ComponentProps, ReactNode } from "react"
export interface IRadioProps extends Omit<FormCheckProps, "type"> {}
export interface IRadioGroupProps
extends ComponentProps<typeof RadioGroup.Root> {}
export interface IRadioProps extends ComponentProps<typeof RadioGroup.Item> {
label?: ReactNode
}
+55
View File
@@ -0,0 +1,55 @@
.group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
}
.radio {
width: 1.25rem;
height: 1.25rem;
background-color: white;
border: 2px solid #d1d5db;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
&:hover {
border-color: var(--purple-400);
}
&:focus-visible {
outline: 2px solid var(--purple-400);
outline-offset: 2px;
}
&[data-state="checked"] {
border-color: var(--purple-400);
}
&[data-disabled] {
opacity: 0.5;
cursor: not-allowed;
}
}
.indicator {
width: 0.625rem;
height: 0.625rem;
background-color: var(--purple-400);
border-radius: 50%;
}
.label {
font-size: 0.875rem;
cursor: pointer;
user-select: none;
}
+26 -6
View File
@@ -1,15 +1,35 @@
"use client"
import type { IRadioProps } from "../model/Radio.d"
import type { IRadioGroupProps, IRadioProps } from "../model/Radio.d"
import type { JSX } from "react"
import { forwardRef } from "react"
import BootstrapFormCheck from "react-bootstrap/FormCheck"
import { Flex, RadioGroup, Text } from "@radix-ui/themes"
import { forwardRef, useId } from "react"
export const Radio = forwardRef<HTMLInputElement, IRadioProps>(
(props, ref): JSX.Element => (
<BootstrapFormCheck ref={ref} type="radio" {...props} />
export const RadioGroupRoot = forwardRef<HTMLDivElement, IRadioGroupProps>(
({ ...props }, ref): JSX.Element => (
<RadioGroup.Root ref={ref} {...props} />
),
)
RadioGroupRoot.displayName = "RadioGroup"
export const Radio = forwardRef<HTMLButtonElement, IRadioProps>(
({ label, id: propId, ...props }, ref): JSX.Element => {
const generatedId = useId()
const id = propId ?? generatedId
return (
<Text as="label" size="2">
<Flex gap="2" align="center">
<RadioGroup.Item ref={ref} id={id} {...props} />
{label}
</Flex>
</Text>
)
},
)
Radio.displayName = "Radio"
export { RadioGroupRoot as RadioGroup }
+10 -2
View File
@@ -1,3 +1,11 @@
import type { FormSelectProps } from "react-bootstrap/FormSelect"
import type { Select } from "@radix-ui/themes"
import type { ComponentProps, ReactNode } from "react"
export interface ISelectProps extends FormSelectProps {}
export interface ISelectProps extends ComponentProps<typeof Select.Root> {
placeholder?: string
children?: ReactNode
}
export interface ISelectItemProps extends ComponentProps<typeof Select.Item> {
children?: ReactNode
}
@@ -0,0 +1,91 @@
.trigger {
display: inline-flex;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
min-width: 10rem;
padding: 0.5rem 0.75rem;
background-color: white;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 0.875rem;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
border-color: var(--purple-400);
}
&:focus-visible {
outline: 2px solid var(--purple-400);
outline-offset: 2px;
}
&[data-placeholder] {
color: #9ca3af;
}
&[data-disabled] {
opacity: 0.5;
cursor: not-allowed;
}
}
.icon {
display: flex;
color: #6b7280;
}
.content {
overflow: hidden;
background-color: white;
border: 1px solid #e5e7eb;
border-radius: 0.5rem;
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
z-index: 50;
}
.viewport {
padding: 0.25rem;
}
.item {
position: relative;
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
padding-right: 2rem;
border-radius: 0.375rem;
font-size: 0.875rem;
cursor: pointer;
user-select: none;
outline: none;
&:hover,
&[data-highlighted] {
background-color: var(--purple-50);
color: var(--purple-600);
}
&[data-disabled] {
opacity: 0.5;
pointer-events: none;
}
}
.itemIndicator {
position: absolute;
right: 0.5rem;
display: flex;
align-items: center;
color: var(--purple-400);
}
.scrollButton {
display: flex;
align-items: center;
justify-content: center;
padding: 0.25rem;
color: #6b7280;
cursor: default;
}
+25 -4
View File
@@ -1,13 +1,34 @@
"use client"
import type { ISelectProps } from "../model/Select.d"
import type { ISelectItemProps, ISelectProps } from "../model/Select.d"
import type { JSX } from "react"
import { Select as RadixSelect } from "@radix-ui/themes"
import { forwardRef } from "react"
import BootstrapFormSelect from "react-bootstrap/FormSelect"
export const Select = forwardRef<HTMLSelectElement, ISelectProps>(
(props, ref): JSX.Element => <BootstrapFormSelect ref={ref} {...props} />,
export const Select = forwardRef<HTMLButtonElement, ISelectProps>(
({ placeholder, children, ...props }, ref): JSX.Element => (
<RadixSelect.Root {...props}>
<RadixSelect.Trigger ref={ref} placeholder={placeholder} />
<RadixSelect.Content position="popper">
{children}
</RadixSelect.Content>
</RadixSelect.Root>
),
)
Select.displayName = "Select"
export const SelectItem = forwardRef<HTMLDivElement, ISelectItemProps>(
({ children, ...props }, ref): JSX.Element => (
<RadixSelect.Item ref={ref} {...props}>
{children}
</RadixSelect.Item>
),
)
SelectItem.displayName = "SelectItem"
export const SelectGroup = RadixSelect.Group
export const SelectLabel = RadixSelect.Label
export const SelectSeparator = RadixSelect.Separator
+5 -2
View File
@@ -1,3 +1,6 @@
import type { TableProps } from "react-bootstrap/Table"
import type { Table } from "@radix-ui/themes"
import type { ComponentProps, ReactNode } from "react"
export interface ITableProps extends TableProps {}
export interface ITableProps extends ComponentProps<typeof Table.Root> {
children?: ReactNode
}
+41
View File
@@ -0,0 +1,41 @@
.table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
th,
td {
padding: 0.75rem 1rem;
text-align: left;
}
thead th {
font-weight: 600;
border-bottom: 2px solid #e5e7eb;
}
tbody td {
border-bottom: 1px solid #e5e7eb;
}
}
.striped {
tbody tr:nth-child(odd) {
background-color: #f9fafb;
}
}
.bordered {
border: 1px solid #e5e7eb;
th,
td {
border: 1px solid #e5e7eb;
}
}
.hover {
tbody tr:hover {
background-color: #f3f4f6;
}
}
+13 -2
View File
@@ -3,11 +3,22 @@
import type { ITableProps } from "../model/Table.d"
import type { JSX } from "react"
import { Table as RadixTable } from "@radix-ui/themes"
import { forwardRef } from "react"
import BootstrapTable from "react-bootstrap/Table"
export const Table = forwardRef<HTMLTableElement, ITableProps>(
(props, ref): JSX.Element => <BootstrapTable ref={ref} {...props} />,
({ children, ...props }, ref): JSX.Element => (
<RadixTable.Root ref={ref} {...props}>
{children}
</RadixTable.Root>
),
)
Table.displayName = "Table"
export const TableHeader = RadixTable.Header
export const TableBody = RadixTable.Body
export const TableRow = RadixTable.Row
export const TableCell = RadixTable.Cell
export const TableColumnHeaderCell = RadixTable.ColumnHeaderCell
export const TableRowHeaderCell = RadixTable.RowHeaderCell
+19 -2
View File
@@ -1,3 +1,20 @@
import type { TabsProps } from "react-bootstrap/Tabs"
import type { Tabs } from "@radix-ui/themes"
import type { ComponentProps, ReactNode } from "react"
export interface ITabsProps extends TabsProps {}
export interface ITabsProps extends ComponentProps<typeof Tabs.Root> {
children?: ReactNode
}
export interface ITabsListProps extends ComponentProps<typeof Tabs.List> {
children?: ReactNode
}
export interface ITabsTriggerProps
extends ComponentProps<typeof Tabs.Trigger> {
children?: ReactNode
}
export interface ITabsContentProps
extends ComponentProps<typeof Tabs.Content> {
children?: ReactNode
}
+49
View File
@@ -0,0 +1,49 @@
.tabs {
display: flex;
flex-direction: column;
}
.list {
display: flex;
gap: 0.25rem;
border-bottom: 1px solid #e5e7eb;
}
.trigger {
padding: 0.75rem 1rem;
background: transparent;
font-size: 0.875rem;
font-weight: 500;
color: #6b7280;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: all 0.15s ease;
&:hover {
color: var(--purple-400);
}
&:focus-visible {
outline: 2px solid var(--purple-400);
outline-offset: -2px;
}
&[data-state="active"] {
color: var(--purple-400);
border-bottom-color: var(--purple-400);
}
&[data-disabled] {
opacity: 0.5;
cursor: not-allowed;
}
}
.content {
padding: 1rem 0;
&:focus {
outline: none;
}
}
+31 -9
View File
@@ -1,18 +1,40 @@
"use client"
import type { ITabsProps } from "../model/Tabs.d"
import type { ForwardRefExoticComponent, JSX, RefAttributes } from "react"
import type {
ITabsContentProps,
ITabsListProps,
ITabsProps,
ITabsTriggerProps,
} from "../model/Tabs.d"
import type { JSX } from "react"
import { Tabs as RadixTabs } from "@radix-ui/themes"
import { forwardRef } from "react"
import BootstrapTabs from "react-bootstrap/Tabs"
const BootstrapTabsWithRef =
BootstrapTabs as unknown as ForwardRefExoticComponent<
ITabsProps & RefAttributes<HTMLDivElement>
>
export const Tabs = forwardRef<HTMLDivElement, ITabsProps>(
(props, ref): JSX.Element => <BootstrapTabsWithRef ref={ref} {...props} />,
({ ...props }, ref): JSX.Element => <RadixTabs.Root ref={ref} {...props} />,
)
Tabs.displayName = "Tabs"
export const TabsList = forwardRef<HTMLDivElement, ITabsListProps>(
({ ...props }, ref): JSX.Element => <RadixTabs.List ref={ref} {...props} />,
)
TabsList.displayName = "TabsList"
export const TabsTrigger = forwardRef<HTMLButtonElement, ITabsTriggerProps>(
({ ...props }, ref): JSX.Element => (
<RadixTabs.Trigger ref={ref} {...props} />
),
)
TabsTrigger.displayName = "TabsTrigger"
export const TabsContent = forwardRef<HTMLDivElement, ITabsContentProps>(
({ ...props }, ref): JSX.Element => (
<RadixTabs.Content ref={ref} {...props} />
),
)
TabsContent.displayName = "TabsContent"
+7 -4
View File
@@ -1,7 +1,10 @@
import type { FormControlProps } from "react-bootstrap/FormControl"
import type { TextField } from "@radix-ui/themes"
import type { ComponentProps, ReactNode } from "react"
export interface ITextFieldProps extends FormControlProps {
export interface ITextFieldProps
extends Omit<ComponentProps<typeof TextField.Root>, "color"> {
id: string
label?: string
undertitle?: string
label?: ReactNode
undertitle?: ReactNode
error?: boolean
}
@@ -0,0 +1,69 @@
.wrapper {
display: flex;
flex-direction: column;
gap: 0.375rem;
}
.label {
font-size: 0.875rem;
font-weight: 500;
color: #374151;
@media (prefers-color-scheme: dark) {
color: #e5e7eb;
}
}
.input {
width: 100%;
padding: 0.5rem 0.75rem;
background-color: white;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
font-size: 0.875rem;
transition: all 0.15s ease;
&::placeholder {
color: #9ca3af;
}
&:hover:not(:disabled) {
border-color: #9ca3af;
}
&:focus {
outline: none;
border-color: var(--purple-400);
box-shadow: 0 0 0 3px rgba(168, 85, 247, 0.1);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
background-color: #f3f4f6;
}
@media (prefers-color-scheme: dark) {
background-color: #1f1f1f;
border-color: #4b5563;
color: #f3f4f6;
&:hover:not(:disabled) {
border-color: #6b7280;
}
}
}
.error {
border-color: var(--color-danger);
&:focus {
border-color: var(--color-danger);
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.1);
}
}
.undertitle {
font-size: 0.75rem;
color: #6b7280;
}
+17 -10
View File
@@ -3,25 +3,32 @@
import type { ITextFieldProps } from "../model/TextField.d"
import type { JSX } from "react"
import React, { forwardRef } from "react"
import BootstrapForm from "react-bootstrap/Form"
import { Text, TextField as RadixTextField } from "@radix-ui/themes"
import { forwardRef } from "react"
export const TextField = forwardRef<HTMLInputElement, ITextFieldProps>(
({ id, label, undertitle, ...props }, ref): JSX.Element => (
<React.Fragment>
{label && <BootstrapForm.Label htmlFor={id}>{label}</BootstrapForm.Label>}
<BootstrapForm.Control
({ id, label, undertitle, error, size = "2", ...props }, ref): JSX.Element => (
<label htmlFor={id}>
{label && (
<Text as="div" size="2" mb="1" weight="medium">
{label}
</Text>
)}
<RadixTextField.Root
id={id}
ref={ref}
size={size}
color={error ? "red" : undefined}
aria-describedby={undertitle ? `${id}-undertitle` : undefined}
aria-invalid={error}
{...props}
aria-describedby={`${id}-undertitle`}
/>
{undertitle && (
<BootstrapForm.Text id={`${id}-undertitle`} muted>
<Text as="p" size="1" color="gray" mt="1" id={`${id}-undertitle`}>
{undertitle}
</BootstrapForm.Text>
</Text>
)}
</React.Fragment>
</label>
),
)