feat: whyrating - initial project from turbostarter boilerplate
19
packages/ui/mobile/components.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "unused.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true
|
||||
},
|
||||
"aliases": {
|
||||
"utils": "@turbostarter/ui",
|
||||
"components": "src/components",
|
||||
"ui": "src/components",
|
||||
"lib": "src/lib",
|
||||
"hooks": "src/hooks"
|
||||
}
|
||||
}
|
||||
4
packages/ui/mobile/eslint.config.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import baseConfig from "@turbostarter/eslint-config/base";
|
||||
import reactConfig from "@turbostarter/eslint-config/react";
|
||||
|
||||
export default [...baseConfig, ...reactConfig];
|
||||
56
packages/ui/mobile/package.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"name": "@turbostarter/ui-mobile",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./*": "./src/components/*.tsx"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "git clean -xdf .cache .turbo dist node_modules",
|
||||
"format": "prettier --check . --ignore-path ../../../.gitignore",
|
||||
"lint": "eslint",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"ui:add": "pnpm dlx @react-native-reusables/cli@latest add && prettier src --write --list-different"
|
||||
},
|
||||
"prettier": "@turbostarter/prettier-config",
|
||||
"dependencies": {
|
||||
"@gorhom/bottom-sheet": "5.2.6",
|
||||
"@rn-primitives/avatar": "1.2.0",
|
||||
"@rn-primitives/checkbox": "1.2.0",
|
||||
"@rn-primitives/context-menu": "1.2.0",
|
||||
"@rn-primitives/dropdown-menu": "1.2.0",
|
||||
"@rn-primitives/label": "1.2.0",
|
||||
"@rn-primitives/progress": "1.2.0",
|
||||
"@rn-primitives/radio-group": "1.2.0",
|
||||
"@rn-primitives/select": "1.2.0",
|
||||
"@rn-primitives/slot": "1.2.0",
|
||||
"@rn-primitives/switch": "1.2.0",
|
||||
"@rn-primitives/table": "1.2.0",
|
||||
"@rn-primitives/tabs": "1.2.0",
|
||||
"@rn-primitives/tooltip": "1.2.0",
|
||||
"@rn-primitives/types": "1.2.0",
|
||||
"@turbostarter/i18n": "workspace:*",
|
||||
"@turbostarter/shared": "workspace:*",
|
||||
"@turbostarter/ui": "workspace:*",
|
||||
"culori": "4.0.2",
|
||||
"input-otp-native": "0.5.0",
|
||||
"lucide-react-native": "0.552.0",
|
||||
"react-native": "catalog:",
|
||||
"react-native-keyboard-controller": "1.18.5",
|
||||
"react-native-reanimated": "~4.1.5",
|
||||
"uniwind": "1.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@turbostarter/eslint-config": "workspace:*",
|
||||
"@turbostarter/prettier-config": "workspace:*",
|
||||
"@turbostarter/tsconfig": "workspace:*",
|
||||
"@types/culori": "4.0.1",
|
||||
"eslint": "catalog:",
|
||||
"prettier": "catalog:",
|
||||
"react": "catalog:react19",
|
||||
"typescript": "catalog:",
|
||||
"zod": "catalog:"
|
||||
}
|
||||
}
|
||||
128
packages/ui/mobile/src/components/alert.tsx
Normal file
@@ -0,0 +1,128 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import { Text, TextClassContext } from "./text";
|
||||
|
||||
import type { Icon } from "./icons";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import type { ViewProps } from "react-native";
|
||||
|
||||
const alertVariants = cva(
|
||||
"bg-card border-border relative w-full rounded-lg border px-4 pt-3.5 pb-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card border-border",
|
||||
destructive: "border-destructive/20 bg-destructive/5",
|
||||
primary: "border-primary/20 bg-primary/5",
|
||||
success: "border-success/20 bg-success/5",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const alertTextVariants = cva("text-sm", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "text-foreground",
|
||||
destructive: "text-destructive",
|
||||
primary: "text-primary",
|
||||
success: "text-success",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
const alertDescriptionVariants = cva("ml-0.5 pb-1.5 pl-6 text-sm", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "text-muted-foreground",
|
||||
destructive: "text-destructive/90",
|
||||
primary: "text-primary/90",
|
||||
success: "text-success/90",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
const AlertContext =
|
||||
React.createContext<VariantProps<typeof alertVariants>["variant"]>(null);
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
children,
|
||||
icon: Icon,
|
||||
iconClassName,
|
||||
...props
|
||||
}: ViewProps &
|
||||
React.RefAttributes<View> &
|
||||
VariantProps<typeof alertVariants> & {
|
||||
icon?: Icon;
|
||||
iconClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<AlertContext.Provider value={variant}>
|
||||
<TextClassContext.Provider
|
||||
value={cn(alertTextVariants({ variant }), className)}
|
||||
>
|
||||
<View
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
>
|
||||
{Icon && (
|
||||
<View className="absolute top-3 left-3.5">
|
||||
<Icon
|
||||
className={cn(alertTextVariants({ variant }), iconClassName)}
|
||||
size={16}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
{children}
|
||||
</View>
|
||||
</TextClassContext.Provider>
|
||||
</AlertContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Text> & React.RefAttributes<Text>) {
|
||||
return (
|
||||
<Text
|
||||
className={cn(
|
||||
"font-sans-medium ml-0.5 min-h-4 pl-6 leading-none tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Text> & React.RefAttributes<Text>) {
|
||||
const variant = React.useContext(AlertContext);
|
||||
|
||||
return (
|
||||
<Text
|
||||
className={cn(alertDescriptionVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Alert, AlertDescription, AlertTitle };
|
||||
47
packages/ui/mobile/src/components/avatar.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import * as AvatarPrimitive from "@rn-primitives/avatar";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
...props
|
||||
}: AvatarPrimitive.RootProps & React.RefAttributes<AvatarPrimitive.RootRef>) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
className={cn(
|
||||
"relative flex size-9 shrink-0 overflow-hidden rounded-full",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: AvatarPrimitive.ImageProps & React.RefAttributes<AvatarPrimitive.ImageRef>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
className={cn("aspect-square size-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: AvatarPrimitive.FallbackProps &
|
||||
React.RefAttributes<AvatarPrimitive.FallbackRef>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
className={cn(
|
||||
"bg-muted border-border flex size-full flex-row items-center justify-center rounded-full border",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export { Avatar, AvatarFallback, AvatarImage };
|
||||
61
packages/ui/mobile/src/components/badge.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import * as Slot from "@rn-primitives/slot";
|
||||
import { cva } from "class-variance-authority";
|
||||
import { View } from "react-native";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import { TextClassContext } from "./text";
|
||||
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import type { ViewProps } from "react-native";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"border-border group shrink-0 flex-row items-center justify-center gap-1 overflow-hidden rounded-full border px-2 py-0.5",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary border-transparent active:opacity-80",
|
||||
secondary: "bg-secondary border-transparent active:opacity-80",
|
||||
destructive: "bg-destructive border-transparent active:opacity-80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const badgeTextVariants = cva("font-sans-medium text-xs", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "text-primary-foreground",
|
||||
secondary: "text-secondary-foreground",
|
||||
destructive: "text-destructive-foreground",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
});
|
||||
|
||||
type BadgeProps = ViewProps &
|
||||
React.RefAttributes<View> & {
|
||||
asChild?: boolean;
|
||||
} & VariantProps<typeof badgeVariants>;
|
||||
|
||||
function Badge({ className, variant, asChild, ...props }: BadgeProps) {
|
||||
const Component = asChild ? Slot.View : View;
|
||||
return (
|
||||
<TextClassContext.Provider value={badgeTextVariants({ variant })}>
|
||||
<Component
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
</TextClassContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeTextVariants, badgeVariants };
|
||||
export type { BadgeProps };
|
||||
377
packages/ui/mobile/src/components/bottom-sheet.tsx
Normal file
@@ -0,0 +1,377 @@
|
||||
import {
|
||||
BottomSheetBackdrop,
|
||||
BottomSheetModal,
|
||||
BottomSheetFooter as GBottomSheetFooter,
|
||||
BottomSheetView as GBottomSheetView,
|
||||
useBottomSheetModal,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import {
|
||||
SCROLLABLE_TYPE,
|
||||
createBottomSheetScrollableComponent,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import { useTheme } from "@react-navigation/native";
|
||||
import * as Slot from "@rn-primitives/slot";
|
||||
import * as React from "react";
|
||||
import { memo } from "react";
|
||||
import { Keyboard, Platform, Pressable, View } from "react-native";
|
||||
import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
|
||||
import Reanimated from "react-native-reanimated";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { withUniwind } from "uniwind";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import { Text } from "./text";
|
||||
|
||||
import type {
|
||||
BottomSheetScrollViewMethods,
|
||||
BottomSheetScrollView as GBottomSheetScrollView,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import type {
|
||||
BottomSheetBackdropProps,
|
||||
BottomSheetFooterProps as GBottomSheetFooterProps,
|
||||
} from "@gorhom/bottom-sheet";
|
||||
import type { BottomSheetModalMethods } from "@gorhom/bottom-sheet/lib/typescript/types";
|
||||
import type { GestureResponderEvent, ViewStyle } from "react-native";
|
||||
import type { KeyboardAwareScrollViewProps } from "react-native-keyboard-controller";
|
||||
|
||||
interface BottomSheetContext {
|
||||
sheetRef: React.RefObject<BottomSheetModal | null>;
|
||||
}
|
||||
|
||||
const BottomSheetContext = React.createContext<BottomSheetContext | null>(null);
|
||||
|
||||
function BottomSheet({ ...props }: React.ComponentProps<typeof View>) {
|
||||
const sheetRef = React.useRef<BottomSheetModal>(null);
|
||||
|
||||
return (
|
||||
<BottomSheetContext.Provider value={{ sheetRef: sheetRef }}>
|
||||
<View {...props} />
|
||||
</BottomSheetContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function useBottomSheetContext() {
|
||||
const context = React.useContext(BottomSheetContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"BottomSheet compound components cannot be rendered outside the BottomSheet component",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
const CLOSED_INDEX = -1;
|
||||
|
||||
type BottomSheetContentRef = React.ComponentRef<typeof BottomSheetModal>;
|
||||
|
||||
type BottomSheetContentProps = Omit<
|
||||
React.ComponentProps<typeof BottomSheetModal>,
|
||||
"backdropComponent"
|
||||
> & {
|
||||
backdropProps?: Partial<React.ComponentProps<typeof BottomSheetBackdrop>>;
|
||||
};
|
||||
|
||||
const BottomSheetContent = React.forwardRef<
|
||||
BottomSheetContentRef,
|
||||
BottomSheetContentProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
enablePanDownToClose = true,
|
||||
enableDynamicSizing = true,
|
||||
backdropProps,
|
||||
backgroundStyle,
|
||||
android_keyboardInputMode = "adjustResize",
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const { colors } = useTheme();
|
||||
const { sheetRef } = useBottomSheetContext();
|
||||
|
||||
React.useImperativeHandle(ref, () => {
|
||||
if (!sheetRef.current) {
|
||||
return {} as BottomSheetModalMethods;
|
||||
}
|
||||
return sheetRef.current;
|
||||
}, [sheetRef]);
|
||||
|
||||
const renderBackdrop = React.useCallback(
|
||||
(props: BottomSheetBackdropProps) => {
|
||||
const {
|
||||
pressBehavior = "close",
|
||||
disappearsOnIndex = CLOSED_INDEX,
|
||||
style,
|
||||
onPress,
|
||||
...rest
|
||||
} = {
|
||||
...props,
|
||||
...backdropProps,
|
||||
};
|
||||
return (
|
||||
<BottomSheetBackdrop
|
||||
disappearsOnIndex={disappearsOnIndex}
|
||||
pressBehavior={pressBehavior}
|
||||
style={style}
|
||||
onPress={() => {
|
||||
if (Keyboard.isVisible()) {
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
onPress?.();
|
||||
}}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[backdropProps],
|
||||
);
|
||||
|
||||
return (
|
||||
<BottomSheetModal
|
||||
ref={sheetRef}
|
||||
index={0}
|
||||
enablePanDownToClose={enablePanDownToClose}
|
||||
backdropComponent={renderBackdrop}
|
||||
enableDynamicSizing={enableDynamicSizing}
|
||||
backgroundStyle={[{ backgroundColor: colors.card }, backgroundStyle]}
|
||||
handleIndicatorStyle={{
|
||||
backgroundColor: colors.border,
|
||||
}}
|
||||
topInset={insets.top}
|
||||
android_keyboardInputMode={android_keyboardInputMode}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function BottomSheetOpenTrigger({
|
||||
onPress,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Pressable> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const { sheetRef } = useBottomSheetContext();
|
||||
function handleOnPress(ev: GestureResponderEvent) {
|
||||
sheetRef.current?.present();
|
||||
onPress?.(ev);
|
||||
}
|
||||
const Trigger = asChild ? Slot.Pressable : Pressable;
|
||||
return <Trigger onPress={handleOnPress} {...props} />;
|
||||
}
|
||||
|
||||
function BottomSheetCloseTrigger({
|
||||
onPress,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Pressable> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const { dismiss } = useBottomSheetModal();
|
||||
function handleOnPress(ev: GestureResponderEvent) {
|
||||
dismiss();
|
||||
if (Keyboard.isVisible()) {
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
onPress?.(ev);
|
||||
}
|
||||
const Trigger = asChild ? Slot.Pressable : Pressable;
|
||||
return <Trigger onPress={handleOnPress} {...props} />;
|
||||
}
|
||||
|
||||
const BOTTOM_SHEET_HEADER_HEIGHT = 60; // BottomSheetHeader height
|
||||
|
||||
function BottomSheetView({
|
||||
className,
|
||||
children,
|
||||
hadHeader = false,
|
||||
style,
|
||||
...props
|
||||
}: Omit<React.ComponentProps<typeof GBottomSheetView>, "style"> & {
|
||||
hadHeader?: boolean;
|
||||
style?: ViewStyle;
|
||||
}) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const paddingBottom =
|
||||
insets.bottom +
|
||||
(Platform.select({
|
||||
ios: 4,
|
||||
android: 16,
|
||||
}) ?? 0) +
|
||||
(hadHeader ? BOTTOM_SHEET_HEADER_HEIGHT : 0);
|
||||
|
||||
return (
|
||||
<GBottomSheetView
|
||||
style={[
|
||||
{
|
||||
paddingBottom,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
className={cn(`gap-4 px-6 pt-4`, className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</GBottomSheetView>
|
||||
);
|
||||
}
|
||||
|
||||
type BottomSheetScrollViewProps = Omit<
|
||||
React.ComponentPropsWithoutRef<typeof GBottomSheetScrollView>,
|
||||
"style"
|
||||
> & {
|
||||
hadHeader?: boolean;
|
||||
className?: string;
|
||||
contentContainerClassName?: string;
|
||||
style?: ViewStyle;
|
||||
};
|
||||
|
||||
const BottomSheetKeyboardAwareScrollView = memo(
|
||||
createBottomSheetScrollableComponent<
|
||||
BottomSheetScrollViewMethods,
|
||||
BottomSheetScrollViewProps
|
||||
>(
|
||||
SCROLLABLE_TYPE.SCROLLVIEW,
|
||||
Reanimated.createAnimatedComponent<KeyboardAwareScrollViewProps>(
|
||||
KeyboardAwareScrollView,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
const StyledBottomSheetKeyboardAwareScrollView = withUniwind(
|
||||
BottomSheetKeyboardAwareScrollView,
|
||||
);
|
||||
|
||||
function BottomSheetScrollView({
|
||||
children,
|
||||
hadHeader = false,
|
||||
style,
|
||||
className,
|
||||
contentContainerClassName,
|
||||
...props
|
||||
}: BottomSheetScrollViewProps) {
|
||||
const insets = useSafeAreaInsets();
|
||||
const paddingBottom =
|
||||
insets.bottom +
|
||||
(Platform.select({
|
||||
ios: 8,
|
||||
android: 16,
|
||||
}) ?? 0) +
|
||||
(hadHeader ? BOTTOM_SHEET_HEADER_HEIGHT : 0);
|
||||
|
||||
return (
|
||||
<StyledBottomSheetKeyboardAwareScrollView
|
||||
className={cn("h-full px-6 pt-4", className)}
|
||||
contentContainerClassName={cn("gap-4", contentContainerClassName)}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
bounces={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={[
|
||||
{
|
||||
gap: 16,
|
||||
paddingBottom,
|
||||
},
|
||||
style,
|
||||
]}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</StyledBottomSheetKeyboardAwareScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
function BottomSheetHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof View>) {
|
||||
return <View className={cn("items-start gap-0.5", className)} {...props} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* To be used in a useCallback function as a props to BottomSheetContent
|
||||
*/
|
||||
function BottomSheetFooter({
|
||||
bottomSheetFooterProps,
|
||||
children,
|
||||
className,
|
||||
style,
|
||||
...props
|
||||
}: Omit<React.ComponentProps<typeof View>, "style"> & {
|
||||
bottomSheetFooterProps: GBottomSheetFooterProps;
|
||||
children?: React.ReactNode;
|
||||
style?: ViewStyle;
|
||||
}) {
|
||||
const insets = useSafeAreaInsets();
|
||||
return (
|
||||
<GBottomSheetFooter {...bottomSheetFooterProps}>
|
||||
<View
|
||||
style={[{ paddingBottom: insets.bottom + 6 }, style]}
|
||||
className={cn("px-6 pt-1.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
</GBottomSheetFooter>
|
||||
);
|
||||
}
|
||||
|
||||
function BottomSheetTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Text>) {
|
||||
return (
|
||||
<Text
|
||||
role="heading"
|
||||
aria-level={3}
|
||||
className={cn(
|
||||
"font-sans-semibold text-xl leading-tight tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BottomSheetDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Text>) {
|
||||
return (
|
||||
<Text
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function useBottomSheet() {
|
||||
const ref = React.useRef<BottomSheetContentRef>(null);
|
||||
|
||||
const open = React.useCallback(() => {
|
||||
ref.current?.present();
|
||||
}, []);
|
||||
|
||||
const close = React.useCallback(() => {
|
||||
ref.current?.dismiss();
|
||||
}, []);
|
||||
|
||||
return { ref, open, close };
|
||||
}
|
||||
|
||||
export {
|
||||
BottomSheet,
|
||||
BottomSheetCloseTrigger,
|
||||
BottomSheetContent,
|
||||
BottomSheetFooter,
|
||||
BottomSheetScrollView,
|
||||
BottomSheetHeader,
|
||||
BottomSheetOpenTrigger,
|
||||
BottomSheetView,
|
||||
BottomSheetTitle,
|
||||
BottomSheetDescription,
|
||||
type BottomSheetContentRef,
|
||||
useBottomSheet,
|
||||
};
|
||||
64
packages/ui/mobile/src/components/built-with.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import * as React from "react";
|
||||
import { Pressable, View, Linking } from "react-native";
|
||||
import NativeSvg, { Path } from "react-native-svg";
|
||||
import { withUniwind } from "uniwind";
|
||||
|
||||
import { useTranslation } from "@turbostarter/i18n";
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import { buttonVariants } from "./button";
|
||||
import { Text } from "./text";
|
||||
|
||||
const Svg = withUniwind(NativeSvg);
|
||||
|
||||
export const BuiltWith = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Pressable>) => {
|
||||
const { t } = useTranslation("common");
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
onPress={() => Linking.openURL("https://www.turbostarter.dev")}
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: "outline",
|
||||
className: "flex-row items-center justify-center gap-0",
|
||||
}),
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Text className="text-sm leading-tight">{t("builtWith")}</Text>
|
||||
<View className="shrink-0 flex-row items-center gap-1.5">
|
||||
<Svg
|
||||
height={18}
|
||||
width={18}
|
||||
viewBox="0 0 512 517"
|
||||
fill="none"
|
||||
className="text-primary ml-1.5"
|
||||
>
|
||||
<Path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M383.309 10.9714C383.309 4.91208 388.221 0 394.28 0C400.339 0 405.251 4.91208 405.251 10.9714V122.57C405.251 138.493 398.332 153.631 386.292 164.051L367.707 180.134L391.678 204.105L428.239 190.516C441.132 185.724 449.686 173.419 449.686 159.664V58.5143C449.686 52.4549 454.598 47.5429 460.657 47.5429C466.717 47.5429 471.629 52.4549 471.629 58.5143V159.664C471.629 182.589 457.372 203.097 435.883 211.084L405.175 222.498C415.977 243.457 415.977 268.543 405.175 289.502L440.649 302.687C459.273 309.609 471.629 327.383 471.629 347.251V453.486C471.629 459.545 466.717 464.457 460.657 464.457C454.598 464.457 449.686 459.545 449.686 453.486V347.251C449.686 336.553 443.033 326.983 433.005 323.255L391.678 307.895L367.707 331.866L388.819 350.137C399.255 359.167 405.251 372.287 405.251 386.087V501.029C405.251 507.088 400.339 512 394.28 512C388.221 512 383.309 507.088 383.309 501.029V386.087C383.309 378.656 380.08 371.592 374.461 366.729L352.151 347.423L266.345 433.229C260.632 438.941 251.37 438.941 245.657 433.229L160.142 347.713L138.168 366.729C132.549 371.592 129.32 378.656 129.32 386.087V501.029C129.32 507.088 124.408 512 118.349 512C112.289 512 107.377 507.088 107.377 501.029V386.087C107.377 372.287 113.374 359.167 123.809 350.137L144.586 332.157L120.493 308.065L79.624 323.255C69.5957 326.983 62.9429 336.553 62.9429 347.251V453.486C62.9429 459.545 58.0308 464.457 51.9714 464.457C45.9121 464.457 41 459.545 41 453.486V347.251C41 327.383 53.3553 309.609 71.9792 302.687L106.928 289.698C95.991 268.634 95.991 243.366 106.928 222.303L76.7452 211.084C55.2562 203.097 41 182.589 41 159.664V58.5143C41 52.4549 45.9121 47.5429 51.9714 47.5429C58.0308 47.5429 62.9429 52.4549 62.9429 58.5143V159.664C62.9429 173.419 71.4966 185.724 84.39 190.516L120.494 203.935L144.586 179.843L126.337 164.051C114.296 153.631 107.377 138.493 107.377 122.57V10.9714C107.377 4.91208 112.289 0 118.349 0C124.408 0 129.32 4.91208 129.32 10.9714V122.57C129.32 132.124 133.472 141.206 140.696 147.458L160.142 164.287L245.657 78.7717C251.37 73.0588 260.632 73.0589 266.345 78.7717L352.151 164.577L371.933 147.458C379.157 141.206 383.309 132.124 383.309 122.57V10.9714ZM267.067 166.768C272.383 161.416 281.338 166.639 279.298 173.901L268.742 211.469C266.776 218.467 272.035 225.408 279.304 225.408H318.878C335.139 225.408 343.311 245.043 331.851 256.58L244.619 344.4C239.303 349.752 230.348 344.529 232.388 337.267L242.944 299.699C244.91 292.701 239.65 285.76 232.381 285.76H192.808C176.547 285.76 168.375 266.125 179.835 254.588L267.067 166.768Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</Svg>
|
||||
|
||||
<Svg
|
||||
height={12}
|
||||
width={84}
|
||||
viewBox="0 0 667 84"
|
||||
fill="none"
|
||||
className="text-foreground"
|
||||
>
|
||||
<Path
|
||||
d="M38.887 82H26.497V12.2781H0.140091V1.12708H65.2439V12.2781H38.887V82ZM106.939 57.4453V22.3027H118.315V82H107.727V72.5385C104.573 78.7335 97.5899 83.1264 89.4801 83.1264C77.428 83.1264 69.0929 76.0303 69.0929 60.7117V22.3027H80.4692V58.459C80.4692 68.8215 85.6505 73.1017 93.0845 73.1017C100.744 73.1017 106.939 66.7941 106.939 57.4453ZM144.364 51.4755V82H132.988V22.3027H143.575V34.918C147.743 26.6955 156.754 21.5143 166.553 21.5143V33.3411C153.713 32.6653 144.364 38.2971 144.364 51.4755ZM231.424 52.1514C231.424 70.0605 221.061 83.1264 204.954 83.1264C196.507 83.1264 189.298 78.8462 185.018 71.2995V82H174.43V1.12708H185.806V32.5526C189.974 25.2313 196.845 21.1764 204.954 21.1764C220.949 21.1764 231.424 34.0169 231.424 52.1514ZM219.597 52.1514C219.597 38.635 212.501 31.201 202.702 31.201C193.24 31.201 185.806 38.5224 185.806 51.9261C185.806 65.1045 193.015 72.9891 202.702 72.9891C212.501 72.9891 219.597 65.4425 219.597 52.1514ZM266.837 83.1264C249.941 83.1264 237.551 69.8353 237.551 52.1514C237.551 34.4675 249.941 21.1764 266.837 21.1764C283.732 21.1764 296.122 34.4675 296.122 52.1514C296.122 69.8353 283.732 83.1264 266.837 83.1264ZM266.837 73.1017C276.636 73.1017 284.408 65.2172 284.408 52.1514C284.408 39.0855 276.636 31.3136 266.837 31.3136C257.037 31.3136 249.378 39.0855 249.378 52.1514C249.378 65.2172 257.037 73.1017 266.837 73.1017ZM366.628 58.3464C366.628 72.4259 355.364 83.1264 335.766 83.1264C316.28 83.1264 304.453 72.4259 303.439 56.3189H316.054C316.617 66.5688 323.15 73.2144 335.54 73.2144C345.79 73.2144 353.45 68.371 353.45 60.0359C353.45 53.2777 349.057 49.8986 339.708 47.8712L327.994 45.6185C316.617 43.3657 306.255 37.6213 306.255 23.8796C306.255 10.2506 318.194 0.000719194 334.977 0.000719194C351.76 0.000719194 364.488 10.2506 365.502 26.3576H352.886C352.211 16.6709 345.227 9.91272 335.09 9.91272C324.615 9.91272 318.87 16.1077 318.87 23.0912C318.87 30.7505 325.516 33.4537 333.062 35.0306L345.002 37.396C358.856 40.2119 366.628 46.2943 366.628 58.3464ZM412.842 70.9616V80.9863C409.351 82.5632 406.309 83.1264 402.705 83.1264C391.667 83.1264 384.007 77.1566 384.007 63.9782V31.9894H370.829V22.3027H384.007V4.61881H395.384V22.3027H413.406V31.9894H395.384V61.3875C395.384 69.61 399.326 72.5385 405.408 72.5385C408.112 72.5385 410.477 72.088 412.842 70.9616ZM459.293 82V72.7638C455.576 79.4094 448.93 83.1264 440.144 83.1264C427.754 83.1264 419.645 76.0303 419.645 65.1045C419.645 53.3904 428.993 47.308 446.79 47.308C450.282 47.308 453.098 47.4206 457.941 47.9838V43.591C457.941 35.0306 453.323 30.1873 445.438 30.1873C437.103 30.1873 432.035 35.1433 431.697 43.4784H421.334C421.897 30.0746 431.471 21.1764 445.438 21.1764C460.194 21.1764 468.754 29.5115 468.754 43.7036V82H459.293ZM430.458 64.7666C430.458 70.9616 435.076 75.0165 442.397 75.0165C451.971 75.0165 457.941 69.0468 457.941 59.9233V55.0799C453.548 54.5167 450.394 54.4041 447.466 54.4041C436.089 54.4041 430.458 57.7832 430.458 64.7666ZM494.214 51.4755V82H482.838V22.3027H493.426V34.918C497.593 26.6955 506.604 21.5143 516.404 21.5143V33.3411C503.563 32.6653 494.214 38.2971 494.214 51.4755ZM561.588 70.9616V80.9863C558.097 82.5632 555.055 83.1264 551.451 83.1264C540.413 83.1264 532.753 77.1566 532.753 63.9782V31.9894H519.575V22.3027H532.753V4.61881H544.13V22.3027H562.152V31.9894H544.13V61.3875C544.13 69.61 548.072 72.5385 554.154 72.5385C556.858 72.5385 559.223 72.088 561.588 70.9616ZM594.787 83.1264C577.554 83.1264 565.952 70.6237 565.952 51.8135C565.952 34.1295 578.004 21.1764 594.449 21.1764C612.246 21.1764 624.073 35.5938 622.045 54.9673H577.554C578.455 67.132 584.537 74.2281 594.562 74.2281C603.01 74.2281 608.867 69.61 610.781 61.8381H622.045C619.117 75.1292 608.867 83.1264 594.787 83.1264ZM594.224 29.7367C585.1 29.7367 578.905 36.2696 577.666 47.4206H609.993C609.43 36.3823 603.46 29.7367 594.224 29.7367ZM644.39 51.4755V82H633.014V22.3027H643.602V34.918C647.769 26.6955 656.78 21.5143 666.58 21.5143V33.3411C653.739 32.6653 644.39 38.2971 644.39 51.4755Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</Svg>
|
||||
</View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
85
packages/ui/mobile/src/components/button.tsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
import { Pressable } from "react-native";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import { TextClassContext } from "./text";
|
||||
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"group shrink-0 flex-row items-center justify-center gap-2 rounded-md shadow-none",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary active:bg-primary/90 shadow-sm shadow-black/5",
|
||||
destructive:
|
||||
"bg-destructive active:bg-destructive/90 dark:bg-destructive/60 shadow-sm shadow-black/5",
|
||||
outline:
|
||||
"border-border bg-background active:bg-accent dark:bg-input/30 dark:border-input dark:active:bg-input/50 border shadow-sm shadow-black/5",
|
||||
secondary:
|
||||
"bg-secondary active:bg-secondary/80 shadow-sm shadow-black/5",
|
||||
ghost: "active:bg-accent dark:active:bg-accent/50",
|
||||
link: "",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
sm: "h-9 gap-1.5 rounded-md px-3",
|
||||
lg: "h-11 rounded-md px-6",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const buttonTextVariants = cva("text-foreground font-sans-medium text-sm", {
|
||||
variants: {
|
||||
variant: {
|
||||
default: "text-primary-foreground",
|
||||
destructive: "text-destructive-foreground",
|
||||
outline: "group-active:text-accent-foreground",
|
||||
secondary:
|
||||
"text-secondary-foreground group-active:text-secondary-foreground",
|
||||
ghost: "group-active:text-accent-foreground",
|
||||
link: "text-primary group-active:underline",
|
||||
},
|
||||
size: {
|
||||
default: "",
|
||||
sm: "",
|
||||
lg: "",
|
||||
icon: "",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
});
|
||||
|
||||
type ButtonProps = React.ComponentProps<typeof Pressable> &
|
||||
React.RefAttributes<typeof Pressable> &
|
||||
VariantProps<typeof buttonVariants>;
|
||||
|
||||
function Button({ className, variant, size, ...props }: ButtonProps) {
|
||||
return (
|
||||
<TextClassContext.Provider value={buttonTextVariants({ variant, size })}>
|
||||
<Pressable
|
||||
className={cn(
|
||||
props.disabled && "opacity-50",
|
||||
buttonVariants({ variant, size }),
|
||||
className,
|
||||
)}
|
||||
role="button"
|
||||
{...props}
|
||||
/>
|
||||
</TextClassContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonTextVariants, buttonVariants };
|
||||
export type { ButtonProps };
|
||||
86
packages/ui/mobile/src/components/card.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import * as React from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import { TextClassContext, Text } from "./text";
|
||||
|
||||
import type { TextRef, ViewRef } from "@rn-primitives/types";
|
||||
import type { ViewProps } from "react-native";
|
||||
|
||||
function Card({ className, ...props }: ViewProps & React.RefAttributes<View>) {
|
||||
return (
|
||||
<TextClassContext.Provider value="text-card-foreground">
|
||||
<View
|
||||
className={cn("border-border bg-card rounded-lg border", className)}
|
||||
{...props}
|
||||
/>
|
||||
</TextClassContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function CardHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof View> & React.RefAttributes<ViewRef>) {
|
||||
return (
|
||||
<View className={cn("flex flex-col gap-1.5 p-5", className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function CardTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof Text> & React.RefAttributes<TextRef>) {
|
||||
return (
|
||||
<Text
|
||||
role="heading"
|
||||
aria-level={3}
|
||||
className={cn(
|
||||
"font-sans-medium text-card-foreground text-2xl leading-none tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof Text> & React.RefAttributes<TextRef>) {
|
||||
return (
|
||||
<Text
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CardContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof View> & React.RefAttributes<ViewRef>) {
|
||||
return <View className={cn("p-5 pt-0", className)} {...props} />;
|
||||
}
|
||||
|
||||
function CardFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof View> & React.RefAttributes<ViewRef>) {
|
||||
return (
|
||||
<View
|
||||
className={cn("flex flex-row items-center p-5 pt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
};
|
||||
48
packages/ui/mobile/src/components/checkbox.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import * as CheckboxPrimitive from "@rn-primitives/checkbox";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import { Icons } from "./icons";
|
||||
|
||||
const DEFAULT_HIT_SLOP = 24;
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
checkedClassName,
|
||||
indicatorClassName,
|
||||
iconClassName,
|
||||
...props
|
||||
}: CheckboxPrimitive.RootProps &
|
||||
React.RefAttributes<CheckboxPrimitive.RootRef> & {
|
||||
checkedClassName?: string;
|
||||
indicatorClassName?: string;
|
||||
iconClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
className={cn(
|
||||
"border-input dark:bg-input/30 size-4 shrink-0 overflow-hidden rounded-[4px] border shadow-sm shadow-black/5",
|
||||
props.checked && cn("border-primary", checkedClassName),
|
||||
props.disabled && "opacity-50",
|
||||
className,
|
||||
)}
|
||||
hitSlop={DEFAULT_HIT_SLOP}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn(
|
||||
"bg-primary h-full w-full items-center justify-center",
|
||||
indicatorClassName,
|
||||
)}
|
||||
>
|
||||
<Icons.Check
|
||||
size={12}
|
||||
strokeWidth={3.5}
|
||||
className={cn("text-primary-foreground", iconClassName)}
|
||||
/>
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Checkbox };
|
||||
291
packages/ui/mobile/src/components/dropdown-menu.tsx
Normal file
@@ -0,0 +1,291 @@
|
||||
import * as DropdownMenuPrimitive from "@rn-primitives/dropdown-menu";
|
||||
import * as React from "react";
|
||||
import { Platform, StyleSheet, Text, View } from "react-native";
|
||||
import Animated from "react-native-reanimated";
|
||||
import { FadeIn } from "react-native-reanimated";
|
||||
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import { Icons } from "./icons";
|
||||
import { TextClassContext } from "./text";
|
||||
|
||||
import type { StyleProp, TextProps, ViewStyle } from "react-native";
|
||||
|
||||
const DropdownMenu = DropdownMenuPrimitive.Root;
|
||||
|
||||
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
|
||||
|
||||
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
|
||||
|
||||
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
|
||||
|
||||
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
|
||||
|
||||
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
iconClassName,
|
||||
...props
|
||||
}: DropdownMenuPrimitive.SubTriggerProps &
|
||||
React.RefAttributes<DropdownMenuPrimitive.SubTriggerRef> & {
|
||||
children?: React.ReactNode;
|
||||
iconClassName?: string;
|
||||
inset?: boolean;
|
||||
}) {
|
||||
const { open } = DropdownMenuPrimitive.useSubContext();
|
||||
const Icon = open ? Icons.ChevronUp : Icons.ChevronDown;
|
||||
return (
|
||||
<TextClassContext.Provider
|
||||
value={cn(
|
||||
"group-active:text-accent-foreground text-sm select-none",
|
||||
open && "text-accent-foreground",
|
||||
)}
|
||||
>
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
className={cn(
|
||||
"active:bg-accent group flex flex-row items-center rounded-sm px-2 py-2 sm:py-1.5",
|
||||
open && "bg-accent",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<>{children}</>
|
||||
<Icon
|
||||
size={16}
|
||||
className={cn(
|
||||
"text-foreground ml-auto size-4 shrink-0",
|
||||
iconClassName,
|
||||
)}
|
||||
/>
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
</TextClassContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: DropdownMenuPrimitive.SubContentProps &
|
||||
React.RefAttributes<DropdownMenuPrimitive.SubContentRef>) {
|
||||
return (
|
||||
<Animated.View entering={FadeIn}>
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
className={cn(
|
||||
"bg-popover border-border overflow-hidden rounded-md border p-1 shadow-lg shadow-black/5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
const FullWindowOverlay =
|
||||
Platform.OS === "ios" ? RNFullWindowOverlay : React.Fragment;
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
overlayClassName,
|
||||
overlayStyle,
|
||||
portalHost,
|
||||
...props
|
||||
}: DropdownMenuPrimitive.ContentProps &
|
||||
React.RefAttributes<DropdownMenuPrimitive.ContentRef> & {
|
||||
overlayStyle?: StyleProp<ViewStyle>;
|
||||
overlayClassName?: string;
|
||||
portalHost?: string;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal hostName={portalHost}>
|
||||
<FullWindowOverlay>
|
||||
<DropdownMenuPrimitive.Overlay
|
||||
style={
|
||||
overlayStyle
|
||||
? StyleSheet.flatten([
|
||||
StyleSheet.absoluteFill,
|
||||
overlayStyle as typeof StyleSheet.absoluteFill,
|
||||
])
|
||||
: StyleSheet.absoluteFill
|
||||
}
|
||||
className={overlayClassName}
|
||||
>
|
||||
<Animated.View entering={FadeIn}>
|
||||
<TextClassContext.Provider value="text-popover-foreground">
|
||||
<DropdownMenuPrimitive.Content
|
||||
className={cn(
|
||||
"bg-popover border-border min-w-32 overflow-hidden rounded-md border p-1 shadow-lg shadow-black/5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TextClassContext.Provider>
|
||||
</Animated.View>
|
||||
</DropdownMenuPrimitive.Overlay>
|
||||
</FullWindowOverlay>
|
||||
</DropdownMenuPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant,
|
||||
...props
|
||||
}: DropdownMenuPrimitive.ItemProps &
|
||||
React.RefAttributes<DropdownMenuPrimitive.ItemRef> & {
|
||||
className?: string;
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<TextClassContext.Provider
|
||||
value={cn(
|
||||
"text-popover-foreground group-active:text-popover-foreground text-sm select-none",
|
||||
variant === "destructive" &&
|
||||
"text-destructive group-active:text-destructive",
|
||||
)}
|
||||
>
|
||||
<DropdownMenuPrimitive.Item
|
||||
className={cn(
|
||||
"active:bg-accent group relative flex flex-row items-center gap-2 rounded-sm px-2 py-2 sm:py-1.5",
|
||||
variant === "destructive" &&
|
||||
"active:bg-destructive/10 dark:active:bg-destructive/20",
|
||||
props.disabled && "opacity-50",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TextClassContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: DropdownMenuPrimitive.CheckboxItemProps &
|
||||
React.RefAttributes<DropdownMenuPrimitive.CheckboxItemRef> & {
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<TextClassContext.Provider value="text-sm text-popover-foreground select-none group-active:text-accent-foreground">
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
className={cn(
|
||||
"active:bg-accent group relative flex flex-row items-center gap-2 rounded-sm py-2 pr-2 pl-8 sm:py-1.5",
|
||||
props.disabled && "opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<View className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Icons.Check className={cn("text-foreground size-4")} />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</View>
|
||||
<>{children}</>
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
</TextClassContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: DropdownMenuPrimitive.RadioItemProps &
|
||||
React.RefAttributes<DropdownMenuPrimitive.RadioItemRef> & {
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<TextClassContext.Provider value="text-sm text-popover-foreground select-none group-active:text-accent-foreground">
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
className={cn(
|
||||
"active:bg-accent group relative flex flex-row items-center gap-2 rounded-sm py-2 pr-2 pl-8 sm:py-1.5",
|
||||
props.disabled && "opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<View className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<View className="bg-foreground h-2 w-2 rounded-full" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</View>
|
||||
<>{children}</>
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
</TextClassContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: DropdownMenuPrimitive.LabelProps &
|
||||
React.RefAttributes<DropdownMenuPrimitive.LabelRef> & {
|
||||
className?: string;
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
className={cn(
|
||||
"text-foreground font-sans-medium px-2 py-2 text-xs sm:py-1.5",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: DropdownMenuPrimitive.SeparatorProps &
|
||||
React.RefAttributes<DropdownMenuPrimitive.SeparatorRef>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: TextProps & React.RefAttributes<Text>) {
|
||||
return (
|
||||
<Text
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
};
|
||||
509
packages/ui/mobile/src/components/form.tsx
Normal file
@@ -0,0 +1,509 @@
|
||||
import * as React from "react";
|
||||
import { Controller, FormProvider, useFormContext } from "react-hook-form";
|
||||
import { View } from "react-native";
|
||||
|
||||
import { isKey, useTranslation } from "@turbostarter/i18n";
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import { Checkbox } from "./checkbox";
|
||||
import { Input } from "./input";
|
||||
import { Label } from "./label";
|
||||
import { RadioGroup } from "./radio-group";
|
||||
import { Select } from "./select";
|
||||
import { Switch } from "./switch";
|
||||
import { Text } from "./text";
|
||||
import { Textarea } from "./textarea";
|
||||
|
||||
import type { Option } from "./select";
|
||||
import type {
|
||||
ControllerProps,
|
||||
FieldPath,
|
||||
FieldValues,
|
||||
Noop,
|
||||
} from "react-hook-form";
|
||||
|
||||
const Form = FormProvider;
|
||||
|
||||
interface FormFieldContextValue<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
> {
|
||||
name: TName;
|
||||
}
|
||||
|
||||
const FormFieldContext = React.createContext<FormFieldContextValue>(
|
||||
{} as FormFieldContextValue,
|
||||
);
|
||||
|
||||
function FormField<
|
||||
TFieldValues extends FieldValues = FieldValues,
|
||||
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
||||
>(props: ControllerProps<TFieldValues, TName>) {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const useFormField = () => {
|
||||
const fieldContext = React.useContext(FormFieldContext);
|
||||
const itemContext = React.useContext(FormItemContext);
|
||||
const { getFieldState, formState, handleSubmit } = useFormContext();
|
||||
|
||||
const fieldState = getFieldState(fieldContext.name, formState);
|
||||
|
||||
const { nativeID } = itemContext;
|
||||
|
||||
return {
|
||||
nativeID,
|
||||
name: fieldContext.name,
|
||||
formItemNativeID: `${nativeID}-form-item`,
|
||||
formDescriptionNativeID: `${nativeID}-form-item-description`,
|
||||
formMessageNativeID: `${nativeID}-form-item-message`,
|
||||
handleSubmit,
|
||||
...fieldState,
|
||||
};
|
||||
};
|
||||
|
||||
interface FormItemContextValue {
|
||||
nativeID: string;
|
||||
}
|
||||
|
||||
const FormItemContext = React.createContext<FormItemContextValue>(
|
||||
{} as FormItemContextValue,
|
||||
);
|
||||
|
||||
function FormItem({ className, ...props }: React.ComponentProps<typeof View>) {
|
||||
const nativeID = React.useId();
|
||||
|
||||
return (
|
||||
<FormItemContext.Provider value={{ nativeID }}>
|
||||
<View className={cn("gap-2", className)} {...props} />
|
||||
</FormItemContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function FormLabel({
|
||||
className,
|
||||
nativeID: _nativeID,
|
||||
children,
|
||||
ref,
|
||||
...props
|
||||
}: Omit<React.ComponentProps<typeof Label>, "children"> & {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { error, formItemNativeID } = useFormField();
|
||||
|
||||
return (
|
||||
<Label
|
||||
ref={ref}
|
||||
className={cn("px-px", error && "text-destructive", className)}
|
||||
nativeID={formItemNativeID}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
|
||||
function FormDescription({
|
||||
className,
|
||||
ref,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Text>) {
|
||||
const { formDescriptionNativeID } = useFormField();
|
||||
|
||||
return (
|
||||
<Text
|
||||
ref={ref}
|
||||
nativeID={formDescriptionNativeID}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function FormMessage({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Text>) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const { error, formMessageNativeID } = useFormField();
|
||||
const body = error ? String(error.message) : children;
|
||||
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Text
|
||||
nativeID={formMessageNativeID}
|
||||
className={cn("text-destructive text-sm", className)}
|
||||
{...props}
|
||||
>
|
||||
{typeof body === "string" && isKey(body, i18n) ? t(body) : body}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
type Override<T, U> = Omit<T, keyof U> & U;
|
||||
|
||||
interface FormFieldFieldProps<T> {
|
||||
name: string;
|
||||
onBlur: Noop;
|
||||
onChange: (val: T) => void;
|
||||
value: T;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type FormItemProps<T extends React.ElementType<any>, U> = Override<
|
||||
React.ComponentPropsWithoutRef<T>,
|
||||
FormFieldFieldProps<U>
|
||||
> & {
|
||||
label?: React.ReactNode;
|
||||
description?: React.ReactNode;
|
||||
};
|
||||
|
||||
function FormInput({
|
||||
label,
|
||||
description,
|
||||
onChange,
|
||||
ref,
|
||||
...props
|
||||
}: FormItemProps<typeof Input, string> & {
|
||||
ref?: React.Ref<React.ComponentRef<typeof Input>>;
|
||||
}) {
|
||||
const inputRef = React.useRef<React.ComponentRef<typeof Input>>(null);
|
||||
const {
|
||||
error,
|
||||
formItemNativeID,
|
||||
formDescriptionNativeID,
|
||||
formMessageNativeID,
|
||||
} = useFormField();
|
||||
|
||||
React.useImperativeHandle(ref, () => {
|
||||
if (!inputRef.current) {
|
||||
return {} as React.ComponentRef<typeof Input>;
|
||||
}
|
||||
return inputRef.current;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [inputRef.current]);
|
||||
|
||||
function handleOnLabelPress() {
|
||||
if (!inputRef.current) {
|
||||
return;
|
||||
}
|
||||
if (inputRef.current.isFocused()) {
|
||||
inputRef.current.blur();
|
||||
} else {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormItem>
|
||||
{!!label && (
|
||||
<FormLabel nativeID={formItemNativeID} onPress={handleOnLabelPress}>
|
||||
{label}
|
||||
</FormLabel>
|
||||
)}
|
||||
|
||||
<Input
|
||||
ref={inputRef}
|
||||
aria-labelledby={formItemNativeID}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionNativeID}`
|
||||
: `${formDescriptionNativeID} ${formMessageNativeID}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
onChangeText={onChange}
|
||||
{...props}
|
||||
/>
|
||||
{!!description && <FormDescription>{description}</FormDescription>}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
|
||||
function FormTextarea({
|
||||
label,
|
||||
description,
|
||||
onChange,
|
||||
ref,
|
||||
...props
|
||||
}: FormItemProps<typeof Textarea, string> & {
|
||||
ref?: React.Ref<React.ComponentRef<typeof Textarea>>;
|
||||
}) {
|
||||
const textareaRef = React.useRef<React.ComponentRef<typeof Textarea>>(null);
|
||||
const {
|
||||
error,
|
||||
formItemNativeID,
|
||||
formDescriptionNativeID,
|
||||
formMessageNativeID,
|
||||
} = useFormField();
|
||||
|
||||
React.useImperativeHandle(ref, () => {
|
||||
if (!textareaRef.current) {
|
||||
return {} as React.ComponentRef<typeof Textarea>;
|
||||
}
|
||||
return textareaRef.current;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [textareaRef.current]);
|
||||
|
||||
function handleOnLabelPress() {
|
||||
if (!textareaRef.current) {
|
||||
return;
|
||||
}
|
||||
if (textareaRef.current.isFocused()) {
|
||||
textareaRef.current.blur();
|
||||
} else {
|
||||
textareaRef.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FormItem>
|
||||
{!!label && (
|
||||
<FormLabel nativeID={formItemNativeID} onPress={handleOnLabelPress}>
|
||||
{label}
|
||||
</FormLabel>
|
||||
)}
|
||||
|
||||
<Textarea
|
||||
ref={textareaRef}
|
||||
aria-labelledby={formItemNativeID}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionNativeID}`
|
||||
: `${formDescriptionNativeID} ${formMessageNativeID}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
onChangeText={onChange}
|
||||
{...props}
|
||||
/>
|
||||
{!!description && <FormDescription>{description}</FormDescription>}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
|
||||
function FormCheckbox({
|
||||
label,
|
||||
description,
|
||||
value,
|
||||
onChange,
|
||||
ref,
|
||||
...props
|
||||
}: Omit<
|
||||
FormItemProps<typeof Checkbox, boolean>,
|
||||
"checked" | "onCheckedChange"
|
||||
> & { ref?: React.Ref<React.ComponentRef<typeof Checkbox>> }) {
|
||||
const {
|
||||
error,
|
||||
formItemNativeID,
|
||||
formDescriptionNativeID,
|
||||
formMessageNativeID,
|
||||
} = useFormField();
|
||||
|
||||
function handleOnLabelPress() {
|
||||
onChange(!value);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormItem>
|
||||
<View className="flex-row items-center gap-2">
|
||||
<Checkbox
|
||||
ref={ref}
|
||||
aria-labelledby={formItemNativeID}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionNativeID}`
|
||||
: `${formDescriptionNativeID} ${formMessageNativeID}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
onCheckedChange={onChange}
|
||||
checked={value}
|
||||
{...props}
|
||||
/>
|
||||
{!!label && (
|
||||
<FormLabel nativeID={formItemNativeID} onPress={handleOnLabelPress}>
|
||||
{label}
|
||||
</FormLabel>
|
||||
)}
|
||||
</View>
|
||||
{!!description && <FormDescription>{description}</FormDescription>}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
|
||||
function FormRadioGroup({
|
||||
label,
|
||||
description,
|
||||
value,
|
||||
onChange,
|
||||
ref,
|
||||
...props
|
||||
}: Omit<FormItemProps<typeof RadioGroup, string>, "onValueChange"> & {
|
||||
ref?: React.Ref<React.ComponentRef<typeof RadioGroup>>;
|
||||
}) {
|
||||
const {
|
||||
error,
|
||||
formItemNativeID,
|
||||
formDescriptionNativeID,
|
||||
formMessageNativeID,
|
||||
} = useFormField();
|
||||
|
||||
return (
|
||||
<FormItem className="gap-3">
|
||||
<View>
|
||||
{!!label && <FormLabel nativeID={formItemNativeID}>{label}</FormLabel>}
|
||||
{!!description && (
|
||||
<FormDescription className="pt-0">{description}</FormDescription>
|
||||
)}
|
||||
</View>
|
||||
<RadioGroup
|
||||
ref={ref}
|
||||
aria-labelledby={formItemNativeID}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionNativeID}`
|
||||
: `${formDescriptionNativeID} ${formMessageNativeID}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
onValueChange={onChange}
|
||||
value={value}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
|
||||
function FormSelect({
|
||||
label,
|
||||
description,
|
||||
onChange,
|
||||
value,
|
||||
ref,
|
||||
...props
|
||||
}: Omit<
|
||||
FormItemProps<typeof Select, Partial<Option>>,
|
||||
"open" | "onOpenChange" | "onValueChange"
|
||||
> & { ref?: React.Ref<React.ComponentRef<typeof Select>> }) {
|
||||
const {
|
||||
error,
|
||||
formItemNativeID,
|
||||
formDescriptionNativeID,
|
||||
formMessageNativeID,
|
||||
} = useFormField();
|
||||
|
||||
return (
|
||||
<FormItem>
|
||||
{!!label && <FormLabel nativeID={formItemNativeID}>{label}</FormLabel>}
|
||||
<Select
|
||||
ref={ref}
|
||||
aria-labelledby={formItemNativeID}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionNativeID}`
|
||||
: `${formDescriptionNativeID} ${formMessageNativeID}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
value={
|
||||
value
|
||||
? { label: value.label ?? "", value: value.label ?? "" }
|
||||
: undefined
|
||||
}
|
||||
onValueChange={onChange}
|
||||
{...props}
|
||||
/>
|
||||
{!!description && <FormDescription>{description}</FormDescription>}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
|
||||
function FormSwitch({
|
||||
label,
|
||||
description,
|
||||
value,
|
||||
onChange,
|
||||
ref,
|
||||
...props
|
||||
}: Omit<
|
||||
FormItemProps<typeof Switch, boolean>,
|
||||
"checked" | "onCheckedChange"
|
||||
> & { ref?: React.Ref<React.ComponentRef<typeof Switch>> }) {
|
||||
const switchRef = React.useRef<React.ComponentRef<typeof Switch>>(null);
|
||||
const {
|
||||
error,
|
||||
formItemNativeID,
|
||||
formDescriptionNativeID,
|
||||
formMessageNativeID,
|
||||
} = useFormField();
|
||||
|
||||
React.useImperativeHandle(ref, () => {
|
||||
if (!switchRef.current) {
|
||||
return {} as React.ComponentRef<typeof Switch>;
|
||||
}
|
||||
return switchRef.current;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [switchRef.current]);
|
||||
|
||||
function handleOnLabelPress() {
|
||||
onChange(!value);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormItem className="px-1">
|
||||
<View className="flex-row items-center gap-3">
|
||||
<Switch
|
||||
ref={switchRef}
|
||||
aria-labelledby={formItemNativeID}
|
||||
aria-describedby={
|
||||
!error
|
||||
? `${formDescriptionNativeID}`
|
||||
: `${formDescriptionNativeID} ${formMessageNativeID}`
|
||||
}
|
||||
aria-invalid={!!error}
|
||||
onCheckedChange={onChange}
|
||||
checked={value}
|
||||
{...props}
|
||||
/>
|
||||
{!!label && (
|
||||
<FormLabel
|
||||
className="pb-0"
|
||||
nativeID={formItemNativeID}
|
||||
onPress={handleOnLabelPress}
|
||||
>
|
||||
{label}
|
||||
</FormLabel>
|
||||
)}
|
||||
</View>
|
||||
{!!description && <FormDescription>{description}</FormDescription>}
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Form,
|
||||
FormCheckbox,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormInput,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
FormRadioGroup,
|
||||
FormSwitch,
|
||||
FormSelect,
|
||||
FormTextarea,
|
||||
useFormField,
|
||||
};
|
||||
58
packages/ui/mobile/src/components/i18n.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { View } from "react-native";
|
||||
|
||||
import { config, LocaleLabel, useTranslation } from "@turbostarter/i18n";
|
||||
import { Locale } from "@turbostarter/i18n";
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import { Button } from "./button";
|
||||
import { Icons } from "./icons";
|
||||
import { Text } from "./text";
|
||||
|
||||
import type { Icon } from "./icons";
|
||||
|
||||
export const LocaleIcon: Record<Locale, Icon> = {
|
||||
[Locale.EN]: Icons.UnitedKingdom,
|
||||
[Locale.ES]: Icons.Spain,
|
||||
} as const;
|
||||
|
||||
interface LocaleCustomizerProps {
|
||||
readonly onChange?: (lang: Locale) => Promise<void> | void;
|
||||
}
|
||||
|
||||
export const LocaleCustomizer = ({ onChange }: LocaleCustomizerProps) => {
|
||||
const { i18n } = useTranslation("common");
|
||||
const lang = i18n.language as Locale;
|
||||
|
||||
const handleLocaleChange = async (lang: Locale) => {
|
||||
await onChange?.(lang);
|
||||
await i18n.changeLanguage(lang);
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="mt-2 flex flex-1 flex-col items-center gap-4">
|
||||
<View className="flex-row flex-wrap gap-2">
|
||||
{config.locales.map((locale) => {
|
||||
const Icon = LocaleIcon[locale];
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={locale}
|
||||
variant="outline"
|
||||
onPress={() => handleLocaleChange(locale)}
|
||||
className={cn(
|
||||
"grow basis-24 flex-row justify-start gap-3 px-3",
|
||||
locale === lang &&
|
||||
"border-primary dark:border-primary border-2",
|
||||
)}
|
||||
>
|
||||
<Icon className="size-5" />
|
||||
<Text className="text-sm capitalize">{LocaleLabel[locale]}</Text>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
131
packages/ui/mobile/src/components/icons.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import {
|
||||
ArrowRight,
|
||||
ArrowLeft,
|
||||
House,
|
||||
KeyRound,
|
||||
Wallet,
|
||||
Newspaper,
|
||||
UserRound,
|
||||
Loader2,
|
||||
Check,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Settings,
|
||||
Sun,
|
||||
SunMoon,
|
||||
Moon,
|
||||
WandSparkles,
|
||||
CircleX,
|
||||
Undo2,
|
||||
ArrowUp,
|
||||
Globe2,
|
||||
GraduationCap,
|
||||
Atom,
|
||||
Brain,
|
||||
Loader,
|
||||
Plus,
|
||||
ChevronRight,
|
||||
Share2,
|
||||
Download,
|
||||
Bell,
|
||||
ThumbsUp,
|
||||
Lock,
|
||||
Building,
|
||||
ChevronLeft,
|
||||
X,
|
||||
UserRoundPlus,
|
||||
ListFilter,
|
||||
Copy,
|
||||
Languages,
|
||||
IdCard,
|
||||
AtSign,
|
||||
Key,
|
||||
ChevronsUpDown,
|
||||
RefreshCw,
|
||||
Workflow,
|
||||
LogOut,
|
||||
MailPlus,
|
||||
Trash,
|
||||
Trash2,
|
||||
ShieldCheck,
|
||||
Pencil,
|
||||
TrendingUp,
|
||||
UsersRound,
|
||||
MonitorSmartphone,
|
||||
} from "lucide-react-native";
|
||||
import { withUniwind } from "uniwind";
|
||||
|
||||
import { Icons as GlobalIcons } from "@turbostarter/ui/assets";
|
||||
|
||||
import type { LucideIcon } from "lucide-react-native";
|
||||
|
||||
const icons = {
|
||||
...GlobalIcons,
|
||||
ArrowRight,
|
||||
ArrowLeft,
|
||||
House,
|
||||
KeyRound,
|
||||
Wallet,
|
||||
Newspaper,
|
||||
UserRound,
|
||||
Loader2,
|
||||
Check,
|
||||
Key,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Building,
|
||||
Settings,
|
||||
Sun,
|
||||
SunMoon,
|
||||
Moon,
|
||||
CircleX,
|
||||
WandSparkles,
|
||||
ChevronsUpDown,
|
||||
Undo2,
|
||||
ArrowUp,
|
||||
Globe2,
|
||||
GraduationCap,
|
||||
Atom,
|
||||
Brain,
|
||||
Loader,
|
||||
Plus,
|
||||
MailPlus,
|
||||
ChevronRight,
|
||||
Share2,
|
||||
Download,
|
||||
Bell,
|
||||
ThumbsUp,
|
||||
Lock,
|
||||
ChevronLeft,
|
||||
ListFilter,
|
||||
X,
|
||||
Languages,
|
||||
IdCard,
|
||||
AtSign,
|
||||
Workflow,
|
||||
LogOut,
|
||||
Trash,
|
||||
Trash2,
|
||||
Pencil,
|
||||
RefreshCw,
|
||||
ShieldCheck,
|
||||
Copy,
|
||||
TrendingUp,
|
||||
UsersRound,
|
||||
UserRoundPlus,
|
||||
MonitorSmartphone,
|
||||
};
|
||||
|
||||
const Icons = {} as {
|
||||
[K in keyof typeof icons]: (typeof icons)[K];
|
||||
};
|
||||
|
||||
(Object.keys(icons) as (keyof typeof icons)[]).forEach((key) => {
|
||||
Icons[key] = withUniwind(
|
||||
icons[key] as React.FC<React.SVGProps<SVGElement>>,
|
||||
) as LucideIcon & React.FC<React.SVGProps<SVGElement>>;
|
||||
});
|
||||
|
||||
export type Icon = (typeof Icons)[keyof typeof Icons];
|
||||
|
||||
export { Icons };
|
||||
112
packages/ui/mobile/src/components/input-otp.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { OTPInput } from "input-otp-native";
|
||||
import React, { useEffect } from "react";
|
||||
import { View, Text } from "react-native";
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
withRepeat,
|
||||
withTiming,
|
||||
withSequence,
|
||||
useSharedValue,
|
||||
} from "react-native-reanimated";
|
||||
import { useCSSVariable } from "uniwind";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import type { SlotProps } from "input-otp-native";
|
||||
|
||||
function InputOTPGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof View>) {
|
||||
return (
|
||||
<View
|
||||
data-slot="input-otp-group"
|
||||
className={cn("flex-row items-center justify-center", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function InputOTPSlot({
|
||||
char,
|
||||
isActive,
|
||||
hasFakeCaret,
|
||||
className,
|
||||
index,
|
||||
max,
|
||||
...props
|
||||
}: React.ComponentProps<typeof View> &
|
||||
SlotProps & { index?: number; max?: number }) {
|
||||
const isFirst = index === 0;
|
||||
const isLast = index === (max ?? 6) - 1;
|
||||
|
||||
return (
|
||||
<View
|
||||
className={cn(
|
||||
"border-input relative flex size-12 items-center justify-center border transition-all outline-none",
|
||||
"dark:bg-input/30",
|
||||
{
|
||||
"border-ring": isActive,
|
||||
"rounded-l-md": isFirst,
|
||||
"rounded-r-md": isLast,
|
||||
},
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{char !== null && (
|
||||
<Text className="text-foreground font-sans-medium text-xl">{char}</Text>
|
||||
)}
|
||||
{hasFakeCaret && <FakeCaret />}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function FakeCaret() {
|
||||
const opacity = useSharedValue(1);
|
||||
const foregroundColor = useCSSVariable("--foreground");
|
||||
|
||||
useEffect(() => {
|
||||
opacity.value = withRepeat(
|
||||
withSequence(
|
||||
withTiming(0, { duration: 500 }),
|
||||
withTiming(1, { duration: 500 }),
|
||||
),
|
||||
-1,
|
||||
true,
|
||||
);
|
||||
}, [opacity]);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
opacity: opacity.value,
|
||||
}));
|
||||
|
||||
const baseStyle = {
|
||||
width: 2,
|
||||
height: 20,
|
||||
borderRadius: 1,
|
||||
...(foregroundColor && { backgroundColor: foregroundColor.toString() }),
|
||||
};
|
||||
|
||||
return (
|
||||
<View className="absolute h-full w-full items-center justify-center">
|
||||
<Animated.View style={[baseStyle, animatedStyle]} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function InputOTPSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof View>) {
|
||||
return (
|
||||
<View
|
||||
{...props}
|
||||
className={cn("w-1 items-center justify-center", className)}
|
||||
>
|
||||
<View className="bg-muted-foreground h-0.5 w-1 rounded-sm" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export { OTPInput as InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
||||
30
packages/ui/mobile/src/components/input.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { TextInput } from "react-native";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import type { TextInputProps } from "react-native";
|
||||
|
||||
function Input({
|
||||
className,
|
||||
placeholderTextColorClassName,
|
||||
selectionColorClassName,
|
||||
...props
|
||||
}: TextInputProps & React.RefAttributes<TextInput>) {
|
||||
return (
|
||||
<TextInput
|
||||
className={cn(
|
||||
"border-input native:leading-[1.25] text-foreground bg-background dark:bg-input/30 flex h-10 w-full min-w-0 flex-row items-center rounded-md border px-3 py-1 font-sans text-base shadow-sm shadow-black/5",
|
||||
props.editable === false && "opacity-50",
|
||||
className,
|
||||
)}
|
||||
placeholderTextColorClassName={cn(
|
||||
"accent-muted-foreground",
|
||||
placeholderTextColorClassName,
|
||||
)}
|
||||
selectionColorClassName={cn("accent-foreground", selectionColorClassName)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Input };
|
||||
37
packages/ui/mobile/src/components/label.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import * as LabelPrimitive from "@rn-primitives/label";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
function Label({
|
||||
className,
|
||||
onPress,
|
||||
onLongPress,
|
||||
onPressIn,
|
||||
onPressOut,
|
||||
disabled,
|
||||
...props
|
||||
}: LabelPrimitive.TextProps & React.RefAttributes<LabelPrimitive.TextRef>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
className={cn(
|
||||
"flex flex-row items-center gap-2 select-none",
|
||||
disabled && "opacity-50",
|
||||
)}
|
||||
onPress={onPress}
|
||||
onLongPress={onLongPress}
|
||||
onPressIn={onPressIn}
|
||||
onPressOut={onPressOut}
|
||||
disabled={disabled}
|
||||
>
|
||||
<LabelPrimitive.Text
|
||||
className={cn(
|
||||
"text-foreground native:leading-tight font-sans-medium text-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</LabelPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Label };
|
||||
94
packages/ui/mobile/src/components/progress.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
import * as ProgressPrimitive from "@rn-primitives/progress";
|
||||
import { Platform, View } from "react-native";
|
||||
import Animated, {
|
||||
Extrapolation,
|
||||
interpolate,
|
||||
useAnimatedStyle,
|
||||
useDerivedValue,
|
||||
withSpring,
|
||||
} from "react-native-reanimated";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
function Progress({
|
||||
className,
|
||||
value,
|
||||
indicatorClassName,
|
||||
...props
|
||||
}: ProgressPrimitive.RootProps &
|
||||
React.RefAttributes<ProgressPrimitive.RootRef> & {
|
||||
indicatorClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<ProgressPrimitive.Root
|
||||
className={cn(
|
||||
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<Indicator value={value} className={indicatorClassName} />
|
||||
</ProgressPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Progress };
|
||||
|
||||
const Indicator = Platform.select({
|
||||
web: WebIndicator,
|
||||
native: NativeIndicator,
|
||||
default: NullIndicator,
|
||||
});
|
||||
|
||||
interface IndicatorProps {
|
||||
value: number | undefined | null;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function WebIndicator({ value, className }: IndicatorProps) {
|
||||
if (Platform.OS !== "web") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View
|
||||
className={cn(
|
||||
"bg-primary h-full w-full flex-1 transition-all",
|
||||
className,
|
||||
)}
|
||||
style={{ transform: `translateX(-${100 - (value ?? 0)}%)` }}
|
||||
>
|
||||
<ProgressPrimitive.Indicator className={cn("h-full w-full", className)} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function NativeIndicator({ value, className }: IndicatorProps) {
|
||||
const progress = useDerivedValue(() => value ?? 0);
|
||||
|
||||
const indicator = useAnimatedStyle(() => {
|
||||
return {
|
||||
width: withSpring(
|
||||
`${interpolate(progress.value, [0, 100], [1, 100], Extrapolation.CLAMP)}%`,
|
||||
{ overshootClamping: true },
|
||||
),
|
||||
};
|
||||
}, [value]);
|
||||
|
||||
if (Platform.OS === "web") {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ProgressPrimitive.Indicator asChild>
|
||||
<Animated.View
|
||||
style={indicator}
|
||||
className={cn("bg-primary h-full", className)}
|
||||
/>
|
||||
</ProgressPrimitive.Indicator>
|
||||
);
|
||||
}
|
||||
|
||||
function NullIndicator(_props: IndicatorProps) {
|
||||
return null;
|
||||
}
|
||||
34
packages/ui/mobile/src/components/radio-group.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import * as RadioGroupPrimitive from "@rn-primitives/radio-group";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
function RadioGroup({
|
||||
className,
|
||||
...props
|
||||
}: RadioGroupPrimitive.RootProps &
|
||||
React.RefAttributes<RadioGroupPrimitive.RootRef>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Root className={cn("gap-3", className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function RadioGroupItem({
|
||||
className,
|
||||
...props
|
||||
}: RadioGroupPrimitive.ItemProps &
|
||||
React.RefAttributes<RadioGroupPrimitive.ItemRef>) {
|
||||
return (
|
||||
<RadioGroupPrimitive.Item
|
||||
className={cn(
|
||||
"border-input dark:bg-input/30 aspect-square size-4 shrink-0 items-center justify-center rounded-full border shadow-sm shadow-black/5",
|
||||
props.disabled && "opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<RadioGroupPrimitive.Indicator className="bg-primary size-2 rounded-full" />
|
||||
</RadioGroupPrimitive.Item>
|
||||
);
|
||||
}
|
||||
|
||||
export { RadioGroup, RadioGroupItem };
|
||||
185
packages/ui/mobile/src/components/select.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
import * as SelectPrimitive from "@rn-primitives/select";
|
||||
import * as React from "react";
|
||||
import { Platform, StyleSheet, View } from "react-native";
|
||||
import Animated, { FadeIn, FadeOut } from "react-native-reanimated";
|
||||
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import { Icons } from "./icons";
|
||||
import { TextClassContext } from "./text";
|
||||
|
||||
type Option = SelectPrimitive.Option;
|
||||
|
||||
const Select = SelectPrimitive.Root;
|
||||
|
||||
const SelectGroup = SelectPrimitive.Group;
|
||||
|
||||
function SelectValue({
|
||||
ref,
|
||||
className,
|
||||
...props
|
||||
}: SelectPrimitive.ValueProps &
|
||||
React.RefAttributes<SelectPrimitive.ValueRef> & {
|
||||
className?: string;
|
||||
}) {
|
||||
const { value } = SelectPrimitive.useRootContext();
|
||||
return (
|
||||
<SelectPrimitive.Value
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-foreground line-clamp-1 flex flex-row items-center gap-2 font-sans text-sm",
|
||||
!value && "text-muted-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
ref,
|
||||
className,
|
||||
children,
|
||||
size = "default",
|
||||
...props
|
||||
}: SelectPrimitive.TriggerProps &
|
||||
React.RefAttributes<SelectPrimitive.TriggerRef> & {
|
||||
children?: React.ReactNode;
|
||||
size?: "default" | "sm";
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-input dark:bg-input/30 dark:active:bg-input/50 bg-background flex h-10 flex-row items-center justify-between gap-2 rounded-sm border px-3 py-2 shadow-sm shadow-black/5",
|
||||
props.disabled && "opacity-50",
|
||||
size === "sm" && "h-8 py-2 sm:py-1.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<>{children}</>
|
||||
<Icons.ChevronDown
|
||||
aria-hidden={true}
|
||||
className="text-muted-foreground"
|
||||
size={16}
|
||||
/>
|
||||
</SelectPrimitive.Trigger>
|
||||
);
|
||||
}
|
||||
|
||||
const FullWindowOverlay =
|
||||
Platform.OS === "ios" ? RNFullWindowOverlay : React.Fragment;
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "popper",
|
||||
portalHost,
|
||||
...props
|
||||
}: SelectPrimitive.ContentProps &
|
||||
React.RefAttributes<SelectPrimitive.ContentRef> & {
|
||||
className?: string;
|
||||
portalHost?: string;
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Portal hostName={portalHost}>
|
||||
<FullWindowOverlay>
|
||||
<SelectPrimitive.Overlay style={StyleSheet.absoluteFill}>
|
||||
<TextClassContext.Provider value="text-popover-foreground font-sans">
|
||||
<Animated.View className="z-50" entering={FadeIn} exiting={FadeOut}>
|
||||
<SelectPrimitive.Content
|
||||
className={cn(
|
||||
"bg-popover border-border relative z-50 min-w-32 rounded-sm border p-1 shadow-md shadow-black/5",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
{...props}
|
||||
>
|
||||
<SelectPrimitive.Viewport
|
||||
className={cn("p-1", position === "popper" && cn("w-full"))}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
</SelectPrimitive.Content>
|
||||
</Animated.View>
|
||||
</TextClassContext.Provider>
|
||||
</SelectPrimitive.Overlay>
|
||||
</FullWindowOverlay>
|
||||
</SelectPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: SelectPrimitive.LabelProps & React.RefAttributes<SelectPrimitive.LabelRef>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
className={cn(
|
||||
"text-muted-foreground px-2 py-2 text-xs sm:py-1.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SelectPrimitive.ItemProps & React.RefAttributes<SelectPrimitive.ItemRef>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
className={cn(
|
||||
"active:bg-accent group relative flex w-full flex-row items-center gap-2 rounded-sm py-2 pr-8 pl-2 sm:py-1.5",
|
||||
Platform.select({
|
||||
web: "focus:bg-accent focus:text-accent-foreground cursor-default outline-none data-disabled:pointer-events-none [&_svg]:pointer-events-none *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
}),
|
||||
props.disabled && "opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<View className="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<Icons.Check className="text-muted-foreground shrink-0" size={16} />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</View>
|
||||
<TextClassContext.Provider value="text-foreground font-sans group-active:text-accent-foreground text-sm select-none">
|
||||
{children as React.ReactNode}
|
||||
</TextClassContext.Provider>
|
||||
</SelectPrimitive.Item>
|
||||
);
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: SelectPrimitive.SeparatorProps &
|
||||
React.RefAttributes<SelectPrimitive.SeparatorRef>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
className={cn(
|
||||
"bg-border -mx-1 my-1 h-px",
|
||||
Platform.select({ web: "pointer-events-none" }),
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
type Option,
|
||||
};
|
||||
42
packages/ui/mobile/src/components/skeleton.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import * as React from "react";
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withRepeat,
|
||||
withSequence,
|
||||
withTiming,
|
||||
} from "react-native-reanimated";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
const duration = 1000;
|
||||
|
||||
function Skeleton({
|
||||
className,
|
||||
style,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof Animated.View>) {
|
||||
const sv = useSharedValue(1);
|
||||
|
||||
React.useEffect(() => {
|
||||
sv.value = withRepeat(
|
||||
withSequence(withTiming(0.5, { duration }), withTiming(1, { duration })),
|
||||
-1,
|
||||
);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const animatedStyle = useAnimatedStyle(() => ({
|
||||
opacity: sv.value,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[animatedStyle, style]}
|
||||
className={cn("bg-secondary dark:bg-muted rounded-md", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton };
|
||||
139
packages/ui/mobile/src/components/slider.tsx
Normal file
@@ -0,0 +1,139 @@
|
||||
import { createContext, useContext } from "react";
|
||||
import { Dimensions, View } from "react-native";
|
||||
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
||||
import Animated, {
|
||||
useSharedValue,
|
||||
useAnimatedScrollHandler,
|
||||
useAnimatedStyle,
|
||||
interpolate,
|
||||
Extrapolation,
|
||||
} from "react-native-reanimated";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import type { FlatList } from "react-native";
|
||||
import type { ViewProps } from "react-native";
|
||||
import type {
|
||||
SharedValue,
|
||||
ScrollHandlerProcessed,
|
||||
FlatListPropsWithLayout,
|
||||
} from "react-native-reanimated";
|
||||
|
||||
interface SliderContextType {
|
||||
threshold: number;
|
||||
scrollX: SharedValue<number> | null;
|
||||
onScroll: ScrollHandlerProcessed<Record<string, unknown>>;
|
||||
}
|
||||
|
||||
const SliderContext = createContext<SliderContextType>({
|
||||
threshold: Dimensions.get("window").width,
|
||||
scrollX: null,
|
||||
onScroll: () => null,
|
||||
});
|
||||
|
||||
const Slider = ({
|
||||
className,
|
||||
threshold = Dimensions.get("window").width,
|
||||
...props
|
||||
}: ViewProps & { threshold?: number }) => {
|
||||
const scrollX = useSharedValue(0);
|
||||
|
||||
const onScroll = useAnimatedScrollHandler({
|
||||
onScroll: ({ contentOffset: { x } }) => {
|
||||
scrollX.value = x;
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<SliderContext.Provider value={{ scrollX, onScroll, threshold }}>
|
||||
<View {...props} className={cn("flex-1 items-center gap-4", className)} />
|
||||
</SliderContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
const SliderList = <ItemT,>(
|
||||
props: FlatListPropsWithLayout<ItemT> & {
|
||||
ref?: React.ForwardedRef<FlatList>;
|
||||
},
|
||||
) => {
|
||||
const { onScroll, threshold } = useContext(SliderContext);
|
||||
const native = Gesture.Native();
|
||||
|
||||
return (
|
||||
<GestureDetector gesture={native}>
|
||||
<Animated.FlatList
|
||||
onScroll={onScroll}
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
initialNumToRender={1}
|
||||
maxToRenderPerBatch={1}
|
||||
scrollEventThrottle={16}
|
||||
decelerationRate="fast"
|
||||
snapToAlignment="start"
|
||||
pagingEnabled
|
||||
snapToInterval={threshold}
|
||||
{...props}
|
||||
/>
|
||||
</GestureDetector>
|
||||
);
|
||||
};
|
||||
|
||||
const SliderListItem = ({
|
||||
index,
|
||||
style,
|
||||
...props
|
||||
}: ViewProps & { index: number }) => {
|
||||
const { scrollX, threshold } = useContext(SliderContext);
|
||||
const opacity = useAnimatedStyle(() => ({
|
||||
opacity: interpolate(
|
||||
scrollX?.value ?? 0,
|
||||
[(index - 1) * threshold, index * threshold, (index + 1) * threshold],
|
||||
[0, 1, 0],
|
||||
Extrapolation.CLAMP,
|
||||
),
|
||||
}));
|
||||
|
||||
return <Animated.View style={[style, opacity]} {...props} />;
|
||||
};
|
||||
|
||||
const SliderPaginationDots = ({ className, ...props }: ViewProps) => {
|
||||
return <View className={cn("flex-row gap-1.5", className)} {...props} />;
|
||||
};
|
||||
|
||||
const SliderPaginationDot = ({
|
||||
className,
|
||||
index = 0,
|
||||
...props
|
||||
}: ViewProps & { index?: number }) => {
|
||||
const { scrollX, threshold } = useContext(SliderContext);
|
||||
|
||||
const animatedDot = useAnimatedStyle(() => ({
|
||||
opacity: interpolate(
|
||||
scrollX?.value ?? 0,
|
||||
[(index - 1) * threshold, index * threshold, (index + 1) * threshold],
|
||||
[0.7, 1, 0.7],
|
||||
),
|
||||
width: interpolate(
|
||||
scrollX?.value ?? 0,
|
||||
[(index - 1) * threshold, index * threshold, (index + 1) * threshold],
|
||||
[11, 28, 11],
|
||||
Extrapolation.CLAMP,
|
||||
),
|
||||
}));
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
className={cn("bg-primary h-3 rounded-full opacity-50", className)}
|
||||
style={animatedDot}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
Slider,
|
||||
SliderList,
|
||||
SliderListItem,
|
||||
SliderPaginationDots,
|
||||
SliderPaginationDot,
|
||||
};
|
||||
47
packages/ui/mobile/src/components/spin.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import { useEffect } from "react";
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
withTiming,
|
||||
Easing,
|
||||
cancelAnimation,
|
||||
withRepeat,
|
||||
useSharedValue,
|
||||
} from "react-native-reanimated";
|
||||
|
||||
export const Spin = ({ children }: { children: React.ReactNode }) => {
|
||||
const rotation = useSharedValue(0);
|
||||
const animatedStyles = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [
|
||||
{
|
||||
rotateZ: `${rotation.value}deg`,
|
||||
},
|
||||
],
|
||||
};
|
||||
}, [rotation.value]);
|
||||
|
||||
useEffect(() => {
|
||||
rotation.value = withRepeat(
|
||||
withTiming(360, {
|
||||
duration: 1000,
|
||||
easing: Easing.linear,
|
||||
}),
|
||||
-1,
|
||||
false,
|
||||
);
|
||||
return () => cancelAnimation(rotation);
|
||||
}, [rotation]);
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
animatedStyles,
|
||||
{
|
||||
alignSelf: "center",
|
||||
},
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
31
packages/ui/mobile/src/components/switch.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import * as SwitchPrimitives from "@rn-primitives/switch";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
function Switch({
|
||||
className,
|
||||
...props
|
||||
}: SwitchPrimitives.RootProps & React.RefAttributes<SwitchPrimitives.RootRef>) {
|
||||
return (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"flex h-7 w-12 shrink-0 flex-row items-center rounded-full border border-transparent shadow-sm shadow-black/5",
|
||||
props.checked ? "bg-primary" : "bg-input dark:bg-input/80",
|
||||
props.disabled && "opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"bg-background size-6 rounded-full transition-transform",
|
||||
props.checked
|
||||
? "dark:bg-primary-foreground translate-x-5"
|
||||
: "dark:bg-foreground translate-x-0",
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Switch };
|
||||
63
packages/ui/mobile/src/components/tabs.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import * as TabsPrimitive from "@rn-primitives/tabs";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import { TextClassContext } from "./text";
|
||||
|
||||
function Tabs({
|
||||
className,
|
||||
...props
|
||||
}: TabsPrimitive.RootProps & React.RefAttributes<TabsPrimitive.RootRef>) {
|
||||
return (
|
||||
<TabsPrimitive.Root
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TabsList({
|
||||
className,
|
||||
...props
|
||||
}: TabsPrimitive.ListProps & React.RefAttributes<TabsPrimitive.ListRef>) {
|
||||
return (
|
||||
<TabsPrimitive.List
|
||||
className={cn(
|
||||
"bg-muted mr-auto flex h-10 flex-row items-center justify-center rounded-lg p-[3px]",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function TabsTrigger({
|
||||
className,
|
||||
...props
|
||||
}: TabsPrimitive.TriggerProps & React.RefAttributes<TabsPrimitive.TriggerRef>) {
|
||||
const { value } = TabsPrimitive.useRootContext();
|
||||
return (
|
||||
<TextClassContext.Provider
|
||||
value={cn(
|
||||
"text-foreground dark:text-muted-foreground font-sans-medium text-sm",
|
||||
value === props.value && "dark:text-foreground",
|
||||
)}
|
||||
>
|
||||
<TabsPrimitive.Trigger
|
||||
className={cn(
|
||||
"flex flex-row items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1.5 shadow-none shadow-black/5",
|
||||
props.disabled && "opacity-50",
|
||||
props.value === value &&
|
||||
"bg-background dark:border-foreground/10 dark:bg-input/30 shadow-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TextClassContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const TabsContent = TabsPrimitive.Content;
|
||||
|
||||
export { Tabs, TabsContent, TabsList, TabsTrigger };
|
||||
31
packages/ui/mobile/src/components/text.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import * as Slot from "@rn-primitives/slot";
|
||||
import * as React from "react";
|
||||
import { Text as RNText } from "react-native";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
const TextClassContext = React.createContext<string | undefined>(undefined);
|
||||
|
||||
const Text = ({
|
||||
className,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<typeof RNText> &
|
||||
React.RefAttributes<RNText> & {
|
||||
asChild?: boolean;
|
||||
}) => {
|
||||
const textClass = React.useContext(TextClassContext);
|
||||
const Component = asChild ? Slot.Text : RNText;
|
||||
return (
|
||||
<Component
|
||||
className={cn(
|
||||
"text-foreground font-sans text-base",
|
||||
textClass,
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { Text, TextClassContext };
|
||||
35
packages/ui/mobile/src/components/textarea.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { TextInput } from "react-native";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import type { TextInputProps } from "react-native";
|
||||
|
||||
function Textarea({
|
||||
className,
|
||||
multiline = true,
|
||||
numberOfLines = 8,
|
||||
placeholderTextColorClassName,
|
||||
selectionColorClassName,
|
||||
...props
|
||||
}: TextInputProps & React.RefAttributes<TextInput>) {
|
||||
return (
|
||||
<TextInput
|
||||
className={cn(
|
||||
"text-foreground border-input dark:bg-input/30 flex min-h-16 w-full flex-row rounded-md border bg-transparent px-3 py-2 font-sans text-base shadow-sm shadow-black/5",
|
||||
props.editable === false && "opacity-50",
|
||||
className,
|
||||
)}
|
||||
placeholderTextColorClassName={cn(
|
||||
"accent-muted-foreground",
|
||||
placeholderTextColorClassName,
|
||||
)}
|
||||
selectionColorClassName={cn("accent-foreground", selectionColorClassName)}
|
||||
multiline={multiline}
|
||||
numberOfLines={numberOfLines}
|
||||
textAlignVertical="top"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Textarea };
|
||||
122
packages/ui/mobile/src/components/theme.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { oklch, formatHex } from "culori";
|
||||
import { memo } from "react";
|
||||
import { FlatList, View } from "react-native";
|
||||
|
||||
import { useTranslation } from "@turbostarter/i18n";
|
||||
import { cn, ThemeColor, ThemeMode, themes } from "@turbostarter/ui";
|
||||
|
||||
import { Button } from "./button";
|
||||
import { Icons } from "./icons";
|
||||
import { Label } from "./label";
|
||||
import { Text } from "./text";
|
||||
|
||||
import type { ThemeConfig } from "@turbostarter/ui";
|
||||
|
||||
interface ThemeCustomizerProps {
|
||||
readonly config: ThemeConfig;
|
||||
readonly onChange: (config: ThemeConfig) => void;
|
||||
readonly resolvedTheme: Exclude<ThemeMode, "system">;
|
||||
}
|
||||
|
||||
export const MODE_ICONS = {
|
||||
[ThemeMode.LIGHT]: Icons.Sun,
|
||||
[ThemeMode.DARK]: Icons.Moon,
|
||||
[ThemeMode.SYSTEM]: Icons.SunMoon,
|
||||
} as const;
|
||||
|
||||
export const ThemeCustomizer = memo<ThemeCustomizerProps>(
|
||||
({ config, onChange, resolvedTheme }) => {
|
||||
const { t } = useTranslation("common");
|
||||
|
||||
return (
|
||||
<View className="mt-2 flex-1 items-center gap-4">
|
||||
<View className="w-full gap-1.5">
|
||||
<Label nativeID="color" className="text-xs">
|
||||
{t("theme.color.label")}
|
||||
</Label>
|
||||
<FlatList
|
||||
bounces={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
numColumns={3}
|
||||
data={Object.values(ThemeColor)}
|
||||
columnWrapperClassName="gap-2"
|
||||
contentContainerClassName="gap-2"
|
||||
renderItem={({ item }) => {
|
||||
const [l, c, h, alpha] = themes[item][resolvedTheme].primary;
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
key={item}
|
||||
onPress={() => onChange({ ...config, color: item })}
|
||||
hitSlop={2}
|
||||
className={cn(
|
||||
"grow basis-24 flex-row justify-start gap-2.5 px-3",
|
||||
config.color === item &&
|
||||
"border-primary dark:border-primary border-2",
|
||||
)}
|
||||
>
|
||||
<View
|
||||
className="flex size-4.5 shrink-0 items-center justify-center rounded-full"
|
||||
style={{
|
||||
backgroundColor: formatHex(
|
||||
oklch({
|
||||
mode: "oklch",
|
||||
l,
|
||||
c,
|
||||
h,
|
||||
alpha,
|
||||
}),
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<Text className="capitalize">{t(`theme.color.${item}`)}</Text>
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View className="w-full gap-1.5">
|
||||
<Label nativeID="mode" className="text-xs">
|
||||
{t("theme.mode.label")}
|
||||
</Label>
|
||||
<FlatList
|
||||
bounces={false}
|
||||
showsVerticalScrollIndicator={false}
|
||||
numColumns={3}
|
||||
data={Object.values(ThemeMode)}
|
||||
columnWrapperClassName="gap-2"
|
||||
contentContainerClassName="gap-2"
|
||||
renderItem={({ item }) => {
|
||||
const isActive = config.mode === item;
|
||||
const Icon = MODE_ICONS[item];
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
key={item}
|
||||
onPress={() => onChange({ ...config, mode: item })}
|
||||
hitSlop={2}
|
||||
className={cn(
|
||||
"grow basis-24 flex-row justify-start gap-2 px-3 capitalize",
|
||||
isActive && "border-primary dark:border-primary border-2",
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
className="text-foreground shrink-0"
|
||||
width={18}
|
||||
height={18}
|
||||
/>
|
||||
<Text className="text-sm capitalize">
|
||||
{t(`theme.mode.${item}`)}
|
||||
</Text>
|
||||
</Button>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
ThemeCustomizer.displayName = "ThemeCustomizer";
|
||||
1
packages/ui/mobile/src/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {};
|
||||
10
packages/ui/mobile/src/typings/uniwind-types.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
// NOTE: This file is generated by uniwind and it should not be edited manually.
|
||||
/// <reference types="uniwind/types" />
|
||||
|
||||
declare module "uniwind" {
|
||||
export interface UniwindConfig {
|
||||
themes: readonly ["light", "dark"];
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
9
packages/ui/mobile/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@turbostarter/tsconfig/internal.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "ES2022"],
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": ["*.ts", "src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
4
packages/ui/shared/eslint.config.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import baseConfig from "@turbostarter/eslint-config/base";
|
||||
import reactConfig from "@turbostarter/eslint-config/react";
|
||||
|
||||
export default [...baseConfig, ...reactConfig];
|
||||
34
packages/ui/shared/package.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"name": "@turbostarter/ui",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./globals.css": "./src/styles/globals.css",
|
||||
"./assets": "./src/assets/index.ts",
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "git clean -xdf .cache .turbo dist node_modules",
|
||||
"build": "pnpm dlx tsx ./src/styles/themes/build.ts",
|
||||
"format": "prettier --check . --ignore-path ../../../.gitignore",
|
||||
"lint": "eslint",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"prettier": "@turbostarter/prettier-config",
|
||||
"devDependencies": {
|
||||
"@turbostarter/eslint-config": "workspace:*",
|
||||
"@turbostarter/prettier-config": "workspace:*",
|
||||
"@turbostarter/tsconfig": "workspace:*",
|
||||
"eslint": "catalog:",
|
||||
"prettier": "catalog:",
|
||||
"react": "catalog:react19",
|
||||
"tailwind-merge": "3.3.1",
|
||||
"typescript": "catalog:"
|
||||
},
|
||||
"dependencies": {
|
||||
"class-variance-authority": "0.7.1",
|
||||
"postcss": "8.5.6",
|
||||
"tailwindcss": "4.1.16"
|
||||
}
|
||||
}
|
||||
35
packages/ui/shared/src/assets/icons.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import Apple from "./svg/brand/apple.svg";
|
||||
import Discord from "./svg/brand/discord.svg";
|
||||
import Facebook from "./svg/brand/facebook.svg";
|
||||
import Github from "./svg/brand/github.svg";
|
||||
import Google from "./svg/brand/google.svg";
|
||||
import Linkedin from "./svg/brand/linkedin.svg";
|
||||
import AndroidStroke from "./svg/brand/stroke/android.svg";
|
||||
import AppleStroke from "./svg/brand/stroke/apple.svg";
|
||||
import ChromeStroke from "./svg/brand/stroke/chrome.svg";
|
||||
import EdgeStroke from "./svg/brand/stroke/edge.svg";
|
||||
import FirefoxStroke from "./svg/brand/stroke/firefox.svg";
|
||||
import Twitter from "./svg/brand/twitter.svg";
|
||||
import Spain from "./svg/flag/spain.svg";
|
||||
import UnitedKingdom from "./svg/flag/united-kingdom.svg";
|
||||
import LogoText from "./svg/logo-text.svg";
|
||||
import Logo from "./svg/logo.svg";
|
||||
|
||||
export const Icons = {
|
||||
Logo,
|
||||
LogoText,
|
||||
Google,
|
||||
Github,
|
||||
Twitter,
|
||||
Apple,
|
||||
Facebook,
|
||||
AppleStroke,
|
||||
AndroidStroke,
|
||||
ChromeStroke,
|
||||
EdgeStroke,
|
||||
FirefoxStroke,
|
||||
Linkedin,
|
||||
Discord,
|
||||
UnitedKingdom,
|
||||
Spain,
|
||||
} as const;
|
||||
1
packages/ui/shared/src/assets/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./icons";
|
||||
10
packages/ui/shared/src/assets/svg/brand/apple.svg
Normal file
@@ -0,0 +1,10 @@
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M18.7108 19.5C17.8808 20.74 17.0008 21.95 15.6608 21.97C14.3208 22 13.8908 21.18 12.3708 21.18C10.8408 21.18 10.3708 21.95 9.10082 22C7.79082 22.05 6.80082 20.68 5.96082 19.47C4.25082 17 2.94082 12.45 4.70082 9.39C5.57082 7.87 7.13082 6.91 8.82082 6.88C10.1008 6.86 11.3208 7.75 12.1108 7.75C12.8908 7.75 14.3708 6.68 15.9208 6.84C16.5708 6.87 18.3908 7.1 19.5608 8.82C19.4708 8.88 17.3908 10.1 17.4108 12.63C17.4408 15.65 20.0608 16.66 20.0908 16.67C20.0608 16.74 19.6708 18.11 18.7108 19.5ZM13.0008 3.5C13.7308 2.67 14.9408 2.04 15.9408 2C16.0708 3.17 15.6008 4.35 14.9008 5.19C14.2108 6.04 13.0708 6.7 11.9508 6.61C11.8008 5.46 12.3608 4.26 13.0008 3.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 824 B |
3
packages/ui/shared/src/assets/svg/brand/discord.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19.2701 5.33C17.9401 4.71 16.5001 4.26 15.0001 4C14.9737 4.00038 14.9486 4.01116 14.9301 4.03C14.7501 4.36 14.5401 4.79 14.4001 5.12C12.8091 4.88015 11.1911 4.88015 9.60012 5.12C9.46012 4.78 9.25012 4.36 9.06012 4.03C9.05012 4.01 9.02012 4 8.99012 4C7.49012 4.26 6.06012 4.71 4.72012 5.33C4.71012 5.33 4.70012 5.34 4.69012 5.35C1.97012 9.42 1.22012 13.38 1.59012 17.3C1.59012 17.32 1.60012 17.34 1.62012 17.35C3.42012 18.67 5.15012 19.47 6.86012 20C6.89012 20.01 6.92012 20 6.93012 19.98C7.33012 19.43 7.69012 18.85 8.00012 18.24C8.02012 18.2 8.00012 18.16 7.96012 18.15C7.39012 17.93 6.85012 17.67 6.32012 17.37C6.28012 17.35 6.28012 17.29 6.31012 17.26C6.42012 17.18 6.53012 17.09 6.64012 17.01C6.66012 16.99 6.69012 16.99 6.71012 17C10.1501 18.57 13.8601 18.57 17.2601 17C17.2801 16.99 17.3101 16.99 17.3301 17.01C17.4401 17.1 17.5501 17.18 17.6601 17.27C17.7001 17.3 17.7001 17.36 17.6501 17.38C17.1301 17.69 16.5801 17.94 16.0101 18.16C15.9701 18.17 15.9601 18.22 15.9701 18.25C16.2901 18.86 16.6501 19.44 17.0401 19.99C17.0701 20 17.1001 20.01 17.1301 20C18.8501 19.47 20.5801 18.67 22.3801 17.35C22.4001 17.34 22.4101 17.32 22.4101 17.3C22.8501 12.77 21.6801 8.84 19.3101 5.35C19.3001 5.34 19.2901 5.33 19.2701 5.33ZM8.52012 14.91C7.49012 14.91 6.63012 13.96 6.63012 12.79C6.63012 11.62 7.47012 10.67 8.52012 10.67C9.58012 10.67 10.4201 11.63 10.4101 12.79C10.4101 13.96 9.57012 14.91 8.52012 14.91ZM15.4901 14.91C14.4601 14.91 13.6001 13.96 13.6001 12.79C13.6001 11.62 14.4401 10.67 15.4901 10.67C16.5501 10.67 17.3901 11.63 17.3801 12.79C17.3801 13.96 16.5501 14.91 15.4901 14.91Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
3
packages/ui/shared/src/assets/svg/brand/facebook.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M22 12C22 6.48 17.52 2 12 2C6.48 2 2 6.48 2 12C2 16.84 5.44 20.87 10 21.8V15H8V12H10V9.5C10 7.57 11.57 6 13.5 6H16V9H14C13.45 9 13 9.45 13 10V12H16V15H13V21.95C18.05 21.45 22 17.19 22 12Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 300 B |
11
packages/ui/shared/src/assets/svg/brand/github.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
>
|
||||
<path
|
||||
d="M12 2C10.6868 2 9.38642 2.25866 8.17317 2.7612C6.95991 3.26375 5.85752 4.00035 4.92893 4.92893C3.05357 6.8043 2 9.34784 2 12C2 16.42 4.87 20.17 8.84 21.5C9.34 21.58 9.5 21.27 9.5 21V19.31C6.73 19.91 6.14 17.97 6.14 17.97C5.68 16.81 5.03 16.5 5.03 16.5C4.12 15.88 5.1 15.9 5.1 15.9C6.1 15.97 6.63 16.93 6.63 16.93C7.5 18.45 8.97 18 9.54 17.76C9.63 17.11 9.89 16.67 10.17 16.42C7.95 16.17 5.62 15.31 5.62 11.5C5.62 10.39 6 9.5 6.65 8.79C6.55 8.54 6.2 7.5 6.75 6.15C6.75 6.15 7.59 5.88 9.5 7.17C10.29 6.95 11.15 6.84 12 6.84C12.85 6.84 13.71 6.95 14.5 7.17C16.41 5.88 17.25 6.15 17.25 6.15C17.8 7.5 17.45 8.54 17.35 8.79C18 9.5 18.38 10.39 18.38 11.5C18.38 15.32 16.04 16.16 13.81 16.41C14.17 16.72 14.5 17.33 14.5 18.26V21C14.5 21.27 14.66 21.59 15.17 21.5C19.14 20.16 22 16.42 22 12C22 10.6868 21.7413 9.38642 21.2388 8.17317C20.7362 6.95991 19.9997 5.85752 19.0711 4.92893C18.1425 4.00035 17.0401 3.26375 15.8268 2.7612C14.6136 2.25866 13.3132 2 12 2Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
22
packages/ui/shared/src/assets/svg/brand/google.svg
Normal file
@@ -0,0 +1,22 @@
|
||||
<svg
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M43.611 20.083H42V20H24V28H35.303C33.654 32.657 29.223 36 24 36C17.373 36 12 30.627 12 24C12 17.373 17.373 12 24 12C27.059 12 29.842 13.154 31.961 15.039L37.618 9.382C34.046 6.053 29.268 4 24 4C12.955 4 4 12.955 4 24C4 35.045 12.955 44 24 44C35.045 44 44 35.045 44 24C44 22.659 43.862 21.35 43.611 20.083Z"
|
||||
fill="#FFC107"
|
||||
/>
|
||||
<path
|
||||
d="M6.30603 14.691L12.877 19.51C14.655 15.108 18.961 12 24 12C27.059 12 29.842 13.154 31.961 15.039L37.618 9.382C34.046 6.053 29.268 4 24 4C16.318 4 9.65603 8.337 6.30603 14.691Z"
|
||||
fill="#FF3D00"
|
||||
/>
|
||||
<path
|
||||
d="M23.9999 44C29.1659 44 33.8599 42.023 37.4089 38.808L31.2189 33.57C29.1435 35.1483 26.6074 36.002 23.9999 36C18.7979 36 14.3809 32.683 12.7169 28.054L6.19495 33.079C9.50495 39.556 16.2269 44 23.9999 44Z"
|
||||
fill="#4CAF50"
|
||||
/>
|
||||
<path
|
||||
d="M43.611 20.083H42V20H24V28H35.303C34.5142 30.2164 33.0934 32.1532 31.216 33.571L31.219 33.569L37.409 38.807C36.971 39.205 44 34 44 24C44 22.659 43.862 21.35 43.611 20.083Z"
|
||||
fill="#1976D2"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
3
packages/ui/shared/src/assets/svg/brand/linkedin.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20.4705 2.00014H3.53055C3.34013 1.9975 3.15105 2.03239 2.97411 2.10282C2.79718 2.17326 2.63585 2.27786 2.49934 2.41065C2.36284 2.54344 2.25383 2.70182 2.17854 2.87675C2.10325 3.05167 2.06316 3.23972 2.06055 3.43014V20.5701C2.06316 20.7606 2.10325 20.9486 2.17854 21.1235C2.25383 21.2985 2.36284 21.4568 2.49934 21.5896C2.63585 21.7224 2.79718 21.827 2.97411 21.8975C3.15105 21.9679 3.34013 22.0028 3.53055 22.0001H20.4705C20.661 22.0028 20.85 21.9679 21.027 21.8975C21.2039 21.827 21.3652 21.7224 21.5017 21.5896C21.6383 21.4568 21.7473 21.2985 21.8226 21.1235C21.8978 20.9486 21.9379 20.7606 21.9405 20.5701V3.43014C21.9379 3.23972 21.8978 3.05167 21.8226 2.87675C21.7473 2.70182 21.6383 2.54344 21.5017 2.41065C21.3652 2.27786 21.2039 2.17326 21.027 2.10282C20.85 2.03239 20.661 1.9975 20.4705 2.00014ZM8.09055 18.7401H5.09055V9.74014H8.09055V18.7401ZM6.59055 8.48014C6.17681 8.48014 5.78002 8.31578 5.48746 8.02323C5.1949 7.73067 5.03055 7.33388 5.03055 6.92014C5.03055 6.5064 5.1949 6.10961 5.48746 5.81705C5.78002 5.5245 6.17681 5.36014 6.59055 5.36014C6.81024 5.33522 7.03272 5.35699 7.24342 5.42402C7.45412 5.49105 7.64829 5.60183 7.8132 5.7491C7.97812 5.89637 8.11007 6.07682 8.20042 6.27862C8.29076 6.48043 8.33746 6.69904 8.33746 6.92014C8.33746 7.14124 8.29076 7.35985 8.20042 7.56166C8.11007 7.76346 7.97812 7.94391 7.8132 8.09118C7.64829 8.23845 7.45412 8.34923 7.24342 8.41626C7.03272 8.48329 6.81024 8.50505 6.59055 8.48014ZM18.9105 18.7401H15.9105V13.9101C15.9105 12.7001 15.4805 11.9101 14.3905 11.9101C14.0532 11.9126 13.7247 12.0184 13.4494 12.2133C13.174 12.4082 12.965 12.6828 12.8505 13.0001C12.7723 13.2352 12.7384 13.4827 12.7505 13.7301V18.7301H9.75055V9.73014H12.7505V11.0001C13.0231 10.5272 13.4195 10.1377 13.897 9.87334C14.3745 9.60902 14.9151 9.47999 15.4605 9.50014C17.4605 9.50014 18.9105 10.7901 18.9105 13.5601V18.7401Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,6 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.9513 18H2.05078C2.55228 12.9465 6.81578 9 12.0008 9C17.1868 9 21.4498 12.9465 21.9513 18Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
|
||||
<path d="M7 10L5 6.5M16.5 10L18.5 6.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7.5 15.5C8.05228 15.5 8.5 15.0523 8.5 14.5C8.5 13.9477 8.05228 13.5 7.5 13.5C6.94772 13.5 6.5 13.9477 6.5 14.5C6.5 15.0523 6.94772 15.5 7.5 15.5Z" fill="currentColor"/>
|
||||
<path d="M16.5 15.5C17.0523 15.5 17.5 15.0523 17.5 14.5C17.5 13.9477 17.0523 13.5 16.5 13.5C15.9477 13.5 15.5 13.9477 15.5 14.5C15.5 15.0523 15.9477 15.5 16.5 15.5Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 744 B |
3
packages/ui/shared/src/assets/svg/brand/stroke/apple.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15.7774 8.2081C15.3044 8.1711 14.7974 8.2841 14.0194 8.5811C14.0844 8.5561 13.2774 8.8711 13.0504 8.9511C12.5484 9.1261 12.1354 9.2221 11.6724 9.2221C11.2144 9.2221 10.7924 9.1301 10.3074 8.9671C10.1376 8.90922 9.96917 8.84721 9.80238 8.7811L9.35338 8.6041C8.70538 8.3501 8.34138 8.2541 8.03838 8.2621C6.88538 8.2761 5.79538 8.9421 5.16138 10.0441C3.86938 12.2871 4.58538 16.3431 6.47438 19.0751C7.47938 20.5191 8.03038 21.0351 8.25138 21.0281C8.47338 21.0181 8.63738 20.9711 9.03538 20.8031L9.20138 20.7321C10.2074 20.3031 10.9114 20.1141 11.9724 20.1141C12.9934 20.1141 13.6754 20.3001 14.6414 20.7161L14.8094 20.7881C15.2064 20.9581 15.3494 20.9961 15.6014 20.9901C15.9584 20.9851 16.3994 20.5731 17.3784 19.1361C17.6464 18.7451 17.8834 18.3331 18.0884 17.9161C17.9538 17.8053 17.8233 17.6896 17.6974 17.5691C16.4074 16.3411 15.6104 14.6851 15.5884 12.6391C15.5675 11.1252 16.0655 9.64977 16.9994 8.4581C16.6075 8.31303 16.1957 8.22872 15.7784 8.2081M15.9334 6.2141C16.6414 6.2621 18.6694 6.4781 19.9894 8.4101C19.8814 8.4701 17.5654 9.8141 17.5894 12.6221C17.6254 15.9821 20.5294 17.0981 20.5654 17.1101C20.5414 17.1941 20.0974 18.7061 19.0294 20.2661C18.1054 21.6221 17.1454 22.9661 15.6334 22.9901C14.1454 23.0261 13.6654 22.1141 11.9734 22.1141C10.2694 22.1141 9.74138 22.9661 8.33738 23.0261C6.87338 23.0741 5.76938 21.5621 4.83338 20.2181C2.92538 17.4581 1.47338 12.4421 3.42938 9.0461C4.40138 7.3541 6.12938 6.2861 8.01338 6.2621C9.44138 6.2261 10.7974 7.2221 11.6734 7.2221C12.5374 7.2221 14.0854 6.0701 15.9334 6.2141ZM14.7934 4.3901C14.0134 5.3261 12.7414 6.0581 11.5054 5.9621C11.3374 4.6901 11.9614 3.3581 12.6814 2.5301C13.4854 1.5941 14.8294 0.898098 15.9454 0.850098C16.0894 2.1461 15.5734 3.4541 14.7934 4.3901Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.365 19.833L12.295 16.491C12.197 16.497 12.0987 16.5 12 16.5C11.1954 16.5001 10.4055 16.2845 9.7125 15.8755C9.01954 15.4666 8.4489 14.8794 8.06 14.175L4.795 8.52C4.26663 9.61449 3.99449 10.8149 3.9992 12.0303C4.00392 13.2456 4.28537 14.4439 4.82222 15.5343C5.35906 16.6247 6.13719 17.5784 7.09757 18.3233C8.05795 19.0681 9.17535 19.5844 10.365 19.833ZM12.59 19.979C13.8021 19.8898 14.9779 19.5256 16.0282 18.9141C17.0786 18.3025 17.9757 17.4597 18.6516 16.4496C19.3275 15.4395 19.7644 14.2887 19.9291 13.0845C20.0937 11.8803 19.9819 10.6545 19.602 9.5H15.742C16.221 10.215 16.5 11.075 16.5 12C16.5 12.848 16.266 13.64 15.858 14.318L12.59 19.979ZM14.143 13.288L14.165 13.25C14.3834 12.8717 14.4988 12.4428 14.4998 12.006C14.5009 11.5693 14.3875 11.1398 14.1709 10.7605C13.9543 10.3812 13.6421 10.0653 13.2654 9.84416C12.8888 9.62304 12.4607 9.50447 12.024 9.50026C11.5872 9.49605 11.157 9.60634 10.7761 9.82015C10.3952 10.034 10.077 10.3438 9.85314 10.7189C9.62929 11.0939 9.50761 11.5211 9.50021 11.9578C9.49282 12.3945 9.59997 12.8256 9.811 13.208L9.835 13.25C10.0526 13.6268 10.3648 13.9403 10.7408 14.1595C11.1167 14.3786 11.5433 14.4959 11.9784 14.4996C12.4136 14.5034 12.8421 14.3935 13.2218 14.1808C13.6014 13.9681 13.9189 13.66 14.143 13.287M6.035 6.667L7.963 10.007C8.33494 9.25405 8.91016 8.62021 9.62359 8.17716C10.337 7.73412 11.1602 7.49955 12 7.5H18.615C17.8819 6.42091 16.8957 5.53773 15.7426 4.92762C14.5895 4.31752 13.3046 3.99903 12 4C10.8737 3.99873 9.75986 4.2359 8.73177 4.69591C7.70367 5.15592 6.78461 5.82836 6.035 6.669M12 22C6.477 22 2 17.523 2 12C2 6.477 6.477 2 12 2C17.523 2 22 6.477 22 12C22 17.523 17.523 22 12 22Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
3
packages/ui/shared/src/assets/svg/brand/stroke/edge.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C16.76 2 20.742 5.325 21.752 9.779C22.112 11.364 21.639 12.604 20.925 13.467C20.3672 14.1309 19.6472 14.6394 18.835 14.943C17.87 15.284 16.785 15.27 15.923 15.119C15.5069 15.0497 15.1 14.9336 14.71 14.773C14.402 14.641 13.98 14.419 13.72 14.047C13.5857 13.8551 13.5231 13.6221 13.543 13.3887C13.563 13.1554 13.6642 12.9364 13.829 12.77C14.314 12.28 14.538 11.775 14.571 11.323C14.603 10.883 14.464 10.387 14.022 9.873C13.319 9.06 12.135 8.903 11.299 9.499C10.495 10.076 10.111 10.956 10.195 11.953C10.28 12.969 10.865 14.169 12.14 15.232C13.317 16.212 15.353 17 17 17C17.5623 17.0006 18.1227 16.9335 18.669 16.8C18.8736 16.7501 19.0887 16.7661 19.2837 16.8457C19.4787 16.9252 19.6435 17.0643 19.7548 17.2431C19.8661 17.4219 19.9181 17.6312 19.9034 17.8413C19.8887 18.0514 19.8081 18.2514 19.673 18.413C18.7353 19.537 17.5619 20.441 16.2359 21.0609C14.9099 21.6808 13.4638 22.0014 12 22C11.23 22 10.48 21.913 9.758 21.748C5.315 20.73 2 16.753 2 12C2 11.006 2.145 10.044 2.417 9.136C3.648 5.01 7.47 2 12 2ZM8.316 12.862C8.1037 13.5547 7.99716 14.2755 8 15C8 16.827 8.938 18.548 10.435 19.847C12.3371 20.2228 14.311 19.8986 15.993 18.934C14.122 18.696 12.146 17.84 10.86 16.768C9.495 15.63 8.63 14.258 8.316 12.862ZM9.951 7.92C9.56185 7.88966 9.17066 7.89736 8.783 7.943C7.308 8.113 5.776 8.816 4.251 10.003C4.0836 10.6554 3.99926 11.3264 4 12C3.9986 13.1049 4.22666 14.198 4.66974 15.2102C5.11282 16.2224 5.76127 17.1315 6.574 17.88C6.19725 16.9664 6.00228 15.9882 6 15C6 11.702 7.717 9.024 9.553 8.106C9.68433 8.03933 9.817 7.978 9.951 7.92ZM12 4C10.8886 3.99867 9.78913 4.22953 8.7721 4.67777C7.75507 5.12601 6.84289 5.78176 6.094 6.603C6.88013 6.27543 7.70753 6.05739 8.553 5.955C10.62 5.715 12.678 6.258 14.6 7.7C15.96 8.72 16.668 10.071 16.566 11.47C16.5218 12.0465 16.35 12.606 16.063 13.108C16.1283 13.1227 16.1963 13.136 16.267 13.148C16.918 13.262 17.63 13.248 18.166 13.058C18.6372 12.8747 19.0557 12.5775 19.384 12.193C19.748 11.753 20.009 11.136 19.802 10.221C19.3982 8.4538 18.4064 6.87601 16.9891 5.74587C15.5718 4.61574 13.8128 4.0002 12 4Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.33125 4.888C9.33729 4.36925 10.4419 4.06982 11.5722 4.00946C12.7025 3.9491 13.8328 4.12919 14.8883 4.53785C15.9439 4.9465 16.9007 5.57442 17.6957 6.38014C18.4907 7.18586 19.1058 8.15106 19.5002 9.212L19.5162 9.285C19.7142 10.042 19.6812 11.365 19.1702 12.574C18.6752 13.744 17.7802 14.714 16.2972 15.021C13.3962 15.62 11.4892 13.696 10.9652 11.741C10.8192 11.1739 10.7116 10.5976 10.6432 10.016C10.5622 9.326 10.5632 8.8 10.6252 8.555C10.6782 8.34516 10.6617 8.12379 10.5782 7.92414C10.4946 7.72449 10.3485 7.55734 10.1619 7.44778C9.97523 7.33823 9.75807 7.29218 9.54302 7.31657C9.32797 7.34095 9.12664 7.43444 8.96925 7.583C7.97702 8.51809 7.23889 9.69016 6.82425 10.989C6.71247 10.3159 6.63702 9.63723 6.59825 8.956C6.53025 7.762 6.57625 6.553 6.80425 5.666C6.85862 5.45518 6.8427 5.23237 6.75891 5.03142C6.67511 4.83047 6.52803 4.66236 6.34 4.55262C6.15196 4.44287 5.93324 4.39749 5.71707 4.42337C5.50089 4.44925 5.29907 4.54497 5.14225 4.696C2.42225 7.313 1.35125 11.148 2.31825 14.516C3.43425 18.821 7.34525 22 11.9992 22C17.5222 22 21.9992 17.523 21.9992 12C21.9992 6.477 17.5222 2 11.9992 2C10.3482 2 8.78825 2.4 7.41325 3.112C7.29285 3.17005 7.1853 3.25164 7.09697 3.35196C7.00865 3.45229 6.94132 3.5693 6.89899 3.69608C6.85666 3.82287 6.84018 3.95685 6.85051 4.09012C6.86085 4.22338 6.8978 4.35322 6.95917 4.47196C7.02055 4.5907 7.10511 4.69593 7.20785 4.78143C7.31059 4.86693 7.42943 4.93097 7.55735 4.96976C7.68526 5.00854 7.81965 5.02129 7.95258 5.00725C8.0855 4.99321 8.21426 4.95266 8.33125 4.888ZM3.99925 12C3.99925 10.95 4.18725 9.922 4.59525 8.95C4.66225 10.281 4.85525 11.539 5.06025 12.306C5.42725 13.676 5.95725 15.085 7.05025 16.176C7.20811 16.3339 7.41428 16.4344 7.6359 16.4616C7.85752 16.4887 8.08185 16.4409 8.27314 16.3257C8.46444 16.2106 8.6117 16.0347 8.69145 15.8262C8.77121 15.6176 8.77889 15.3884 8.71325 15.175C8.32805 13.9141 8.36669 12.5618 8.82325 11.325C8.89025 11.675 8.96325 11.997 9.03325 12.259C9.75725 14.962 12.5052 17.846 16.7012 16.979C17.4946 16.822 18.2464 16.5008 18.9082 16.036C18.1138 17.3944 16.9365 18.4884 15.5236 19.1813C14.1107 19.8741 12.5249 20.1351 10.9645 19.9315C9.40403 19.7278 7.93827 19.0687 6.75046 18.0365C5.56265 17.0043 4.70552 15.6448 4.28625 14.128C4.13925 13.417 3.99925 12.732 3.99925 12Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
11
packages/ui/shared/src/assets/svg/brand/twitter.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg
|
||||
viewBox="0 0 28 28"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
||||
>
|
||||
<path
|
||||
d="M19.751 5H22.818L16.118 12.625L24 23H17.828L12.995 16.707L7.464 23H4.394L11.561 14.845L4 5H10.328L14.698 10.752L19.751 5ZM18.675 21.172H20.375L9.404 6.732H7.58L18.675 21.172Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 347 B |
32
packages/ui/shared/src/assets/svg/flag/spain.svg
Normal file
@@ -0,0 +1,32 @@
|
||||
<svg viewBox="0 0 40 41" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.5 20.2829C0.5 24.1735 1.62103 27.8004 3.53339 30.8338H36.9666C38.879 27.8004 40 24.1735 40 20.2829C40 16.3922 38.879 12.7653 36.9666 9.73193H3.53339C1.62103 12.7653 0.5 16.3922 0.5 20.2829Z" fill="#FFCE31"/>
|
||||
<path d="M37.0343 9.73205C33.5393 4.19282 27.3407 0.5 20.2847 0.5C13.2288 0.5 7.03015 4.19282 3.53516 9.73205H37.0343ZM3.53516 30.8339C7.03015 36.3731 13.2288 40.0659 20.2847 40.0659C27.3407 40.0659 33.5393 36.3731 37.0343 30.8339H3.53516Z" fill="#ED4C5C"/>
|
||||
<path d="M5.25 18.1064H7.36018V19.2934H5.25V18.1064ZM5.25 26.811H7.42613V27.932H5.25V26.811Z" fill="#C8B100"/>
|
||||
<path d="M5.04847 24.9648C4.85064 25.0967 4.71875 25.2286 4.71875 25.2946C4.71875 25.3605 4.78469 25.4264 4.91658 25.4924C5.04847 25.5583 5.18035 25.6902 5.11441 25.8221C5.2463 25.6902 5.31224 25.5583 5.31224 25.4264C5.31224 25.2286 5.18035 25.0308 5.04847 24.9648Z" fill="#ED4C5C"/>
|
||||
<path d="M5.58008 19.2939H7.09677V26.8115H5.58008V19.2939Z" fill="white"/>
|
||||
<path d="M8.67538 22.0634C8.34567 21.9315 7.75218 21.7996 7.09274 21.7996C6.89491 21.7996 6.63114 21.7996 6.36737 21.8656C5.44416 21.9975 4.71879 22.3931 4.78473 22.6569L4.45501 21.9315C4.38907 21.6018 5.18039 21.2062 6.16954 21.0083C6.49926 20.9424 6.82897 20.9424 7.09274 20.9424C7.75218 20.9424 8.34567 21.0083 8.67538 21.1402V22.0634Z" fill="#ED4C5C"/>
|
||||
<path d="M5.57889 23.0525C5.18323 23.0525 4.85352 22.9207 4.85352 22.7228C4.85352 22.5909 4.9854 22.3931 5.24917 22.2612H5.64483L5.57889 23.0525ZM7.09559 22.4591C7.35936 22.525 7.55719 22.5909 7.68907 22.6569C7.75502 22.7228 7.49124 22.9866 7.09559 23.1844V22.4591Z" fill="#ED4C5C"/>
|
||||
<path d="M4.58636 24.5032C4.52042 24.3713 4.98202 24.1075 5.57551 23.9097C5.83928 23.8437 6.03711 23.7118 6.36683 23.5799C7.15815 23.2502 7.81758 22.7886 7.68569 22.6567L7.81758 23.4481C7.88352 23.5799 7.35598 23.9756 6.56466 24.3713C6.30088 24.5032 5.83928 24.701 5.57551 24.7669C5.11391 24.8988 4.71825 25.1626 4.71825 25.2285L4.58636 24.5032Z" fill="#ED4C5C"/>
|
||||
<path d="M19.4273 18.1064H21.5375V19.2934H19.4273V18.1064ZM19.3613 26.811H21.5375V27.932H19.3613V26.811Z" fill="#C8B100"/>
|
||||
<path d="M21.7325 24.9648C21.9304 25.0967 22.0622 25.2286 22.0622 25.2946C22.0622 25.3605 21.9963 25.4264 21.8644 25.4924C21.7325 25.6243 21.6006 25.8221 21.6666 25.8881C21.5347 25.7562 21.4688 25.6243 21.4688 25.4924C21.4688 25.2286 21.6006 25.0308 21.7325 24.9648Z" fill="#ED4C5C"/>
|
||||
<path d="M19.6914 19.2939H21.2081V26.8115H19.6914V19.2939Z" fill="white"/>
|
||||
<path d="M18.1035 22.0634C18.4332 21.9315 19.0267 21.7996 19.6862 21.7996C19.884 21.7996 20.1478 21.7996 20.4115 21.8656C21.3347 21.9975 22.0601 22.3931 21.9942 22.6569L22.3239 21.8656C22.3898 21.5359 21.5985 21.1402 20.6094 20.9424H19.6862C19.0267 20.9424 18.4332 21.0083 18.1035 21.1402V22.0634Z" fill="#ED4C5C"/>
|
||||
<path d="M21.2023 23.0525C21.598 23.0525 21.9277 22.9207 21.9277 22.7228C21.9277 22.5909 21.7958 22.3931 21.532 22.2612H21.1364L21.2023 23.0525ZM19.6856 22.4591C19.4218 22.525 19.224 22.5909 19.0921 22.6569C19.0262 22.7228 19.29 22.9866 19.6856 23.1844V22.4591Z" fill="#ED4C5C"/>
|
||||
<path d="M22.1937 24.5041C22.2596 24.3722 21.798 24.1084 21.2045 23.9106C20.9408 23.8446 20.7429 23.7127 20.4132 23.5809C19.6219 23.2511 18.9625 22.7895 19.0943 22.6577L18.9625 23.449C18.8965 23.5809 19.4241 23.9765 20.2154 24.3722C20.4792 24.5041 20.9408 24.7019 21.2045 24.7678C21.6661 24.8997 22.0618 25.2294 21.9959 25.2954L22.1937 24.5041ZM13.3573 13.8872C14.6102 13.8872 17.182 14.151 18.1052 15.0742C17.1161 17.4481 15.5334 16.459 13.3573 16.459C11.2471 16.459 9.59852 17.4481 8.60938 15.0742C9.53258 14.151 12.0384 13.8872 13.3573 13.8872Z" fill="#ED4C5C"/>
|
||||
<path d="M16.5877 16.5244C15.7964 16.0628 14.6095 15.9969 13.3565 15.9969C12.1036 15.9969 10.9166 16.1288 10.1253 16.5244L10.3891 17.6454C11.1145 17.8433 12.1696 17.9752 13.3565 17.9752C14.5435 17.9752 15.5327 17.8433 16.324 17.6454L16.5877 16.5244ZM17.7088 13.6889C17.445 13.491 16.9175 13.2932 16.4559 13.2932C16.258 13.2932 16.0602 13.2932 15.8624 13.3591C15.8624 13.3591 15.4667 12.8316 14.5435 12.8316C14.2138 12.8316 13.95 12.8975 13.6862 13.0294V12.9635C13.6203 12.8316 13.4884 12.6997 13.3565 12.6997C13.2246 12.6997 13.0268 12.8975 13.0268 13.0294V13.0954C12.763 12.9635 12.4993 12.8975 12.1696 12.8975C11.2463 12.8975 10.8507 13.491 10.8507 13.4251C10.6529 13.3591 10.455 13.3591 10.2572 13.3591C7.22381 13.3591 8.7405 15.4034 8.7405 15.4034L9.07022 15.0077C8.34484 14.0845 9.00427 13.557 10.3231 13.557C10.521 13.557 10.6529 13.557 10.7847 13.6229C10.3231 14.2823 11.1804 14.8758 11.1804 14.8758L11.3782 14.5461C10.9166 14.2164 10.8507 13.0954 12.1696 13.0954C12.4993 13.0954 12.763 13.1613 13.0268 13.3591C13.0268 13.4251 12.9609 14.3483 12.8949 14.4802L13.4225 14.9418L13.95 14.4802C13.8841 14.2823 13.8181 13.4251 13.8181 13.3591C14.016 13.2273 14.3457 13.0954 14.6754 13.0954C16.0602 13.0954 16.0602 14.2164 15.4667 14.5461L15.6645 14.8758C15.6645 14.8758 16.3899 14.2823 16.0602 13.6229C16.1921 13.6229 16.3899 13.557 16.5218 13.557C18.1044 13.557 18.1704 14.7439 17.7747 15.0077L18.0385 15.4034C17.9066 15.4034 18.632 14.4802 17.7088 13.6889Z" fill="#C8B100"/>
|
||||
<path d="M12.9629 12.4357C12.9629 12.2379 13.1607 12.04 13.3586 12.04C13.6223 12.04 13.7542 12.2379 13.7542 12.4357C13.7542 12.6335 13.5564 12.8314 13.3586 12.8314C13.1607 12.8314 12.9629 12.6335 12.9629 12.4357Z" fill="#005BBF"/>
|
||||
<path d="M13.2267 11.3149V11.5128H13.0288V11.7106H13.2267V12.37H12.9629V12.5679H13.7542L13.8202 12.436L13.7542 12.37H13.4904V11.7106H13.6883V11.5128H13.4904V11.3149H13.2267Z" fill="#C8B100"/>
|
||||
<path d="M13.358 17.8435C12.3029 17.8435 11.3797 17.7116 10.6543 17.5138C11.3797 17.316 12.3029 17.1841 13.358 17.1841C14.4131 17.1841 15.3363 17.316 16.0616 17.5138C15.4022 17.7116 14.4131 17.8435 13.358 17.8435Z" fill="#ED4C5C"/>
|
||||
<path d="M13.4237 29.2509C12.1708 29.2509 10.9838 28.9212 9.9287 28.4596C9.13738 28.0639 8.67578 27.3385 8.67578 26.4813V23.316H18.1716V26.4813C18.1716 27.3385 17.6441 28.1298 16.9187 28.4596C15.8636 28.9871 14.6766 29.2509 13.4237 29.2509ZM13.3578 18.0405H18.1057V23.316H13.3578V18.0405Z" fill="white"/>
|
||||
<path d="M13.4237 26.4812C13.4237 27.7341 12.3686 28.7233 11.0497 28.7233C9.73087 28.7233 8.67578 27.7341 8.67578 26.4812V23.3159H13.4237V26.4812Z" fill="#ED4C5C"/>
|
||||
<path d="M9.66447 28.3281C9.79635 28.3941 9.99418 28.526 10.258 28.5919V23.1846H9.73041L9.66447 28.3281ZM8.60938 26.4158C8.60938 27.0752 8.87315 27.6028 9.13692 27.8665V23.1846H8.60938V26.4158Z" fill="#C8B100"/>
|
||||
<path d="M10.7207 28.7238H11.2482V23.1846H10.7207V28.7238Z" fill="#C7B500"/>
|
||||
<path d="M11.7773 28.5919C11.9752 28.526 12.2389 28.3941 12.3708 28.3281V23.1846H11.8433L11.7773 28.5919Z" fill="#C8B100"/>
|
||||
<path d="M8.60938 18.0405H13.3573V23.316H8.60938V18.0405Z" fill="#ED4C5C"/>
|
||||
<path d="M12.8945 27.8665C13.1583 27.6687 13.3561 27.2071 13.4221 26.6795V23.2505H12.8945V27.8665Z" fill="#C8B100"/>
|
||||
<path d="M18.1708 23.3169V26.4821C18.1708 27.7351 17.1157 28.7242 15.7969 28.7242C14.478 28.7242 13.4229 27.7351 13.4229 26.4821V23.3169H18.1708ZM16.4563 18.9646C16.6541 19.3603 16.6541 20.3494 16.0606 20.1516C16.1925 20.2175 16.2585 20.6791 16.4563 20.9429C16.786 21.3386 17.1817 21.0089 17.1157 20.5472C16.9838 19.8219 17.0498 19.3603 17.1817 18.6349C17.1817 18.7008 17.5114 18.7008 17.6433 18.569C17.5773 18.7668 17.5114 19.0306 17.6433 19.0306C17.5114 19.2284 17.1817 19.5581 17.1157 19.7559C17.0498 20.2175 17.7752 21.0748 16.9838 21.2726C16.4563 21.4045 16.786 21.8002 16.9838 21.998C16.9838 21.998 16.7201 22.8553 16.852 22.7893C16.3244 22.9871 16.4563 22.5255 16.4563 22.5255C16.7201 21.7342 15.9947 21.6683 16.0606 21.5364C15.4012 21.4705 16.1266 22.1299 15.5331 22.1299C15.4012 22.1299 15.1374 22.2618 15.1374 22.2618C14.4121 22.1958 14.8077 21.5364 15.0715 21.6023C15.2693 21.6683 15.4671 21.998 15.4671 21.5364C15.4671 21.5364 15.1374 21.0088 15.9947 21.0088C15.665 21.0088 15.4671 20.7451 15.3353 20.4154C15.2034 20.4813 15.0055 20.811 14.2802 20.877C14.2802 20.877 14.0823 20.1516 14.2802 20.2835C14.5439 20.4154 14.6758 20.4154 14.9396 20.1516C14.8077 19.9538 14.0164 19.69 14.1483 19.2284C14.1483 19.0965 14.5439 18.8987 14.5439 18.8987C14.478 19.2284 14.6758 19.5581 15.0715 19.5581C15.599 19.624 15.4012 19.4262 15.4671 19.2943C15.5331 19.1624 15.9287 19.3603 15.7969 19.0306C15.7969 18.9646 15.3353 18.8987 15.4671 18.7008C15.7309 18.3711 16.1266 18.6349 16.4563 18.9646ZM13.4229 28.5923L13.291 28.2626L13.4229 27.8669L13.5548 28.2626L13.4229 28.5923Z" fill="#ED4C5C"/>
|
||||
<path d="M10.0617 19.1618V19.4915H10.1936V19.7553H9.86385V20.4147H10.0617V21.8655H9.66602V22.5909H12.3697V21.8655H12.04V20.4147H12.1719V19.7553H11.8421V19.4915H12.04V19.1618H11.3805V19.4915H11.5124V19.7553H11.1827V18.964H11.3805V18.6343H10.6552V18.964H10.853V19.7553H10.5233V19.4915H10.6552V19.1618H10.0617ZM17.5133 27.2728V23.9757H14.0842V27.2728L15.6669 27.9982H15.8647L17.5133 27.2728ZM15.6669 24.2395V25.3605L14.5458 24.2395H15.6669ZM14.282 24.3054L15.6009 25.6243L14.282 26.9431V24.3054ZM14.4139 27.2069L15.6669 25.954V27.8004L14.4139 27.2069ZM15.8647 27.7344V25.888L17.1176 27.141L15.8647 27.7344ZM17.2495 26.9431L15.9306 25.6243L17.2495 24.3054V26.9431ZM15.8647 24.2395H16.9857L15.8647 25.3605V24.2395Z" fill="#C8B100"/>
|
||||
<path d="M11.8438 23.2507C11.8438 22.2615 12.5032 21.5361 13.3604 21.5361C14.2177 21.5361 14.8771 22.3275 14.8771 23.2507C14.8771 24.1739 14.2177 24.9652 13.3604 24.9652C12.5032 24.9652 11.8438 24.2398 11.8438 23.2507Z" fill="#ED4C5C"/>
|
||||
<path d="M12.3027 23.25C12.3027 22.5246 12.7643 21.9971 13.3578 21.9971C13.9513 21.9971 14.4129 22.5906 14.4129 23.25C14.4129 23.9754 13.9513 24.5029 13.3578 24.5029C12.8303 24.5689 12.3027 23.9754 12.3027 23.25Z" fill="#005BBF"/>
|
||||
<path d="M12.8966 22.3926L12.6328 23.118L12.8306 23.1839L12.6988 23.4477H13.0944L12.9625 23.1839L13.1604 23.118L12.8966 22.3926ZM13.8857 22.3926L13.622 23.118L13.8198 23.1839L13.6879 23.4477H14.0836L14.0176 23.1839L14.2154 23.118L13.8857 22.3926ZM13.4241 23.2498L13.0944 23.9752L13.2922 24.0412L13.2263 24.3049H13.556L13.4901 24.0412L13.6879 23.9752L13.4241 23.2498Z" fill="#C8B100"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 9.8 KiB |
@@ -0,0 +1,9 @@
|
||||
<svg viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.3773 62.0687V47.4062L10.4336 55.4812C13.5148 58.35 17.2336 60.5812 21.3773 62.0687ZM42.6273 62.0687C46.7711 60.5812 50.4898 58.35 53.5711 55.4812L42.6273 47.3V62.0687ZM1.93359 42.625C2.25234 43.6875 2.67734 44.6438 3.20859 45.7063L7.35234 42.625H1.93359ZM56.6523 42.625L60.7961 45.7063C61.2211 44.75 61.6461 43.6875 62.0711 42.625H56.6523Z" fill="#2A5F9E"/>
|
||||
<path d="M22.9699 38.375H0.763672C1.08242 39.8625 1.50742 41.2438 1.93242 42.625H7.35117L3.20742 45.7063C4.05742 47.5125 5.01367 49.1062 6.18242 50.7L17.1262 42.625H21.3762V44.75L8.94492 53.8875L10.4324 55.375L21.3762 47.4062V62.0687C22.7574 62.6 24.1387 62.9188 25.6262 63.2375V38.375H22.9699ZM63.2387 38.375H38.3762V63.2375C39.8637 62.9188 41.2449 62.4937 42.6262 62.0687V47.4062L53.5699 55.375C55.0574 53.9938 56.3324 52.5062 57.6074 50.9125L46.2387 42.625H53.4637L59.9449 47.4062C60.2637 46.875 60.5824 46.2375 60.7949 45.7063L56.6512 42.625H62.0699C62.4949 41.2438 62.9199 39.8625 63.2387 38.375Z" fill="white"/>
|
||||
<path d="M6.18359 50.7C7.03359 51.8687 7.88359 52.9313 8.83984 53.9938L21.3773 44.8562V42.7312H17.1273L6.18359 50.7ZM46.3461 42.625L57.7148 50.9125C58.1398 50.3812 58.4586 49.85 58.8836 49.3187C58.9898 49.2125 58.9898 49.1062 59.0961 49.1062C59.4148 48.575 59.8398 47.9375 60.1586 47.4062L53.4648 42.625H46.3461Z" fill="#ED4C5C"/>
|
||||
<path d="M42.6273 1.93066V16.5932L53.5711 8.51816C50.4898 5.64941 46.7711 3.41816 42.6273 1.93066ZM21.3773 1.93066C17.2336 3.41816 13.5148 5.64941 10.4336 8.51816L21.3773 16.6994V1.93066ZM62.0711 21.3744C61.7523 20.3119 61.3273 19.3557 60.7961 18.2932L56.6523 21.3744H62.0711ZM7.35234 21.3744L3.20859 18.2932C2.78359 19.3557 2.35859 20.3119 1.93359 21.3744H7.35234Z" fill="#2A5F9E"/>
|
||||
<path d="M41.0324 25.6247H63.1324C62.8137 24.1372 62.3887 22.756 61.9637 21.3747H56.5449L60.6887 18.2935C59.8387 16.4872 58.8824 14.8935 57.7137 13.2997L46.8762 21.3747H42.6262V19.2497L55.0574 10.1122L53.5699 8.62471L42.6262 16.5935V1.93096C41.2449 1.39971 39.8637 1.08096 38.3762 0.762207V25.6247H41.0324ZM0.763672 25.6247H25.6262V0.762207C24.1387 1.08096 22.7574 1.50596 21.3762 1.93096V16.5935L10.4324 8.62471C8.94492 10.006 7.66992 11.4935 6.39492 13.0872L17.7637 21.3747H10.5387L4.05742 16.5935C3.73867 17.1247 3.41992 17.7622 3.20742 18.2935L7.35117 21.3747H1.93242C1.50742 22.756 1.08242 24.1372 0.763672 25.6247Z" fill="white"/>
|
||||
<path d="M57.8199 13.3006C56.9699 12.1318 56.1199 11.0693 55.1637 10.0068L42.6262 19.1443V21.2693H46.8762L57.8199 13.3006ZM17.6574 21.3756L6.39492 13.0881C5.96992 13.6193 5.65117 14.1506 5.22617 14.6818C5.11992 14.7881 5.11992 14.8943 5.01367 14.8943C4.69492 15.4256 4.26992 16.0631 3.95117 16.5943L10.4324 21.3756H17.6574Z" fill="#ED4C5C"/>
|
||||
<path d="M63.2375 25.625H38.375V0.7625C36.3563 0.3375 34.2312 0.125 32 0.125C28.6531 0.125 27.6437 0.3375 25.625 0.7625V25.625H0.7625C0.3375 27.6437 0.125 29.7688 0.125 32C0.125 35.3469 0.3375 36.3563 0.7625 38.375H25.625V63.2375C27.6437 63.6625 29.7688 63.875 32 63.875C35.3469 63.875 36.3563 63.6625 38.375 63.2375V38.375H63.2375C63.6625 36.3563 63.875 34.2312 63.875 32C63.875 28.6531 63.6625 27.6437 63.2375 25.625Z" fill="#ED4C5C"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
3
packages/ui/shared/src/assets/svg/logo-text.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 667 84" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M38.887 82H26.497V12.2781H0.140091V1.12708H65.2439V12.2781H38.887V82ZM106.939 57.4453V22.3027H118.315V82H107.727V72.5385C104.573 78.7335 97.5899 83.1264 89.4801 83.1264C77.428 83.1264 69.0929 76.0303 69.0929 60.7117V22.3027H80.4692V58.459C80.4692 68.8215 85.6505 73.1017 93.0845 73.1017C100.744 73.1017 106.939 66.7941 106.939 57.4453ZM144.364 51.4755V82H132.988V22.3027H143.575V34.918C147.743 26.6955 156.754 21.5143 166.553 21.5143V33.3411C153.713 32.6653 144.364 38.2971 144.364 51.4755ZM231.424 52.1514C231.424 70.0605 221.061 83.1264 204.954 83.1264C196.507 83.1264 189.298 78.8462 185.018 71.2995V82H174.43V1.12708H185.806V32.5526C189.974 25.2313 196.845 21.1764 204.954 21.1764C220.949 21.1764 231.424 34.0169 231.424 52.1514ZM219.597 52.1514C219.597 38.635 212.501 31.201 202.702 31.201C193.24 31.201 185.806 38.5224 185.806 51.9261C185.806 65.1045 193.015 72.9891 202.702 72.9891C212.501 72.9891 219.597 65.4425 219.597 52.1514ZM266.837 83.1264C249.941 83.1264 237.551 69.8353 237.551 52.1514C237.551 34.4675 249.941 21.1764 266.837 21.1764C283.732 21.1764 296.122 34.4675 296.122 52.1514C296.122 69.8353 283.732 83.1264 266.837 83.1264ZM266.837 73.1017C276.636 73.1017 284.408 65.2172 284.408 52.1514C284.408 39.0855 276.636 31.3136 266.837 31.3136C257.037 31.3136 249.378 39.0855 249.378 52.1514C249.378 65.2172 257.037 73.1017 266.837 73.1017ZM366.628 58.3464C366.628 72.4259 355.364 83.1264 335.766 83.1264C316.28 83.1264 304.453 72.4259 303.439 56.3189H316.054C316.617 66.5688 323.15 73.2144 335.54 73.2144C345.79 73.2144 353.45 68.371 353.45 60.0359C353.45 53.2777 349.057 49.8986 339.708 47.8712L327.994 45.6185C316.617 43.3657 306.255 37.6213 306.255 23.8796C306.255 10.2506 318.194 0.000719194 334.977 0.000719194C351.76 0.000719194 364.488 10.2506 365.502 26.3576H352.886C352.211 16.6709 345.227 9.91272 335.09 9.91272C324.615 9.91272 318.87 16.1077 318.87 23.0912C318.87 30.7505 325.516 33.4537 333.062 35.0306L345.002 37.396C358.856 40.2119 366.628 46.2943 366.628 58.3464ZM412.842 70.9616V80.9863C409.351 82.5632 406.309 83.1264 402.705 83.1264C391.667 83.1264 384.007 77.1566 384.007 63.9782V31.9894H370.829V22.3027H384.007V4.61881H395.384V22.3027H413.406V31.9894H395.384V61.3875C395.384 69.61 399.326 72.5385 405.408 72.5385C408.112 72.5385 410.477 72.088 412.842 70.9616ZM459.293 82V72.7638C455.576 79.4094 448.93 83.1264 440.144 83.1264C427.754 83.1264 419.645 76.0303 419.645 65.1045C419.645 53.3904 428.993 47.308 446.79 47.308C450.282 47.308 453.098 47.4206 457.941 47.9838V43.591C457.941 35.0306 453.323 30.1873 445.438 30.1873C437.103 30.1873 432.035 35.1433 431.697 43.4784H421.334C421.897 30.0746 431.471 21.1764 445.438 21.1764C460.194 21.1764 468.754 29.5115 468.754 43.7036V82H459.293ZM430.458 64.7666C430.458 70.9616 435.076 75.0165 442.397 75.0165C451.971 75.0165 457.941 69.0468 457.941 59.9233V55.0799C453.548 54.5167 450.394 54.4041 447.466 54.4041C436.089 54.4041 430.458 57.7832 430.458 64.7666ZM494.214 51.4755V82H482.838V22.3027H493.426V34.918C497.593 26.6955 506.604 21.5143 516.404 21.5143V33.3411C503.563 32.6653 494.214 38.2971 494.214 51.4755ZM561.588 70.9616V80.9863C558.097 82.5632 555.055 83.1264 551.451 83.1264C540.413 83.1264 532.753 77.1566 532.753 63.9782V31.9894H519.575V22.3027H532.753V4.61881H544.13V22.3027H562.152V31.9894H544.13V61.3875C544.13 69.61 548.072 72.5385 554.154 72.5385C556.858 72.5385 559.223 72.088 561.588 70.9616ZM594.787 83.1264C577.554 83.1264 565.952 70.6237 565.952 51.8135C565.952 34.1295 578.004 21.1764 594.449 21.1764C612.246 21.1764 624.073 35.5938 622.045 54.9673H577.554C578.455 67.132 584.537 74.2281 594.562 74.2281C603.01 74.2281 608.867 69.61 610.781 61.8381H622.045C619.117 75.1292 608.867 83.1264 594.787 83.1264ZM594.224 29.7367C585.1 29.7367 578.905 36.2696 577.666 47.4206H609.993C609.43 36.3823 603.46 29.7367 594.224 29.7367ZM644.39 51.4755V82H633.014V22.3027H643.602V34.918C647.769 26.6955 656.78 21.5143 666.58 21.5143V33.3411C653.739 32.6653 644.39 38.2971 644.39 51.4755Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
3
packages/ui/shared/src/assets/svg/logo.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg viewBox="0 0 512 517" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M383.309 10.9714C383.309 4.91208 388.221 0 394.28 0C400.339 0 405.251 4.91208 405.251 10.9714V122.57C405.251 138.493 398.332 153.631 386.292 164.051L367.707 180.134L391.678 204.105L428.239 190.516C441.132 185.724 449.686 173.419 449.686 159.664V58.5143C449.686 52.4549 454.598 47.5429 460.657 47.5429C466.717 47.5429 471.629 52.4549 471.629 58.5143V159.664C471.629 182.589 457.372 203.097 435.883 211.084L405.175 222.498C415.977 243.457 415.977 268.543 405.175 289.502L440.649 302.687C459.273 309.609 471.629 327.383 471.629 347.251V453.486C471.629 459.545 466.717 464.457 460.657 464.457C454.598 464.457 449.686 459.545 449.686 453.486V347.251C449.686 336.553 443.033 326.983 433.005 323.255L391.678 307.895L367.707 331.866L388.819 350.137C399.255 359.167 405.251 372.287 405.251 386.087V501.029C405.251 507.088 400.339 512 394.28 512C388.221 512 383.309 507.088 383.309 501.029V386.087C383.309 378.656 380.08 371.592 374.461 366.729L352.151 347.423L266.345 433.229C260.632 438.941 251.37 438.941 245.657 433.229L160.142 347.713L138.168 366.729C132.549 371.592 129.32 378.656 129.32 386.087V501.029C129.32 507.088 124.408 512 118.349 512C112.289 512 107.377 507.088 107.377 501.029V386.087C107.377 372.287 113.374 359.167 123.809 350.137L144.586 332.157L120.493 308.065L79.624 323.255C69.5957 326.983 62.9429 336.553 62.9429 347.251V453.486C62.9429 459.545 58.0308 464.457 51.9714 464.457C45.9121 464.457 41 459.545 41 453.486V347.251C41 327.383 53.3553 309.609 71.9792 302.687L106.928 289.698C95.991 268.634 95.991 243.366 106.928 222.303L76.7452 211.084C55.2562 203.097 41 182.589 41 159.664V58.5143C41 52.4549 45.9121 47.5429 51.9714 47.5429C58.0308 47.5429 62.9429 52.4549 62.9429 58.5143V159.664C62.9429 173.419 71.4966 185.724 84.39 190.516L120.494 203.935L144.586 179.843L126.337 164.051C114.296 153.631 107.377 138.493 107.377 122.57V10.9714C107.377 4.91208 112.289 0 118.349 0C124.408 0 129.32 4.91208 129.32 10.9714V122.57C129.32 132.124 133.472 141.206 140.696 147.458L160.142 164.287L245.657 78.7717C251.37 73.0588 260.632 73.0589 266.345 78.7717L352.151 164.577L371.933 147.458C379.157 141.206 383.309 132.124 383.309 122.57V10.9714ZM267.067 166.768C272.383 161.416 281.338 166.639 279.298 173.901L268.742 211.469C266.776 218.467 272.035 225.408 279.304 225.408H318.878C335.139 225.408 343.311 245.043 331.851 256.58L244.619 344.4C239.303 349.752 230.348 344.529 232.388 337.267L242.944 299.699C244.91 292.701 239.65 285.76 232.381 285.76H192.808C176.547 285.76 168.375 266.125 179.835 254.588L267.067 166.768Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
5
packages/ui/shared/src/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
/// <reference types="./typings/svg.d.ts" />
|
||||
|
||||
export * from "./utils";
|
||||
export * from "./styles/themes/themes";
|
||||
export * from "./types";
|
||||
51
packages/ui/shared/src/styles/globals.css
Normal file
@@ -0,0 +1,51 @@
|
||||
@import "tailwindcss";
|
||||
@import "./themes/variables.css";
|
||||
|
||||
@custom-variant light (&:is(.light *));
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--radius: 0.65rem;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
--color-card-foreground: var(--card-foreground);
|
||||
--color-popover: var(--popover);
|
||||
--color-popover-foreground: var(--popover-foreground);
|
||||
--color-primary: var(--primary);
|
||||
--color-primary-foreground: var(--primary-foreground);
|
||||
--color-secondary: var(--secondary);
|
||||
--color-secondary-foreground: var(--secondary-foreground);
|
||||
--color-muted: var(--muted);
|
||||
--color-muted-foreground: var(--muted-foreground);
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-success: var(--success);
|
||||
--color-success-foreground: var(--success-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
--color-chart-1: var(--chart-1);
|
||||
--color-chart-2: var(--chart-2);
|
||||
--color-chart-3: var(--chart-3);
|
||||
--color-chart-4: var(--chart-4);
|
||||
--color-chart-5: var(--chart-5);
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||
--color-sidebar-accent: var(--sidebar-accent);
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
}
|
||||
86
packages/ui/shared/src/styles/themes/build.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
import { themes } from "./themes";
|
||||
|
||||
import type { ColorVariable, ThemeColorsVariables } from "../../types";
|
||||
|
||||
const OUTPUT_PATH = "variables.css";
|
||||
|
||||
const oklchToString = (l: number, c: number, h: number, a?: number) =>
|
||||
`oklch(${l} ${c} ${h}${a ? ` / ${a * 100}%` : ""})`;
|
||||
|
||||
const indent = (level: number) => " ".repeat(level);
|
||||
|
||||
const generateCSSVariables = (theme: ThemeColorsVariables, indentLevel = 0) =>
|
||||
Object.entries(theme)
|
||||
.map(
|
||||
([key, value]: [string, ColorVariable]) =>
|
||||
// @ts-expect-error - value is actually a tuple
|
||||
`${indent(indentLevel)}--${key}: ${oklchToString(...value)};`,
|
||||
)
|
||||
.join("\n");
|
||||
|
||||
const generateThemeCSS = () => {
|
||||
const themeEntries = Object.entries(themes);
|
||||
const defaultTheme = themeEntries[0]?.[1];
|
||||
|
||||
if (!defaultTheme) {
|
||||
throw new Error("No themes found");
|
||||
}
|
||||
|
||||
const generatedFileHeader = [
|
||||
"/*",
|
||||
" * This file is auto-generated. Do NOT edit it directly.",
|
||||
" *",
|
||||
" * Edit the theme source files instead:",
|
||||
" * - ./themes.ts",
|
||||
" * - ./colors/*.ts",
|
||||
" */",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
const defaultThemeCss = [
|
||||
`${indent(2)}@variant light {`,
|
||||
generateCSSVariables(defaultTheme.light, 3),
|
||||
`${indent(2)}}`,
|
||||
"",
|
||||
`${indent(2)}@variant dark {`,
|
||||
generateCSSVariables(defaultTheme.dark, 3),
|
||||
`${indent(2)}}`,
|
||||
].join("\n");
|
||||
|
||||
const themeCss = themeEntries
|
||||
.map(([themeName, { light, dark }]) =>
|
||||
[
|
||||
`${indent(2)}[data-theme="${themeName}"] {`,
|
||||
generateCSSVariables(light, 3),
|
||||
"",
|
||||
`${indent(3)}@variant dark {`,
|
||||
generateCSSVariables(dark, 4),
|
||||
`${indent(3)}}`,
|
||||
`${indent(2)}}`,
|
||||
].join("\n"),
|
||||
)
|
||||
.join("\n\n");
|
||||
|
||||
return [
|
||||
generatedFileHeader,
|
||||
"@layer theme {",
|
||||
`${indent(1)}:root {`,
|
||||
defaultThemeCss,
|
||||
"",
|
||||
themeCss,
|
||||
`${indent(1)}}`,
|
||||
"}",
|
||||
].join("\n");
|
||||
};
|
||||
|
||||
const outputPath = path.join(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
`./${OUTPUT_PATH}`,
|
||||
);
|
||||
|
||||
fs.writeFileSync(outputPath, `${generateThemeCSS()}\n`, "utf8");
|
||||
console.log(`CSS file generated at: ${outputPath}`);
|
||||
78
packages/ui/shared/src/styles/themes/colors/blue.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { ThemeMode } from "../../../types";
|
||||
|
||||
import type { ThemeColors } from "../../../types";
|
||||
|
||||
export const blue = {
|
||||
[ThemeMode.LIGHT]: {
|
||||
background: [1, 0, 0],
|
||||
foreground: [0.141, 0.005, 285.823],
|
||||
card: [1, 0, 0],
|
||||
"card-foreground": [0.141, 0.005, 285.823],
|
||||
popover: [1, 0, 0],
|
||||
"popover-foreground": [0.141, 0.005, 285.823],
|
||||
primary: [0.623, 0.214, 259.815],
|
||||
"primary-foreground": [0.97, 0.014, 254.604],
|
||||
secondary: [0.967, 0.001, 286.375],
|
||||
"secondary-foreground": [0.21, 0.006, 285.885],
|
||||
muted: [0.967, 0.001, 286.375],
|
||||
"muted-foreground": [0.552, 0.016, 285.938],
|
||||
accent: [0.967, 0.001, 286.375],
|
||||
"accent-foreground": [0.21, 0.006, 285.885],
|
||||
destructive: [0.577, 0.245, 27.325],
|
||||
"destructive-foreground": [0.985, 0.002, 247.839],
|
||||
success: [0.7999, 0.1816, 151.78],
|
||||
"success-foreground": [0.9848, 0, 0],
|
||||
border: [0.92, 0.004, 286.32],
|
||||
input: [0.92, 0.004, 286.32],
|
||||
ring: [0.623, 0.214, 259.815],
|
||||
"chart-1": [0.809, 0.105, 251.813],
|
||||
"chart-2": [0.623, 0.214, 259.815],
|
||||
"chart-3": [0.546, 0.245, 262.881],
|
||||
"chart-4": [0.488, 0.243, 264.376],
|
||||
"chart-5": [0.424, 0.199, 265.638],
|
||||
sidebar: [0.985, 0, 0],
|
||||
"sidebar-foreground": [0.141, 0.005, 285.823],
|
||||
"sidebar-primary": [0.623, 0.214, 259.815],
|
||||
"sidebar-primary-foreground": [0.97, 0.014, 254.604],
|
||||
"sidebar-accent": [0.967, 0.001, 286.375],
|
||||
"sidebar-accent-foreground": [0.21, 0.006, 285.885],
|
||||
"sidebar-border": [0.92, 0.004, 286.32],
|
||||
"sidebar-ring": [0.623, 0.214, 259.815],
|
||||
},
|
||||
[ThemeMode.DARK]: {
|
||||
background: [0.141, 0.005, 285.823],
|
||||
foreground: [0.985, 0, 0],
|
||||
card: [0.21, 0.006, 285.885],
|
||||
"card-foreground": [0.985, 0, 0],
|
||||
popover: [0.21, 0.006, 285.885],
|
||||
"popover-foreground": [0.985, 0, 0],
|
||||
primary: [0.546, 0.245, 262.881],
|
||||
"primary-foreground": [0.379, 0.146, 265.522],
|
||||
secondary: [0.274, 0.006, 286.033],
|
||||
"secondary-foreground": [0.985, 0, 0],
|
||||
muted: [0.274, 0.006, 286.033],
|
||||
"muted-foreground": [0.705, 0.015, 286.067],
|
||||
accent: [0.274, 0.006, 286.033],
|
||||
"accent-foreground": [0.985, 0, 0],
|
||||
destructive: [0.704, 0.191, 22.216],
|
||||
"destructive-foreground": [0.985, 0.002, 247.839],
|
||||
success: [0.627, 0.194, 149.214],
|
||||
"success-foreground": [0.985, 0, 0],
|
||||
border: [1, 0, 0, 0.1],
|
||||
input: [1, 0, 0, 0.15],
|
||||
ring: [0.488, 0.243, 264.376],
|
||||
"chart-1": [0.809, 0.105, 251.813],
|
||||
"chart-2": [0.623, 0.214, 259.815],
|
||||
"chart-3": [0.546, 0.245, 262.881],
|
||||
"chart-4": [0.488, 0.243, 264.376],
|
||||
"chart-5": [0.424, 0.199, 265.638],
|
||||
sidebar: [0.21, 0.006, 285.885],
|
||||
"sidebar-foreground": [0.985, 0, 0],
|
||||
"sidebar-primary": [0.546, 0.245, 262.881],
|
||||
"sidebar-primary-foreground": [0.379, 0.146, 265.522],
|
||||
"sidebar-accent": [0.274, 0.006, 286.033],
|
||||
"sidebar-accent-foreground": [0.985, 0, 0],
|
||||
"sidebar-border": [1, 0, 0, 0.1],
|
||||
"sidebar-ring": [0.488, 0.243, 264.376],
|
||||
},
|
||||
} satisfies ThemeColors;
|
||||
78
packages/ui/shared/src/styles/themes/colors/gray.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { ThemeMode } from "../../../types";
|
||||
|
||||
import type { ThemeColors } from "../../../types";
|
||||
|
||||
export const gray = {
|
||||
[ThemeMode.LIGHT]: {
|
||||
background: [1, 0, 0],
|
||||
foreground: [0.13, 0.028, 261.692],
|
||||
card: [1, 0, 0],
|
||||
"card-foreground": [0.13, 0.028, 261.692],
|
||||
popover: [1, 0, 0],
|
||||
"popover-foreground": [0.13, 0.028, 261.692],
|
||||
primary: [0.21, 0.034, 264.665],
|
||||
"primary-foreground": [0.985, 0.002, 247.839],
|
||||
secondary: [0.967, 0.003, 264.542],
|
||||
"secondary-foreground": [0.21, 0.034, 264.665],
|
||||
muted: [0.967, 0.003, 264.542],
|
||||
"muted-foreground": [0.551, 0.027, 264.364],
|
||||
accent: [0.967, 0.003, 264.542],
|
||||
"accent-foreground": [0.21, 0.034, 264.665],
|
||||
destructive: [0.577, 0.245, 27.325],
|
||||
"destructive-foreground": [0.985, 0.002, 247.839],
|
||||
success: [0.7999, 0.1816, 151.78],
|
||||
"success-foreground": [0.9848, 0, 0],
|
||||
border: [0.928, 0.006, 264.531],
|
||||
input: [0.928, 0.006, 264.531],
|
||||
ring: [0.707, 0.022, 261.325],
|
||||
"chart-1": [0.872, 0.01, 258.338],
|
||||
"chart-2": [0.551, 0.027, 264.364],
|
||||
"chart-3": [0.446, 0.03, 256.802],
|
||||
"chart-4": [0.373, 0.034, 259.733],
|
||||
"chart-5": [0.278, 0.033, 256.848],
|
||||
sidebar: [0.985, 0.002, 247.839],
|
||||
"sidebar-foreground": [0.13, 0.028, 261.692],
|
||||
"sidebar-primary": [0.21, 0.034, 264.665],
|
||||
"sidebar-primary-foreground": [0.985, 0.002, 247.839],
|
||||
"sidebar-accent": [0.967, 0.003, 264.542],
|
||||
"sidebar-accent-foreground": [0.21, 0.034, 264.665],
|
||||
"sidebar-border": [0.928, 0.006, 264.531],
|
||||
"sidebar-ring": [0.707, 0.022, 261.325],
|
||||
},
|
||||
[ThemeMode.DARK]: {
|
||||
background: [0.13, 0.028, 261.692],
|
||||
foreground: [0.985, 0.002, 247.839],
|
||||
card: [0.21, 0.034, 264.665],
|
||||
"card-foreground": [0.985, 0.002, 247.839],
|
||||
popover: [0.21, 0.034, 264.665],
|
||||
"popover-foreground": [0.985, 0.002, 247.839],
|
||||
primary: [0.928, 0.006, 264.531],
|
||||
"primary-foreground": [0.21, 0.034, 264.665],
|
||||
secondary: [0.278, 0.033, 256.848],
|
||||
"secondary-foreground": [0.985, 0.002, 247.839],
|
||||
muted: [0.278, 0.033, 256.848],
|
||||
"muted-foreground": [0.707, 0.022, 261.325],
|
||||
accent: [0.278, 0.033, 256.848],
|
||||
"accent-foreground": [0.985, 0.002, 247.839],
|
||||
destructive: [0.704, 0.191, 22.216],
|
||||
"destructive-foreground": [0.985, 0.002, 247.839],
|
||||
success: [0.627, 0.194, 149.214],
|
||||
"success-foreground": [0.985, 0, 0],
|
||||
border: [1, 0, 0, 0.1],
|
||||
input: [1, 0, 0, 0.15],
|
||||
ring: [0.551, 0.027, 264.364],
|
||||
"chart-1": [0.872, 0.01, 258.338],
|
||||
"chart-2": [0.551, 0.027, 264.364],
|
||||
"chart-3": [0.446, 0.03, 256.802],
|
||||
"chart-4": [0.373, 0.034, 259.733],
|
||||
"chart-5": [0.278, 0.033, 256.848],
|
||||
sidebar: [0.21, 0.034, 264.665],
|
||||
"sidebar-foreground": [0.985, 0.002, 247.839],
|
||||
"sidebar-primary": [0.488, 0.243, 264.376],
|
||||
"sidebar-primary-foreground": [0.985, 0.002, 247.839],
|
||||
"sidebar-accent": [0.278, 0.033, 256.848],
|
||||
"sidebar-accent-foreground": [0.985, 0.002, 247.839],
|
||||
"sidebar-border": [1, 0, 0, 0.1],
|
||||
"sidebar-ring": [0.551, 0.027, 264.364],
|
||||
},
|
||||
} satisfies ThemeColors;
|
||||
78
packages/ui/shared/src/styles/themes/colors/green.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { ThemeMode } from "../../../types";
|
||||
|
||||
import type { ThemeColors } from "../../../types";
|
||||
|
||||
export const green = {
|
||||
[ThemeMode.LIGHT]: {
|
||||
background: [1, 0, 0],
|
||||
foreground: [0.141, 0.005, 285.823],
|
||||
card: [1, 0, 0],
|
||||
"card-foreground": [0.141, 0.005, 285.823],
|
||||
popover: [1, 0, 0],
|
||||
"popover-foreground": [0.141, 0.005, 285.823],
|
||||
primary: [0.723, 0.219, 149.579],
|
||||
"primary-foreground": [0.982, 0.018, 155.826],
|
||||
secondary: [0.967, 0.001, 286.375],
|
||||
"secondary-foreground": [0.21, 0.006, 285.885],
|
||||
muted: [0.967, 0.001, 286.375],
|
||||
"muted-foreground": [0.552, 0.016, 285.938],
|
||||
accent: [0.967, 0.001, 286.375],
|
||||
"accent-foreground": [0.21, 0.006, 285.885],
|
||||
destructive: [0.577, 0.245, 27.325],
|
||||
"destructive-foreground": [0.985, 0.002, 247.839],
|
||||
success: [0.7999, 0.1816, 151.78],
|
||||
"success-foreground": [0.9848, 0, 0],
|
||||
border: [0.92, 0.004, 286.32],
|
||||
input: [0.92, 0.004, 286.32],
|
||||
ring: [0.723, 0.219, 149.579],
|
||||
"chart-1": [0.871, 0.15, 154.449],
|
||||
"chart-2": [0.723, 0.219, 149.579],
|
||||
"chart-3": [0.627, 0.194, 149.214],
|
||||
"chart-4": [0.527, 0.154, 150.069],
|
||||
"chart-5": [0.448, 0.119, 151.328],
|
||||
sidebar: [0.985, 0, 0],
|
||||
"sidebar-foreground": [0.141, 0.005, 285.823],
|
||||
"sidebar-primary": [0.723, 0.219, 149.579],
|
||||
"sidebar-primary-foreground": [0.982, 0.018, 155.826],
|
||||
"sidebar-accent": [0.967, 0.001, 286.375],
|
||||
"sidebar-accent-foreground": [0.21, 0.006, 285.885],
|
||||
"sidebar-border": [0.92, 0.004, 286.32],
|
||||
"sidebar-ring": [0.723, 0.219, 149.579],
|
||||
},
|
||||
[ThemeMode.DARK]: {
|
||||
background: [0.141, 0.005, 285.823],
|
||||
foreground: [0.985, 0, 0],
|
||||
card: [0.21, 0.006, 285.885],
|
||||
"card-foreground": [0.985, 0, 0],
|
||||
popover: [0.21, 0.006, 285.885],
|
||||
"popover-foreground": [0.985, 0, 0],
|
||||
primary: [0.696, 0.17, 162.48],
|
||||
"primary-foreground": [0.393, 0.095, 152.535],
|
||||
secondary: [0.274, 0.006, 286.033],
|
||||
"secondary-foreground": [0.985, 0, 0],
|
||||
muted: [0.274, 0.006, 286.033],
|
||||
"muted-foreground": [0.705, 0.015, 286.067],
|
||||
accent: [0.274, 0.006, 286.033],
|
||||
"accent-foreground": [0.985, 0, 0],
|
||||
destructive: [0.704, 0.191, 22.216],
|
||||
"destructive-foreground": [0.985, 0.002, 247.839],
|
||||
success: [0.627, 0.194, 149.214],
|
||||
"success-foreground": [0.985, 0, 0],
|
||||
border: [1, 0, 0, 0.1],
|
||||
input: [1, 0, 0, 0.15],
|
||||
ring: [0.527, 0.154, 150.069],
|
||||
"chart-1": [0.871, 0.15, 154.449],
|
||||
"chart-2": [0.723, 0.219, 149.579],
|
||||
"chart-3": [0.627, 0.194, 149.214],
|
||||
"chart-4": [0.527, 0.154, 150.069],
|
||||
"chart-5": [0.448, 0.119, 151.328],
|
||||
sidebar: [0.21, 0.006, 285.885],
|
||||
"sidebar-foreground": [0.985, 0, 0],
|
||||
"sidebar-primary": [0.696, 0.17, 162.48],
|
||||
"sidebar-primary-foreground": [0.393, 0.095, 152.535],
|
||||
"sidebar-accent": [0.274, 0.006, 286.033],
|
||||
"sidebar-accent-foreground": [0.985, 0, 0],
|
||||
"sidebar-border": [1, 0, 0, 0.1],
|
||||
"sidebar-ring": [0.527, 0.154, 150.069],
|
||||
},
|
||||
} satisfies ThemeColors;
|
||||
78
packages/ui/shared/src/styles/themes/colors/orange.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { ThemeMode } from "../../../types";
|
||||
|
||||
import type { ThemeColors } from "../../../types";
|
||||
|
||||
export const orange = {
|
||||
[ThemeMode.LIGHT]: {
|
||||
background: [1, 0, 0],
|
||||
foreground: [0.141, 0.005, 285.823],
|
||||
card: [1, 0, 0],
|
||||
"card-foreground": [0.141, 0.005, 285.823],
|
||||
popover: [1, 0, 0],
|
||||
"popover-foreground": [0.141, 0.005, 285.823],
|
||||
primary: [0.6387, 0.2151, 36.46],
|
||||
"primary-foreground": [0.98, 0.016, 73.684],
|
||||
secondary: [0.967, 0.001, 286.375],
|
||||
"secondary-foreground": [0.21, 0.006, 285.885],
|
||||
muted: [0.967, 0.001, 286.375],
|
||||
"muted-foreground": [0.552, 0.016, 285.938],
|
||||
accent: [0.967, 0.001, 286.375],
|
||||
"accent-foreground": [0.21, 0.006, 285.885],
|
||||
destructive: [0.577, 0.245, 27.325],
|
||||
"destructive-foreground": [0.985, 0.002, 247.839],
|
||||
success: [0.7999, 0.1816, 151.78],
|
||||
"success-foreground": [0.9848, 0, 0],
|
||||
border: [0.92, 0.004, 286.32],
|
||||
input: [0.92, 0.004, 286.32],
|
||||
ring: [0.6387, 0.2151, 36.46],
|
||||
"chart-1": [0.6387, 0.2151, 36.46],
|
||||
"chart-2": [0.7027, 0.1783, 44.12],
|
||||
"chart-3": [0.7732, 0.1275, 51.73],
|
||||
"chart-4": [0.8478, 0.0818, 58.78],
|
||||
"chart-5": [0.92, 0.0421, 65.38],
|
||||
sidebar: [0.985, 0, 0],
|
||||
"sidebar-foreground": [0.141, 0.005, 285.823],
|
||||
"sidebar-primary": [0.6387, 0.2151, 36.46],
|
||||
"sidebar-primary-foreground": [0.98, 0.016, 73.684],
|
||||
"sidebar-accent": [0.967, 0.001, 286.375],
|
||||
"sidebar-accent-foreground": [0.21, 0.006, 285.885],
|
||||
"sidebar-border": [0.92, 0.004, 286.32],
|
||||
"sidebar-ring": [0.6387, 0.2151, 36.46],
|
||||
},
|
||||
[ThemeMode.DARK]: {
|
||||
background: [0.141, 0.005, 285.823],
|
||||
foreground: [0.985, 0, 0],
|
||||
card: [0.21, 0.006, 285.885],
|
||||
"card-foreground": [0.985, 0, 0],
|
||||
popover: [0.21, 0.006, 285.885],
|
||||
"popover-foreground": [0.985, 0, 0],
|
||||
primary: [0.6387, 0.2151, 36.46],
|
||||
"primary-foreground": [0.98, 0.016, 73.684],
|
||||
secondary: [0.274, 0.006, 286.033],
|
||||
"secondary-foreground": [0.985, 0, 0],
|
||||
muted: [0.274, 0.006, 286.033],
|
||||
"muted-foreground": [0.705, 0.015, 286.067],
|
||||
accent: [0.274, 0.006, 286.033],
|
||||
"accent-foreground": [0.985, 0, 0],
|
||||
destructive: [0.704, 0.191, 22.216],
|
||||
"destructive-foreground": [0.985, 0.002, 247.839],
|
||||
success: [0.627, 0.194, 149.214],
|
||||
"success-foreground": [0.985, 0, 0],
|
||||
border: [1, 0, 0, 0.1],
|
||||
input: [1, 0, 0, 0.15],
|
||||
ring: [0.6387, 0.2151, 36.46],
|
||||
"chart-1": [0.6387, 0.2151, 36.46],
|
||||
"chart-2": [0.7027, 0.1783, 44.12],
|
||||
"chart-3": [0.7732, 0.1275, 51.73],
|
||||
"chart-4": [0.8478, 0.0818, 58.78],
|
||||
"chart-5": [0.92, 0.0421, 65.38],
|
||||
sidebar: [0.21, 0.006, 285.885],
|
||||
"sidebar-foreground": [0.985, 0, 0],
|
||||
"sidebar-primary": [0.6387, 0.2151, 36.46],
|
||||
"sidebar-primary-foreground": [0.98, 0.016, 73.684],
|
||||
"sidebar-accent": [0.274, 0.006, 286.033],
|
||||
"sidebar-accent-foreground": [0.985, 0, 0],
|
||||
"sidebar-border": [1, 0, 0, 0.1],
|
||||
"sidebar-ring": [0.6387, 0.2151, 36.46],
|
||||
},
|
||||
} satisfies ThemeColors;
|
||||
78
packages/ui/shared/src/styles/themes/colors/red.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { ThemeMode } from "../../../types";
|
||||
|
||||
import type { ThemeColors } from "../../../types";
|
||||
|
||||
export const red = {
|
||||
[ThemeMode.LIGHT]: {
|
||||
background: [1, 0, 0],
|
||||
foreground: [0.141, 0.005, 285.823],
|
||||
card: [1, 0, 0],
|
||||
"card-foreground": [0.141, 0.005, 285.823],
|
||||
popover: [1, 0, 0],
|
||||
"popover-foreground": [0.141, 0.005, 285.823],
|
||||
primary: [0.637, 0.237, 25.331],
|
||||
"primary-foreground": [0.971, 0.013, 17.38],
|
||||
secondary: [0.967, 0.001, 286.375],
|
||||
"secondary-foreground": [0.21, 0.006, 285.885],
|
||||
muted: [0.967, 0.001, 286.375],
|
||||
"muted-foreground": [0.552, 0.016, 285.938],
|
||||
accent: [0.967, 0.001, 286.375],
|
||||
"accent-foreground": [0.21, 0.006, 285.885],
|
||||
destructive: [0.577, 0.245, 27.325],
|
||||
"destructive-foreground": [0.985, 0.002, 247.839],
|
||||
success: [0.7999, 0.1816, 151.78],
|
||||
"success-foreground": [0.9848, 0, 0],
|
||||
border: [0.92, 0.004, 286.32],
|
||||
input: [0.92, 0.004, 286.32],
|
||||
ring: [0.637, 0.237, 25.331],
|
||||
"chart-1": [0.808, 0.114, 19.571],
|
||||
"chart-2": [0.637, 0.237, 25.331],
|
||||
"chart-3": [0.577, 0.245, 27.325],
|
||||
"chart-4": [0.505, 0.213, 27.518],
|
||||
"chart-5": [0.444, 0.177, 26.899],
|
||||
sidebar: [0.985, 0, 0],
|
||||
"sidebar-foreground": [0.141, 0.005, 285.823],
|
||||
"sidebar-primary": [0.637, 0.237, 25.331],
|
||||
"sidebar-primary-foreground": [0.971, 0.013, 17.38],
|
||||
"sidebar-accent": [0.967, 0.001, 286.375],
|
||||
"sidebar-accent-foreground": [0.21, 0.006, 285.885],
|
||||
"sidebar-border": [0.92, 0.004, 286.32],
|
||||
"sidebar-ring": [0.637, 0.237, 25.331],
|
||||
},
|
||||
[ThemeMode.DARK]: {
|
||||
background: [0.141, 0.005, 285.823],
|
||||
foreground: [0.985, 0, 0],
|
||||
card: [0.21, 0.006, 285.885],
|
||||
"card-foreground": [0.985, 0, 0],
|
||||
popover: [0.21, 0.006, 285.885],
|
||||
"popover-foreground": [0.985, 0, 0],
|
||||
primary: [0.637, 0.237, 25.331],
|
||||
"primary-foreground": [0.971, 0.013, 17.38],
|
||||
secondary: [0.274, 0.006, 286.033],
|
||||
"secondary-foreground": [0.985, 0, 0],
|
||||
muted: [0.274, 0.006, 286.033],
|
||||
"muted-foreground": [0.705, 0.015, 286.067],
|
||||
accent: [0.274, 0.006, 286.033],
|
||||
"accent-foreground": [0.985, 0, 0],
|
||||
destructive: [0.704, 0.191, 22.216],
|
||||
"destructive-foreground": [0.985, 0.002, 247.839],
|
||||
success: [0.627, 0.194, 149.214],
|
||||
"success-foreground": [0.985, 0, 0],
|
||||
border: [1, 0, 0, 0.1],
|
||||
input: [1, 0, 0, 0.15],
|
||||
ring: [0.637, 0.237, 25.331],
|
||||
"chart-1": [0.808, 0.114, 19.571],
|
||||
"chart-2": [0.637, 0.237, 25.331],
|
||||
"chart-3": [0.577, 0.245, 27.325],
|
||||
"chart-4": [0.505, 0.213, 27.518],
|
||||
"chart-5": [0.444, 0.177, 26.899],
|
||||
sidebar: [0.21, 0.006, 285.885],
|
||||
"sidebar-foreground": [0.985, 0, 0],
|
||||
"sidebar-primary": [0.637, 0.237, 25.331],
|
||||
"sidebar-primary-foreground": [0.971, 0.013, 17.38],
|
||||
"sidebar-accent": [0.274, 0.006, 286.033],
|
||||
"sidebar-accent-foreground": [0.985, 0, 0],
|
||||
"sidebar-border": [1, 0, 0, 0.1],
|
||||
"sidebar-ring": [0.637, 0.237, 25.331],
|
||||
},
|
||||
} satisfies ThemeColors;
|
||||
78
packages/ui/shared/src/styles/themes/colors/rose.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { ThemeMode } from "../../../types";
|
||||
|
||||
import type { ThemeColors } from "../../../types";
|
||||
|
||||
export const rose = {
|
||||
[ThemeMode.LIGHT]: {
|
||||
background: [1, 0, 0],
|
||||
foreground: [0.141, 0.005, 285.823],
|
||||
card: [1, 0, 0],
|
||||
"card-foreground": [0.141, 0.005, 285.823],
|
||||
popover: [1, 0, 0],
|
||||
"popover-foreground": [0.141, 0.005, 285.823],
|
||||
primary: [0.645, 0.246, 16.439],
|
||||
"primary-foreground": [0.969, 0.015, 12.422],
|
||||
secondary: [0.967, 0.001, 286.375],
|
||||
"secondary-foreground": [0.21, 0.006, 285.885],
|
||||
muted: [0.967, 0.001, 286.375],
|
||||
"muted-foreground": [0.552, 0.016, 285.938],
|
||||
accent: [0.967, 0.001, 286.375],
|
||||
"accent-foreground": [0.21, 0.006, 285.885],
|
||||
destructive: [0.577, 0.245, 27.325],
|
||||
"destructive-foreground": [0.985, 0.002, 247.839],
|
||||
success: [0.7999, 0.1816, 151.78],
|
||||
"success-foreground": [0.9848, 0, 0],
|
||||
border: [0.92, 0.004, 286.32],
|
||||
input: [0.92, 0.004, 286.32],
|
||||
ring: [0.645, 0.246, 16.439],
|
||||
"chart-1": [0.81, 0.117, 11.638],
|
||||
"chart-2": [0.645, 0.246, 16.439],
|
||||
"chart-3": [0.586, 0.253, 17.585],
|
||||
"chart-4": [0.514, 0.222, 16.935],
|
||||
"chart-5": [0.455, 0.188, 13.697],
|
||||
sidebar: [0.985, 0, 0],
|
||||
"sidebar-foreground": [0.141, 0.005, 285.823],
|
||||
"sidebar-primary": [0.645, 0.246, 16.439],
|
||||
"sidebar-primary-foreground": [0.969, 0.015, 12.422],
|
||||
"sidebar-accent": [0.967, 0.001, 286.375],
|
||||
"sidebar-accent-foreground": [0.21, 0.006, 285.885],
|
||||
"sidebar-border": [0.92, 0.004, 286.32],
|
||||
"sidebar-ring": [0.645, 0.246, 16.439],
|
||||
},
|
||||
[ThemeMode.DARK]: {
|
||||
background: [0.141, 0.005, 285.823],
|
||||
foreground: [0.985, 0, 0],
|
||||
card: [0.21, 0.006, 285.885],
|
||||
"card-foreground": [0.985, 0, 0],
|
||||
popover: [0.21, 0.006, 285.885],
|
||||
"popover-foreground": [0.985, 0, 0],
|
||||
primary: [0.645, 0.246, 16.439],
|
||||
"primary-foreground": [0.969, 0.015, 12.422],
|
||||
secondary: [0.274, 0.006, 286.033],
|
||||
"secondary-foreground": [0.985, 0, 0],
|
||||
muted: [0.274, 0.006, 286.033],
|
||||
"muted-foreground": [0.705, 0.015, 286.067],
|
||||
accent: [0.274, 0.006, 286.033],
|
||||
"accent-foreground": [0.985, 0, 0],
|
||||
destructive: [0.704, 0.191, 22.216],
|
||||
"destructive-foreground": [0.985, 0.002, 247.839],
|
||||
success: [0.627, 0.194, 149.214],
|
||||
"success-foreground": [0.985, 0, 0],
|
||||
border: [1, 0, 0, 0.1],
|
||||
input: [1, 0, 0, 0.15],
|
||||
ring: [0.645, 0.246, 16.439],
|
||||
"chart-1": [0.81, 0.117, 11.638],
|
||||
"chart-2": [0.645, 0.246, 16.439],
|
||||
"chart-3": [0.586, 0.253, 17.585],
|
||||
"chart-4": [0.514, 0.222, 16.935],
|
||||
"chart-5": [0.455, 0.188, 13.697],
|
||||
sidebar: [0.21, 0.006, 285.885],
|
||||
"sidebar-foreground": [0.985, 0, 0],
|
||||
"sidebar-primary": [0.645, 0.246, 16.439],
|
||||
"sidebar-primary-foreground": [0.969, 0.015, 12.422],
|
||||
"sidebar-accent": [0.274, 0.006, 286.033],
|
||||
"sidebar-accent-foreground": [0.985, 0, 0],
|
||||
"sidebar-border": [1, 0, 0, 0.1],
|
||||
"sidebar-ring": [0.645, 0.246, 16.439],
|
||||
},
|
||||
} satisfies ThemeColors;
|
||||
78
packages/ui/shared/src/styles/themes/colors/stone.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { ThemeMode } from "../../../types";
|
||||
|
||||
import type { ThemeColors } from "../../../types";
|
||||
|
||||
export const stone = {
|
||||
[ThemeMode.LIGHT]: {
|
||||
background: [1, 0, 0],
|
||||
foreground: [0.147, 0.004, 49.25],
|
||||
card: [1, 0, 0],
|
||||
"card-foreground": [0.147, 0.004, 49.25],
|
||||
popover: [1, 0, 0],
|
||||
"popover-foreground": [0.147, 0.004, 49.25],
|
||||
primary: [0.216, 0.006, 56.043],
|
||||
"primary-foreground": [0.985, 0.001, 106.423],
|
||||
secondary: [0.97, 0.001, 106.424],
|
||||
"secondary-foreground": [0.216, 0.006, 56.043],
|
||||
muted: [0.97, 0.001, 106.424],
|
||||
"muted-foreground": [0.553, 0.013, 58.071],
|
||||
accent: [0.97, 0.001, 106.424],
|
||||
"accent-foreground": [0.216, 0.006, 56.043],
|
||||
destructive: [0.577, 0.245, 27.325],
|
||||
"destructive-foreground": [0.985, 0.002, 247.839],
|
||||
success: [0.7999, 0.1816, 151.78],
|
||||
"success-foreground": [0.9848, 0, 0],
|
||||
border: [0.923, 0.003, 48.717],
|
||||
input: [0.923, 0.003, 48.717],
|
||||
ring: [0.709, 0.01, 56.259],
|
||||
"chart-1": [0.869, 0.005, 56.366],
|
||||
"chart-2": [0.553, 0.013, 58.071],
|
||||
"chart-3": [0.444, 0.011, 73.639],
|
||||
"chart-4": [0.374, 0.01, 67.558],
|
||||
"chart-5": [0.268, 0.007, 34.298],
|
||||
sidebar: [0.985, 0.001, 106.423],
|
||||
"sidebar-foreground": [0.147, 0.004, 49.25],
|
||||
"sidebar-primary": [0.216, 0.006, 56.043],
|
||||
"sidebar-primary-foreground": [0.985, 0.001, 106.423],
|
||||
"sidebar-accent": [0.97, 0.001, 106.424],
|
||||
"sidebar-accent-foreground": [0.216, 0.006, 56.043],
|
||||
"sidebar-border": [0.923, 0.003, 48.717],
|
||||
"sidebar-ring": [0.709, 0.01, 56.259],
|
||||
},
|
||||
[ThemeMode.DARK]: {
|
||||
background: [0.147, 0.004, 49.25],
|
||||
foreground: [0.985, 0.001, 106.423],
|
||||
card: [0.216, 0.006, 56.043],
|
||||
"card-foreground": [0.985, 0.001, 106.423],
|
||||
popover: [0.216, 0.006, 56.043],
|
||||
"popover-foreground": [0.985, 0.001, 106.423],
|
||||
primary: [0.923, 0.003, 48.717],
|
||||
"primary-foreground": [0.216, 0.006, 56.043],
|
||||
secondary: [0.268, 0.007, 34.298],
|
||||
"secondary-foreground": [0.985, 0.001, 106.423],
|
||||
muted: [0.268, 0.007, 34.298],
|
||||
"muted-foreground": [0.709, 0.01, 56.259],
|
||||
accent: [0.268, 0.007, 34.298],
|
||||
"accent-foreground": [0.985, 0.001, 106.423],
|
||||
destructive: [0.704, 0.191, 22.216],
|
||||
"destructive-foreground": [0.985, 0.002, 247.839],
|
||||
success: [0.627, 0.194, 149.214],
|
||||
"success-foreground": [0.985, 0, 0],
|
||||
border: [1, 0, 0, 0.1],
|
||||
input: [1, 0, 0, 0.15],
|
||||
ring: [0.553, 0.013, 58.071],
|
||||
"chart-1": [0.869, 0.005, 56.366],
|
||||
"chart-2": [0.553, 0.013, 58.071],
|
||||
"chart-3": [0.444, 0.011, 73.639],
|
||||
"chart-4": [0.374, 0.01, 67.558],
|
||||
"chart-5": [0.268, 0.007, 34.298],
|
||||
sidebar: [0.216, 0.006, 56.043],
|
||||
"sidebar-foreground": [0.985, 0.001, 106.423],
|
||||
"sidebar-primary": [0.488, 0.243, 264.376],
|
||||
"sidebar-primary-foreground": [0.985, 0.001, 106.423],
|
||||
"sidebar-accent": [0.268, 0.007, 34.298],
|
||||
"sidebar-accent-foreground": [0.985, 0.001, 106.423],
|
||||
"sidebar-border": [1, 0, 0, 0.1],
|
||||
"sidebar-ring": [0.553, 0.013, 58.071],
|
||||
},
|
||||
} satisfies ThemeColors;
|
||||
78
packages/ui/shared/src/styles/themes/colors/violet.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { ThemeMode } from "../../../types";
|
||||
|
||||
import type { ThemeColors } from "../../../types";
|
||||
|
||||
export const violet = {
|
||||
[ThemeMode.LIGHT]: {
|
||||
background: [1, 0, 0],
|
||||
foreground: [0.141, 0.005, 285.823],
|
||||
card: [1, 0, 0],
|
||||
"card-foreground": [0.141, 0.005, 285.823],
|
||||
popover: [1, 0, 0],
|
||||
"popover-foreground": [0.141, 0.005, 285.823],
|
||||
primary: [0.606, 0.25, 292.717],
|
||||
"primary-foreground": [0.969, 0.016, 293.756],
|
||||
secondary: [0.967, 0.001, 286.375],
|
||||
"secondary-foreground": [0.21, 0.006, 285.885],
|
||||
muted: [0.967, 0.001, 286.375],
|
||||
"muted-foreground": [0.552, 0.016, 285.938],
|
||||
accent: [0.967, 0.001, 286.375],
|
||||
"accent-foreground": [0.21, 0.006, 285.885],
|
||||
destructive: [0.577, 0.245, 27.325],
|
||||
"destructive-foreground": [0.985, 0.002, 247.839],
|
||||
success: [0.7999, 0.1816, 151.78],
|
||||
"success-foreground": [0.9848, 0, 0],
|
||||
border: [0.92, 0.004, 286.32],
|
||||
input: [0.92, 0.004, 286.32],
|
||||
ring: [0.606, 0.25, 292.717],
|
||||
"chart-1": [0.811, 0.111, 293.571],
|
||||
"chart-2": [0.606, 0.25, 292.717],
|
||||
"chart-3": [0.541, 0.281, 293.009],
|
||||
"chart-4": [0.491, 0.27, 292.581],
|
||||
"chart-5": [0.432, 0.232, 292.759],
|
||||
sidebar: [0.985, 0, 0],
|
||||
"sidebar-foreground": [0.141, 0.005, 285.823],
|
||||
"sidebar-primary": [0.606, 0.25, 292.717],
|
||||
"sidebar-primary-foreground": [0.969, 0.016, 293.756],
|
||||
"sidebar-accent": [0.967, 0.001, 286.375],
|
||||
"sidebar-accent-foreground": [0.21, 0.006, 285.885],
|
||||
"sidebar-border": [0.92, 0.004, 286.32],
|
||||
"sidebar-ring": [0.606, 0.25, 292.717],
|
||||
},
|
||||
[ThemeMode.DARK]: {
|
||||
background: [0.141, 0.005, 285.823],
|
||||
foreground: [0.985, 0, 0],
|
||||
card: [0.21, 0.006, 285.885],
|
||||
"card-foreground": [0.985, 0, 0],
|
||||
popover: [0.21, 0.006, 285.885],
|
||||
"popover-foreground": [0.985, 0, 0],
|
||||
primary: [0.541, 0.281, 293.009],
|
||||
"primary-foreground": [0.969, 0.016, 293.756],
|
||||
secondary: [0.274, 0.006, 286.033],
|
||||
"secondary-foreground": [0.985, 0, 0],
|
||||
muted: [0.274, 0.006, 286.033],
|
||||
"muted-foreground": [0.705, 0.015, 286.067],
|
||||
accent: [0.274, 0.006, 286.033],
|
||||
"accent-foreground": [0.985, 0, 0],
|
||||
destructive: [0.704, 0.191, 22.216],
|
||||
"destructive-foreground": [0.985, 0.002, 247.839],
|
||||
success: [0.627, 0.194, 149.214],
|
||||
"success-foreground": [0.985, 0, 0],
|
||||
border: [1, 0, 0, 0.1],
|
||||
input: [1, 0, 0, 0.15],
|
||||
ring: [0.541, 0.281, 293.009],
|
||||
"chart-1": [0.811, 0.111, 293.571],
|
||||
"chart-2": [0.606, 0.25, 292.717],
|
||||
"chart-3": [0.541, 0.281, 293.009],
|
||||
"chart-4": [0.491, 0.27, 292.581],
|
||||
"chart-5": [0.432, 0.232, 292.759],
|
||||
sidebar: [0.21, 0.006, 285.885],
|
||||
"sidebar-foreground": [0.985, 0, 0],
|
||||
"sidebar-primary": [0.541, 0.281, 293.009],
|
||||
"sidebar-primary-foreground": [0.969, 0.016, 293.756],
|
||||
"sidebar-accent": [0.274, 0.006, 286.033],
|
||||
"sidebar-accent-foreground": [0.985, 0, 0],
|
||||
"sidebar-border": [1, 0, 0, 0.1],
|
||||
"sidebar-ring": [0.541, 0.281, 293.009],
|
||||
},
|
||||
} satisfies ThemeColors;
|
||||
78
packages/ui/shared/src/styles/themes/colors/yellow.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { ThemeMode } from "../../../types";
|
||||
|
||||
import type { ThemeColors } from "../../../types";
|
||||
|
||||
export const yellow = {
|
||||
[ThemeMode.LIGHT]: {
|
||||
background: [1, 0, 0],
|
||||
foreground: [0.141, 0.005, 285.823],
|
||||
card: [1, 0, 0],
|
||||
"card-foreground": [0.141, 0.005, 285.823],
|
||||
popover: [1, 0, 0],
|
||||
"popover-foreground": [0.141, 0.005, 285.823],
|
||||
primary: [0.795, 0.184, 86.047],
|
||||
"primary-foreground": [0.421, 0.095, 57.708],
|
||||
secondary: [0.967, 0.001, 286.375],
|
||||
"secondary-foreground": [0.21, 0.006, 285.885],
|
||||
muted: [0.967, 0.001, 286.375],
|
||||
"muted-foreground": [0.552, 0.016, 285.938],
|
||||
accent: [0.967, 0.001, 286.375],
|
||||
"accent-foreground": [0.21, 0.006, 285.885],
|
||||
destructive: [0.577, 0.245, 27.325],
|
||||
"destructive-foreground": [0.985, 0.002, 247.839],
|
||||
success: [0.7999, 0.1816, 151.78],
|
||||
"success-foreground": [0.9848, 0, 0],
|
||||
border: [0.92, 0.004, 286.32],
|
||||
input: [0.92, 0.004, 286.32],
|
||||
ring: [0.795, 0.184, 86.047],
|
||||
"chart-1": [0.905, 0.182, 98.111],
|
||||
"chart-2": [0.795, 0.184, 86.047],
|
||||
"chart-3": [0.681, 0.162, 75.834],
|
||||
"chart-4": [0.554, 0.135, 66.442],
|
||||
"chart-5": [0.476, 0.114, 61.907],
|
||||
sidebar: [0.985, 0, 0],
|
||||
"sidebar-foreground": [0.141, 0.005, 285.823],
|
||||
"sidebar-primary": [0.795, 0.184, 86.047],
|
||||
"sidebar-primary-foreground": [0.421, 0.095, 57.708],
|
||||
"sidebar-accent": [0.967, 0.001, 286.375],
|
||||
"sidebar-accent-foreground": [0.21, 0.006, 285.885],
|
||||
"sidebar-border": [0.92, 0.004, 286.32],
|
||||
"sidebar-ring": [0.795, 0.184, 86.047],
|
||||
},
|
||||
[ThemeMode.DARK]: {
|
||||
background: [0.141, 0.005, 285.823],
|
||||
foreground: [0.985, 0, 0],
|
||||
card: [0.21, 0.006, 285.885],
|
||||
"card-foreground": [0.985, 0, 0],
|
||||
popover: [0.21, 0.006, 285.885],
|
||||
"popover-foreground": [0.985, 0, 0],
|
||||
primary: [0.795, 0.184, 86.047],
|
||||
"primary-foreground": [0.421, 0.095, 57.708],
|
||||
secondary: [0.274, 0.006, 286.033],
|
||||
"secondary-foreground": [0.985, 0, 0],
|
||||
muted: [0.274, 0.006, 286.033],
|
||||
"muted-foreground": [0.705, 0.015, 286.067],
|
||||
accent: [0.274, 0.006, 286.033],
|
||||
"accent-foreground": [0.985, 0, 0],
|
||||
destructive: [0.704, 0.191, 22.216],
|
||||
"destructive-foreground": [0.985, 0.002, 247.839],
|
||||
success: [0.627, 0.194, 149.214],
|
||||
"success-foreground": [0.985, 0, 0],
|
||||
border: [1, 0, 0, 0.1],
|
||||
input: [1, 0, 0, 0.15],
|
||||
ring: [0.554, 0.135, 66.442],
|
||||
"chart-1": [0.905, 0.182, 98.111],
|
||||
"chart-2": [0.795, 0.184, 86.047],
|
||||
"chart-3": [0.681, 0.162, 75.834],
|
||||
"chart-4": [0.554, 0.135, 66.442],
|
||||
"chart-5": [0.476, 0.114, 61.907],
|
||||
sidebar: [0.21, 0.006, 285.885],
|
||||
"sidebar-foreground": [0.985, 0, 0],
|
||||
"sidebar-primary": [0.795, 0.184, 86.047],
|
||||
"sidebar-primary-foreground": [0.421, 0.095, 57.708],
|
||||
"sidebar-accent": [0.274, 0.006, 286.033],
|
||||
"sidebar-accent-foreground": [0.985, 0, 0],
|
||||
"sidebar-border": [1, 0, 0, 0.1],
|
||||
"sidebar-ring": [0.554, 0.135, 66.442],
|
||||
},
|
||||
} satisfies ThemeColors;
|
||||
23
packages/ui/shared/src/styles/themes/themes.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { blue } from "./colors/blue";
|
||||
import { gray } from "./colors/gray";
|
||||
import { green } from "./colors/green";
|
||||
import { orange } from "./colors/orange";
|
||||
import { red } from "./colors/red";
|
||||
import { rose } from "./colors/rose";
|
||||
import { stone } from "./colors/stone";
|
||||
import { violet } from "./colors/violet";
|
||||
import { yellow } from "./colors/yellow";
|
||||
|
||||
import type { ThemeColor, ThemeColors } from "../../types";
|
||||
|
||||
export const themes: Record<ThemeColor, ThemeColors> = {
|
||||
orange,
|
||||
blue,
|
||||
gray,
|
||||
green,
|
||||
red,
|
||||
rose,
|
||||
stone,
|
||||
violet,
|
||||
yellow,
|
||||
};
|
||||
751
packages/ui/shared/src/styles/themes/variables.css
Normal file
@@ -0,0 +1,751 @@
|
||||
/*
|
||||
* This file is auto-generated. Do NOT edit it directly.
|
||||
*
|
||||
* Edit the theme source files instead:
|
||||
* - ./themes.ts
|
||||
* - ./colors/*.ts
|
||||
*/
|
||||
|
||||
@layer theme {
|
||||
:root {
|
||||
@variant light {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.6387 0.2151 36.46);
|
||||
--primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.985 0.002 247.839);
|
||||
--success: oklch(0.7999 0.1816 151.78);
|
||||
--success-foreground: oklch(0.9848 0 0);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.6387 0.2151 36.46);
|
||||
--chart-1: oklch(0.6387 0.2151 36.46);
|
||||
--chart-2: oklch(0.7027 0.1783 44.12);
|
||||
--chart-3: oklch(0.7732 0.1275 51.73);
|
||||
--chart-4: oklch(0.8478 0.0818 58.78);
|
||||
--chart-5: oklch(0.92 0.0421 65.38);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.6387 0.2151 36.46);
|
||||
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.6387 0.2151 36.46);
|
||||
}
|
||||
|
||||
@variant dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.6387 0.2151 36.46);
|
||||
--primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--destructive-foreground: oklch(0.985 0.002 247.839);
|
||||
--success: oklch(0.627 0.194 149.214);
|
||||
--success-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.6387 0.2151 36.46);
|
||||
--chart-1: oklch(0.6387 0.2151 36.46);
|
||||
--chart-2: oklch(0.7027 0.1783 44.12);
|
||||
--chart-3: oklch(0.7732 0.1275 51.73);
|
||||
--chart-4: oklch(0.8478 0.0818 58.78);
|
||||
--chart-5: oklch(0.92 0.0421 65.38);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.6387 0.2151 36.46);
|
||||
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.6387 0.2151 36.46);
|
||||
}
|
||||
|
||||
[data-theme="orange"] {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.6387 0.2151 36.46);
|
||||
--primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.985 0.002 247.839);
|
||||
--success: oklch(0.7999 0.1816 151.78);
|
||||
--success-foreground: oklch(0.9848 0 0);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.6387 0.2151 36.46);
|
||||
--chart-1: oklch(0.6387 0.2151 36.46);
|
||||
--chart-2: oklch(0.7027 0.1783 44.12);
|
||||
--chart-3: oklch(0.7732 0.1275 51.73);
|
||||
--chart-4: oklch(0.8478 0.0818 58.78);
|
||||
--chart-5: oklch(0.92 0.0421 65.38);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.6387 0.2151 36.46);
|
||||
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.6387 0.2151 36.46);
|
||||
|
||||
@variant dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.6387 0.2151 36.46);
|
||||
--primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--destructive-foreground: oklch(0.985 0.002 247.839);
|
||||
--success: oklch(0.627 0.194 149.214);
|
||||
--success-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.6387 0.2151 36.46);
|
||||
--chart-1: oklch(0.6387 0.2151 36.46);
|
||||
--chart-2: oklch(0.7027 0.1783 44.12);
|
||||
--chart-3: oklch(0.7732 0.1275 51.73);
|
||||
--chart-4: oklch(0.8478 0.0818 58.78);
|
||||
--chart-5: oklch(0.92 0.0421 65.38);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.6387 0.2151 36.46);
|
||||
--sidebar-primary-foreground: oklch(0.98 0.016 73.684);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.6387 0.2151 36.46);
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="blue"] {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.623 0.214 259.815);
|
||||
--primary-foreground: oklch(0.97 0.014 254.604);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.985 0.002 247.839);
|
||||
--success: oklch(0.7999 0.1816 151.78);
|
||||
--success-foreground: oklch(0.9848 0 0);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.623 0.214 259.815);
|
||||
--chart-1: oklch(0.809 0.105 251.813);
|
||||
--chart-2: oklch(0.623 0.214 259.815);
|
||||
--chart-3: oklch(0.546 0.245 262.881);
|
||||
--chart-4: oklch(0.488 0.243 264.376);
|
||||
--chart-5: oklch(0.424 0.199 265.638);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.623 0.214 259.815);
|
||||
--sidebar-primary-foreground: oklch(0.97 0.014 254.604);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.623 0.214 259.815);
|
||||
|
||||
@variant dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.546 0.245 262.881);
|
||||
--primary-foreground: oklch(0.379 0.146 265.522);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--destructive-foreground: oklch(0.985 0.002 247.839);
|
||||
--success: oklch(0.627 0.194 149.214);
|
||||
--success-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.488 0.243 264.376);
|
||||
--chart-1: oklch(0.809 0.105 251.813);
|
||||
--chart-2: oklch(0.623 0.214 259.815);
|
||||
--chart-3: oklch(0.546 0.245 262.881);
|
||||
--chart-4: oklch(0.488 0.243 264.376);
|
||||
--chart-5: oklch(0.424 0.199 265.638);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.546 0.245 262.881);
|
||||
--sidebar-primary-foreground: oklch(0.379 0.146 265.522);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.488 0.243 264.376);
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="gray"] {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.13 0.028 261.692);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.13 0.028 261.692);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.13 0.028 261.692);
|
||||
--primary: oklch(0.21 0.034 264.665);
|
||||
--primary-foreground: oklch(0.985 0.002 247.839);
|
||||
--secondary: oklch(0.967 0.003 264.542);
|
||||
--secondary-foreground: oklch(0.21 0.034 264.665);
|
||||
--muted: oklch(0.967 0.003 264.542);
|
||||
--muted-foreground: oklch(0.551 0.027 264.364);
|
||||
--accent: oklch(0.967 0.003 264.542);
|
||||
--accent-foreground: oklch(0.21 0.034 264.665);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.985 0.002 247.839);
|
||||
--success: oklch(0.7999 0.1816 151.78);
|
||||
--success-foreground: oklch(0.9848 0 0);
|
||||
--border: oklch(0.928 0.006 264.531);
|
||||
--input: oklch(0.928 0.006 264.531);
|
||||
--ring: oklch(0.707 0.022 261.325);
|
||||
--chart-1: oklch(0.872 0.01 258.338);
|
||||
--chart-2: oklch(0.551 0.027 264.364);
|
||||
--chart-3: oklch(0.446 0.03 256.802);
|
||||
--chart-4: oklch(0.373 0.034 259.733);
|
||||
--chart-5: oklch(0.278 0.033 256.848);
|
||||
--sidebar: oklch(0.985 0.002 247.839);
|
||||
--sidebar-foreground: oklch(0.13 0.028 261.692);
|
||||
--sidebar-primary: oklch(0.21 0.034 264.665);
|
||||
--sidebar-primary-foreground: oklch(0.985 0.002 247.839);
|
||||
--sidebar-accent: oklch(0.967 0.003 264.542);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.034 264.665);
|
||||
--sidebar-border: oklch(0.928 0.006 264.531);
|
||||
--sidebar-ring: oklch(0.707 0.022 261.325);
|
||||
|
||||
@variant dark {
|
||||
--background: oklch(0.13 0.028 261.692);
|
||||
--foreground: oklch(0.985 0.002 247.839);
|
||||
--card: oklch(0.21 0.034 264.665);
|
||||
--card-foreground: oklch(0.985 0.002 247.839);
|
||||
--popover: oklch(0.21 0.034 264.665);
|
||||
--popover-foreground: oklch(0.985 0.002 247.839);
|
||||
--primary: oklch(0.928 0.006 264.531);
|
||||
--primary-foreground: oklch(0.21 0.034 264.665);
|
||||
--secondary: oklch(0.278 0.033 256.848);
|
||||
--secondary-foreground: oklch(0.985 0.002 247.839);
|
||||
--muted: oklch(0.278 0.033 256.848);
|
||||
--muted-foreground: oklch(0.707 0.022 261.325);
|
||||
--accent: oklch(0.278 0.033 256.848);
|
||||
--accent-foreground: oklch(0.985 0.002 247.839);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--destructive-foreground: oklch(0.985 0.002 247.839);
|
||||
--success: oklch(0.627 0.194 149.214);
|
||||
--success-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.551 0.027 264.364);
|
||||
--chart-1: oklch(0.872 0.01 258.338);
|
||||
--chart-2: oklch(0.551 0.027 264.364);
|
||||
--chart-3: oklch(0.446 0.03 256.802);
|
||||
--chart-4: oklch(0.373 0.034 259.733);
|
||||
--chart-5: oklch(0.278 0.033 256.848);
|
||||
--sidebar: oklch(0.21 0.034 264.665);
|
||||
--sidebar-foreground: oklch(0.985 0.002 247.839);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0.002 247.839);
|
||||
--sidebar-accent: oklch(0.278 0.033 256.848);
|
||||
--sidebar-accent-foreground: oklch(0.985 0.002 247.839);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.551 0.027 264.364);
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="green"] {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.723 0.219 149.579);
|
||||
--primary-foreground: oklch(0.982 0.018 155.826);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.985 0.002 247.839);
|
||||
--success: oklch(0.7999 0.1816 151.78);
|
||||
--success-foreground: oklch(0.9848 0 0);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.723 0.219 149.579);
|
||||
--chart-1: oklch(0.871 0.15 154.449);
|
||||
--chart-2: oklch(0.723 0.219 149.579);
|
||||
--chart-3: oklch(0.627 0.194 149.214);
|
||||
--chart-4: oklch(0.527 0.154 150.069);
|
||||
--chart-5: oklch(0.448 0.119 151.328);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.723 0.219 149.579);
|
||||
--sidebar-primary-foreground: oklch(0.982 0.018 155.826);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.723 0.219 149.579);
|
||||
|
||||
@variant dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.696 0.17 162.48);
|
||||
--primary-foreground: oklch(0.393 0.095 152.535);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--destructive-foreground: oklch(0.985 0.002 247.839);
|
||||
--success: oklch(0.627 0.194 149.214);
|
||||
--success-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.527 0.154 150.069);
|
||||
--chart-1: oklch(0.871 0.15 154.449);
|
||||
--chart-2: oklch(0.723 0.219 149.579);
|
||||
--chart-3: oklch(0.627 0.194 149.214);
|
||||
--chart-4: oklch(0.527 0.154 150.069);
|
||||
--chart-5: oklch(0.448 0.119 151.328);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.696 0.17 162.48);
|
||||
--sidebar-primary-foreground: oklch(0.393 0.095 152.535);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.527 0.154 150.069);
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="red"] {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.637 0.237 25.331);
|
||||
--primary-foreground: oklch(0.971 0.013 17.38);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.985 0.002 247.839);
|
||||
--success: oklch(0.7999 0.1816 151.78);
|
||||
--success-foreground: oklch(0.9848 0 0);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.637 0.237 25.331);
|
||||
--chart-1: oklch(0.808 0.114 19.571);
|
||||
--chart-2: oklch(0.637 0.237 25.331);
|
||||
--chart-3: oklch(0.577 0.245 27.325);
|
||||
--chart-4: oklch(0.505 0.213 27.518);
|
||||
--chart-5: oklch(0.444 0.177 26.899);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.637 0.237 25.331);
|
||||
--sidebar-primary-foreground: oklch(0.971 0.013 17.38);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.637 0.237 25.331);
|
||||
|
||||
@variant dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.637 0.237 25.331);
|
||||
--primary-foreground: oklch(0.971 0.013 17.38);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--destructive-foreground: oklch(0.985 0.002 247.839);
|
||||
--success: oklch(0.627 0.194 149.214);
|
||||
--success-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.637 0.237 25.331);
|
||||
--chart-1: oklch(0.808 0.114 19.571);
|
||||
--chart-2: oklch(0.637 0.237 25.331);
|
||||
--chart-3: oklch(0.577 0.245 27.325);
|
||||
--chart-4: oklch(0.505 0.213 27.518);
|
||||
--chart-5: oklch(0.444 0.177 26.899);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.637 0.237 25.331);
|
||||
--sidebar-primary-foreground: oklch(0.971 0.013 17.38);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.637 0.237 25.331);
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="rose"] {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.645 0.246 16.439);
|
||||
--primary-foreground: oklch(0.969 0.015 12.422);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.985 0.002 247.839);
|
||||
--success: oklch(0.7999 0.1816 151.78);
|
||||
--success-foreground: oklch(0.9848 0 0);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.645 0.246 16.439);
|
||||
--chart-1: oklch(0.81 0.117 11.638);
|
||||
--chart-2: oklch(0.645 0.246 16.439);
|
||||
--chart-3: oklch(0.586 0.253 17.585);
|
||||
--chart-4: oklch(0.514 0.222 16.935);
|
||||
--chart-5: oklch(0.455 0.188 13.697);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.645 0.246 16.439);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.015 12.422);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.645 0.246 16.439);
|
||||
|
||||
@variant dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.645 0.246 16.439);
|
||||
--primary-foreground: oklch(0.969 0.015 12.422);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--destructive-foreground: oklch(0.985 0.002 247.839);
|
||||
--success: oklch(0.627 0.194 149.214);
|
||||
--success-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.645 0.246 16.439);
|
||||
--chart-1: oklch(0.81 0.117 11.638);
|
||||
--chart-2: oklch(0.645 0.246 16.439);
|
||||
--chart-3: oklch(0.586 0.253 17.585);
|
||||
--chart-4: oklch(0.514 0.222 16.935);
|
||||
--chart-5: oklch(0.455 0.188 13.697);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.645 0.246 16.439);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.015 12.422);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.645 0.246 16.439);
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="stone"] {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.147 0.004 49.25);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.147 0.004 49.25);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.147 0.004 49.25);
|
||||
--primary: oklch(0.216 0.006 56.043);
|
||||
--primary-foreground: oklch(0.985 0.001 106.423);
|
||||
--secondary: oklch(0.97 0.001 106.424);
|
||||
--secondary-foreground: oklch(0.216 0.006 56.043);
|
||||
--muted: oklch(0.97 0.001 106.424);
|
||||
--muted-foreground: oklch(0.553 0.013 58.071);
|
||||
--accent: oklch(0.97 0.001 106.424);
|
||||
--accent-foreground: oklch(0.216 0.006 56.043);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.985 0.002 247.839);
|
||||
--success: oklch(0.7999 0.1816 151.78);
|
||||
--success-foreground: oklch(0.9848 0 0);
|
||||
--border: oklch(0.923 0.003 48.717);
|
||||
--input: oklch(0.923 0.003 48.717);
|
||||
--ring: oklch(0.709 0.01 56.259);
|
||||
--chart-1: oklch(0.869 0.005 56.366);
|
||||
--chart-2: oklch(0.553 0.013 58.071);
|
||||
--chart-3: oklch(0.444 0.011 73.639);
|
||||
--chart-4: oklch(0.374 0.01 67.558);
|
||||
--chart-5: oklch(0.268 0.007 34.298);
|
||||
--sidebar: oklch(0.985 0.001 106.423);
|
||||
--sidebar-foreground: oklch(0.147 0.004 49.25);
|
||||
--sidebar-primary: oklch(0.216 0.006 56.043);
|
||||
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
|
||||
--sidebar-accent: oklch(0.97 0.001 106.424);
|
||||
--sidebar-accent-foreground: oklch(0.216 0.006 56.043);
|
||||
--sidebar-border: oklch(0.923 0.003 48.717);
|
||||
--sidebar-ring: oklch(0.709 0.01 56.259);
|
||||
|
||||
@variant dark {
|
||||
--background: oklch(0.147 0.004 49.25);
|
||||
--foreground: oklch(0.985 0.001 106.423);
|
||||
--card: oklch(0.216 0.006 56.043);
|
||||
--card-foreground: oklch(0.985 0.001 106.423);
|
||||
--popover: oklch(0.216 0.006 56.043);
|
||||
--popover-foreground: oklch(0.985 0.001 106.423);
|
||||
--primary: oklch(0.923 0.003 48.717);
|
||||
--primary-foreground: oklch(0.216 0.006 56.043);
|
||||
--secondary: oklch(0.268 0.007 34.298);
|
||||
--secondary-foreground: oklch(0.985 0.001 106.423);
|
||||
--muted: oklch(0.268 0.007 34.298);
|
||||
--muted-foreground: oklch(0.709 0.01 56.259);
|
||||
--accent: oklch(0.268 0.007 34.298);
|
||||
--accent-foreground: oklch(0.985 0.001 106.423);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--destructive-foreground: oklch(0.985 0.002 247.839);
|
||||
--success: oklch(0.627 0.194 149.214);
|
||||
--success-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.553 0.013 58.071);
|
||||
--chart-1: oklch(0.869 0.005 56.366);
|
||||
--chart-2: oklch(0.553 0.013 58.071);
|
||||
--chart-3: oklch(0.444 0.011 73.639);
|
||||
--chart-4: oklch(0.374 0.01 67.558);
|
||||
--chart-5: oklch(0.268 0.007 34.298);
|
||||
--sidebar: oklch(0.216 0.006 56.043);
|
||||
--sidebar-foreground: oklch(0.985 0.001 106.423);
|
||||
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
|
||||
--sidebar-accent: oklch(0.268 0.007 34.298);
|
||||
--sidebar-accent-foreground: oklch(0.985 0.001 106.423);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.553 0.013 58.071);
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="violet"] {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.606 0.25 292.717);
|
||||
--primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.985 0.002 247.839);
|
||||
--success: oklch(0.7999 0.1816 151.78);
|
||||
--success-foreground: oklch(0.9848 0 0);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.606 0.25 292.717);
|
||||
--chart-1: oklch(0.811 0.111 293.571);
|
||||
--chart-2: oklch(0.606 0.25 292.717);
|
||||
--chart-3: oklch(0.541 0.281 293.009);
|
||||
--chart-4: oklch(0.491 0.27 292.581);
|
||||
--chart-5: oklch(0.432 0.232 292.759);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.606 0.25 292.717);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.606 0.25 292.717);
|
||||
|
||||
@variant dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.541 0.281 293.009);
|
||||
--primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--destructive-foreground: oklch(0.985 0.002 247.839);
|
||||
--success: oklch(0.627 0.194 149.214);
|
||||
--success-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.541 0.281 293.009);
|
||||
--chart-1: oklch(0.811 0.111 293.571);
|
||||
--chart-2: oklch(0.606 0.25 292.717);
|
||||
--chart-3: oklch(0.541 0.281 293.009);
|
||||
--chart-4: oklch(0.491 0.27 292.581);
|
||||
--chart-5: oklch(0.432 0.232 292.759);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.541 0.281 293.009);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.541 0.281 293.009);
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="yellow"] {
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.141 0.005 285.823);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.141 0.005 285.823);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.141 0.005 285.823);
|
||||
--primary: oklch(0.795 0.184 86.047);
|
||||
--primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--secondary: oklch(0.967 0.001 286.375);
|
||||
--secondary-foreground: oklch(0.21 0.006 285.885);
|
||||
--muted: oklch(0.967 0.001 286.375);
|
||||
--muted-foreground: oklch(0.552 0.016 285.938);
|
||||
--accent: oklch(0.967 0.001 286.375);
|
||||
--accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.985 0.002 247.839);
|
||||
--success: oklch(0.7999 0.1816 151.78);
|
||||
--success-foreground: oklch(0.9848 0 0);
|
||||
--border: oklch(0.92 0.004 286.32);
|
||||
--input: oklch(0.92 0.004 286.32);
|
||||
--ring: oklch(0.795 0.184 86.047);
|
||||
--chart-1: oklch(0.905 0.182 98.111);
|
||||
--chart-2: oklch(0.795 0.184 86.047);
|
||||
--chart-3: oklch(0.681 0.162 75.834);
|
||||
--chart-4: oklch(0.554 0.135 66.442);
|
||||
--chart-5: oklch(0.476 0.114 61.907);
|
||||
--sidebar: oklch(0.985 0 0);
|
||||
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
||||
--sidebar-primary: oklch(0.795 0.184 86.047);
|
||||
--sidebar-primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--sidebar-accent: oklch(0.967 0.001 286.375);
|
||||
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
||||
--sidebar-border: oklch(0.92 0.004 286.32);
|
||||
--sidebar-ring: oklch(0.795 0.184 86.047);
|
||||
|
||||
@variant dark {
|
||||
--background: oklch(0.141 0.005 285.823);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.21 0.006 285.885);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.21 0.006 285.885);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.795 0.184 86.047);
|
||||
--primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--destructive-foreground: oklch(0.985 0.002 247.839);
|
||||
--success: oklch(0.627 0.194 149.214);
|
||||
--success-foreground: oklch(0.985 0 0);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.554 0.135 66.442);
|
||||
--chart-1: oklch(0.905 0.182 98.111);
|
||||
--chart-2: oklch(0.795 0.184 86.047);
|
||||
--chart-3: oklch(0.681 0.162 75.834);
|
||||
--chart-4: oklch(0.554 0.135 66.442);
|
||||
--chart-5: oklch(0.476 0.114 61.907);
|
||||
--sidebar: oklch(0.21 0.006 285.885);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.795 0.184 86.047);
|
||||
--sidebar-primary-foreground: oklch(0.421 0.095 57.708);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.554 0.135 66.442);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
80
packages/ui/shared/src/types/index.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
export const ThemeMode = {
|
||||
SYSTEM: "system",
|
||||
LIGHT: "light",
|
||||
DARK: "dark",
|
||||
} as const;
|
||||
|
||||
export const ThemeColor = {
|
||||
ORANGE: "orange",
|
||||
ROSE: "rose",
|
||||
RED: "red",
|
||||
YELLOW: "yellow",
|
||||
GRAY: "gray",
|
||||
STONE: "stone",
|
||||
GREEN: "green",
|
||||
BLUE: "blue",
|
||||
VIOLET: "violet",
|
||||
} as const;
|
||||
|
||||
export const DEFAULT_BREAKPOINTS = {
|
||||
sm: 640,
|
||||
md: 768,
|
||||
lg: 1024,
|
||||
xl: 1280,
|
||||
"2xl": 1536,
|
||||
} as const;
|
||||
|
||||
export type ThemeColor = (typeof ThemeColor)[keyof typeof ThemeColor];
|
||||
export type ThemeMode = (typeof ThemeMode)[keyof typeof ThemeMode];
|
||||
|
||||
// OKLCH color variable (with optional opacity)
|
||||
export type ColorVariable =
|
||||
| readonly [number, number, number]
|
||||
| readonly [number, number, number, number];
|
||||
|
||||
export interface ThemeConfig {
|
||||
mode: ThemeMode;
|
||||
color: ThemeColor;
|
||||
}
|
||||
|
||||
export interface ThemeColorsVariables {
|
||||
background: ColorVariable;
|
||||
foreground: ColorVariable;
|
||||
card: ColorVariable;
|
||||
"card-foreground": ColorVariable;
|
||||
popover: ColorVariable;
|
||||
"popover-foreground": ColorVariable;
|
||||
primary: ColorVariable;
|
||||
"primary-foreground": ColorVariable;
|
||||
secondary: ColorVariable;
|
||||
"secondary-foreground": ColorVariable;
|
||||
muted: ColorVariable;
|
||||
"muted-foreground": ColorVariable;
|
||||
accent: ColorVariable;
|
||||
"accent-foreground": ColorVariable;
|
||||
success: ColorVariable;
|
||||
"success-foreground": ColorVariable;
|
||||
destructive: ColorVariable;
|
||||
"destructive-foreground": ColorVariable;
|
||||
border: ColorVariable;
|
||||
input: ColorVariable;
|
||||
ring: ColorVariable;
|
||||
"chart-1": ColorVariable;
|
||||
"chart-2": ColorVariable;
|
||||
"chart-3": ColorVariable;
|
||||
"chart-4": ColorVariable;
|
||||
"chart-5": ColorVariable;
|
||||
sidebar: ColorVariable;
|
||||
"sidebar-foreground": ColorVariable;
|
||||
"sidebar-primary": ColorVariable;
|
||||
"sidebar-primary-foreground": ColorVariable;
|
||||
"sidebar-accent": ColorVariable;
|
||||
"sidebar-accent-foreground": ColorVariable;
|
||||
"sidebar-border": ColorVariable;
|
||||
"sidebar-ring": ColorVariable;
|
||||
}
|
||||
|
||||
export interface ThemeColors {
|
||||
[ThemeMode.LIGHT]: ThemeColorsVariables;
|
||||
[ThemeMode.DARK]: ThemeColorsVariables;
|
||||
}
|
||||
5
packages/ui/shared/src/typings/svg.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare module "*.svg" {
|
||||
import type { FC, SVGProps } from "react";
|
||||
const content: FC<SVGProps<SVGElement>>;
|
||||
export default content;
|
||||
}
|
||||
6
packages/ui/shared/src/utils/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { cx } from "class-variance-authority";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
const cn = (...inputs: Parameters<typeof cx>) => twMerge(cx(inputs));
|
||||
|
||||
export { cn };
|
||||
9
packages/ui/shared/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "@turbostarter/tsconfig/internal.json",
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "ES2022"],
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": ["*.ts", "src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
9
packages/ui/shared/turbo.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "https://turborepo.org/schema.json",
|
||||
"extends": ["//"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"outputs": ["./src/styles/themes/variables.css"]
|
||||
}
|
||||
}
|
||||
}
|
||||
19
packages/ui/web/components.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "",
|
||||
"css": "unused.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true
|
||||
},
|
||||
"aliases": {
|
||||
"utils": "@turbostarter/ui",
|
||||
"components": "#components",
|
||||
"ui": "#components",
|
||||
"lib": "#lib",
|
||||
"hooks": "#hooks"
|
||||
}
|
||||
}
|
||||
4
packages/ui/web/eslint.config.js
Normal file
@@ -0,0 +1,4 @@
|
||||
import baseConfig from "@turbostarter/eslint-config/base";
|
||||
import reactConfig from "@turbostarter/eslint-config/react";
|
||||
|
||||
export default [...baseConfig, ...reactConfig];
|
||||
51
packages/ui/web/package.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@turbostarter/ui-web",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./globals.css": "./src/styles/globals.css",
|
||||
"./*": "./src/components/*.tsx"
|
||||
},
|
||||
"imports": {
|
||||
"#*": "./src/*.tsx"
|
||||
},
|
||||
"scripts": {
|
||||
"clean": "git clean -xdf .cache .turbo dist node_modules",
|
||||
"format": "prettier --check . --ignore-path ../../../.gitignore",
|
||||
"lint": "eslint",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"ui:add": "pnpm dlx shadcn@latest add && prettier src --write --list-different"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tanstack/react-table": "catalog:",
|
||||
"@turbostarter/i18n": "workspace:*",
|
||||
"@turbostarter/shared": "workspace:*",
|
||||
"@turbostarter/ui": "workspace:*",
|
||||
"cmdk": "1.1.1",
|
||||
"input-otp": "1.4.2",
|
||||
"lucide-react": "0.552.0",
|
||||
"radix-ui": "1.4.3",
|
||||
"react-day-picker": "9.11.1",
|
||||
"react-resizable-panels": "4.1.1",
|
||||
"react-textarea-autosize": "8.5.9",
|
||||
"recharts": "2.15.4",
|
||||
"vaul": "1.1.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/typography": "0.5.19",
|
||||
"@turbostarter/eslint-config": "workspace:*",
|
||||
"@turbostarter/prettier-config": "workspace:*",
|
||||
"@turbostarter/tsconfig": "workspace:*",
|
||||
"eslint": "catalog:",
|
||||
"prettier": "catalog:",
|
||||
"react": "catalog:react19",
|
||||
"react-hook-form": "catalog:",
|
||||
"tailwindcss": "4.1.16",
|
||||
"tw-animate-css": "1.4.0",
|
||||
"typescript": "catalog:",
|
||||
"zod": "catalog:"
|
||||
},
|
||||
"prettier": "@turbostarter/prettier-config"
|
||||
}
|
||||
66
packages/ui/web/src/components/accordion.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { Accordion as AccordionPrimitive } from "radix-ui";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
import { Icons } from "@turbostarter/ui-web/icons";
|
||||
|
||||
function Accordion({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Root>) {
|
||||
return <AccordionPrimitive.Root data-slot="accordion" {...props} />;
|
||||
}
|
||||
|
||||
function AccordionItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Item>) {
|
||||
return (
|
||||
<AccordionPrimitive.Item
|
||||
data-slot="accordion-item"
|
||||
className={cn("border-b last:border-b-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionTrigger({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
|
||||
return (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
data-slot="accordion-trigger"
|
||||
className={cn(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 flex flex-1 items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&[data-state=open]>svg]:rotate-180",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<Icons.ChevronDown className="text-muted-foreground pointer-events-none size-4 shrink-0 translate-y-0.5 transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
);
|
||||
}
|
||||
|
||||
function AccordionContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AccordionPrimitive.Content>) {
|
||||
return (
|
||||
<AccordionPrimitive.Content
|
||||
data-slot="accordion-content"
|
||||
className="data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down overflow-hidden text-sm"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pt-0 pb-4", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
);
|
||||
}
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
|
||||
157
packages/ui/web/src/components/alert-dialog.tsx
Normal file
@@ -0,0 +1,157 @@
|
||||
"use client";
|
||||
|
||||
import { AlertDialog as AlertDialogPrimitive } from "radix-ui";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
import { buttonVariants } from "@turbostarter/ui-web/button";
|
||||
|
||||
function AlertDialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
|
||||
return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />;
|
||||
}
|
||||
|
||||
function AlertDialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
data-slot="alert-dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
|
||||
return (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
data-slot="alert-dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Title
|
||||
data-slot="alert-dialog-title"
|
||||
className={cn("text-lg font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Description
|
||||
data-slot="alert-dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogAction({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Action
|
||||
className={cn(buttonVariants(), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDialogCancel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
|
||||
return (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
className={cn(buttonVariants({ variant: "outline" }), "mt-2 sm:mt-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogPortal,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
};
|
||||
73
packages/ui/web/src/components/alert.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-card text-card-foreground [&_[data-slot=alert-description]]:text-muted-foreground",
|
||||
destructive:
|
||||
"border-destructive/20 bg-destructive/5 text-destructive *:data-[slot=alert-description]:text-destructive/90",
|
||||
primary:
|
||||
"border-primary/20 bg-primary/5 text-primary [&_[data-slot=alert-description]]:text-primary/90",
|
||||
success:
|
||||
"border-success/20 bg-success/5 text-success *:data-[slot=alert-description]:text-success/90",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 text-left font-medium tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"col-start-2 grid justify-items-start gap-1 text-left text-sm [&_p]:leading-relaxed",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription };
|
||||
176
packages/ui/web/src/components/animated-beam.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
"use client";
|
||||
|
||||
import { motion } from "framer-motion";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
interface AnimatedBeamProps {
|
||||
className?: string;
|
||||
containerRef: React.RefObject<HTMLElement | null>;
|
||||
fromRef: React.RefObject<HTMLElement | null>;
|
||||
toRef: React.RefObject<HTMLElement | null>;
|
||||
curvature?: number;
|
||||
reverse?: boolean;
|
||||
pathColor?: string;
|
||||
pathWidth?: number;
|
||||
pathOpacity?: number;
|
||||
gradientStartColor?: string;
|
||||
gradientStopColor?: string;
|
||||
delay?: number;
|
||||
duration?: number;
|
||||
startXOffset?: number;
|
||||
startYOffset?: number;
|
||||
endXOffset?: number;
|
||||
endYOffset?: number;
|
||||
}
|
||||
|
||||
function AnimatedBeam({
|
||||
className,
|
||||
containerRef,
|
||||
fromRef,
|
||||
toRef,
|
||||
curvature = 0,
|
||||
reverse = false,
|
||||
duration = Math.random() * 3 + 4,
|
||||
delay = 0,
|
||||
pathColor = "gray",
|
||||
pathWidth = 2,
|
||||
pathOpacity = 0.2,
|
||||
gradientStartColor = "#ffaa40",
|
||||
gradientStopColor = "#9c40ff",
|
||||
startXOffset = 0,
|
||||
startYOffset = 0,
|
||||
endXOffset = 0,
|
||||
endYOffset = 0,
|
||||
}: AnimatedBeamProps) {
|
||||
const id = React.useId();
|
||||
const [pathD, setPathD] = React.useState("");
|
||||
const [svgDimensions, setSvgDimensions] = React.useState({
|
||||
width: 0,
|
||||
height: 0,
|
||||
});
|
||||
|
||||
const gradientCoordinates = reverse
|
||||
? {
|
||||
x1: ["90%", "-10%"],
|
||||
x2: ["100%", "0%"],
|
||||
y1: ["0%", "0%"],
|
||||
y2: ["0%", "0%"],
|
||||
}
|
||||
: {
|
||||
x1: ["10%", "110%"],
|
||||
x2: ["0%", "100%"],
|
||||
y1: ["0%", "0%"],
|
||||
y2: ["0%", "0%"],
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const updatePath = () => {
|
||||
if (containerRef.current && fromRef.current && toRef.current) {
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const rectA = fromRef.current.getBoundingClientRect();
|
||||
const rectB = toRef.current.getBoundingClientRect();
|
||||
|
||||
const svgWidth = containerRect.width;
|
||||
const svgHeight = containerRect.height;
|
||||
setSvgDimensions({ width: svgWidth, height: svgHeight });
|
||||
|
||||
const startX =
|
||||
rectA.left - containerRect.left + rectA.width / 2 + startXOffset;
|
||||
const startY =
|
||||
rectA.top - containerRect.top + rectA.height / 2 + startYOffset;
|
||||
const endX =
|
||||
rectB.left - containerRect.left + rectB.width / 2 + endXOffset;
|
||||
const endY =
|
||||
rectB.top - containerRect.top + rectB.height / 2 + endYOffset;
|
||||
|
||||
const controlY = startY - curvature;
|
||||
const d = `M ${startX},${startY} Q ${(startX + endX) / 2},${controlY} ${endX},${endY}`;
|
||||
setPathD(d);
|
||||
}
|
||||
};
|
||||
|
||||
updatePath();
|
||||
|
||||
const resizeObserver = new ResizeObserver(updatePath);
|
||||
if (containerRef.current) {
|
||||
resizeObserver.observe(containerRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [
|
||||
containerRef,
|
||||
fromRef,
|
||||
toRef,
|
||||
curvature,
|
||||
startXOffset,
|
||||
startYOffset,
|
||||
endXOffset,
|
||||
endYOffset,
|
||||
]);
|
||||
|
||||
return (
|
||||
<svg
|
||||
fill="none"
|
||||
width={svgDimensions.width}
|
||||
height={svgDimensions.height}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={cn(
|
||||
"pointer-events-none absolute left-0 top-0 transform-gpu stroke-2",
|
||||
className,
|
||||
)}
|
||||
viewBox={`0 0 ${svgDimensions.width} ${svgDimensions.height}`}
|
||||
>
|
||||
<path
|
||||
d={pathD}
|
||||
stroke={pathColor}
|
||||
strokeWidth={pathWidth}
|
||||
strokeOpacity={pathOpacity}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<path
|
||||
d={pathD}
|
||||
strokeWidth={pathWidth}
|
||||
stroke={`url(#${id})`}
|
||||
strokeOpacity="1"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
<defs>
|
||||
<motion.linearGradient
|
||||
className="transform-gpu"
|
||||
id={id}
|
||||
gradientUnits="userSpaceOnUse"
|
||||
initial={{
|
||||
x1: "0%",
|
||||
x2: "0%",
|
||||
y1: "0%",
|
||||
y2: "0%",
|
||||
}}
|
||||
animate={{
|
||||
x1: gradientCoordinates.x1,
|
||||
x2: gradientCoordinates.x2,
|
||||
y1: gradientCoordinates.y1,
|
||||
y2: gradientCoordinates.y2,
|
||||
}}
|
||||
transition={{
|
||||
delay,
|
||||
duration,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
repeat: Infinity,
|
||||
repeatDelay: 0,
|
||||
}}
|
||||
>
|
||||
<stop stopColor={gradientStartColor} stopOpacity="0" />
|
||||
<stop stopColor={gradientStartColor} />
|
||||
<stop offset="32.5%" stopColor={gradientStopColor} />
|
||||
<stop offset="100%" stopColor={gradientStopColor} stopOpacity="0" />
|
||||
</motion.linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export { AnimatedBeam };
|
||||
53
packages/ui/web/src/components/avatar.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { Avatar as AvatarPrimitive } from "radix-ui";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
function Avatar({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Root>) {
|
||||
return (
|
||||
<AvatarPrimitive.Root
|
||||
data-slot="avatar"
|
||||
className={cn(
|
||||
"relative flex size-8 shrink-0 overflow-hidden rounded-full",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarImage({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Image>) {
|
||||
return (
|
||||
<AvatarPrimitive.Image
|
||||
data-slot="avatar-image"
|
||||
className={cn("aspect-square size-full", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AvatarFallback({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
|
||||
return (
|
||||
<AvatarPrimitive.Fallback
|
||||
data-slot="avatar-fallback"
|
||||
className={cn(
|
||||
"bg-muted flex size-full items-center justify-center rounded-full border",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Avatar, AvatarImage, AvatarFallback };
|
||||
48
packages/ui/web/src/components/badge.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] [&>svg]:pointer-events-none [&>svg]:size-3",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"bg-primary text-primary-foreground [a&]:hover:bg-primary/90 border-transparent",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90 border-transparent",
|
||||
destructive:
|
||||
"bg-destructive [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 border-transparent text-white",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"span"> &
|
||||
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
||||
const Comp = asChild ? Slot : "span";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="badge"
|
||||
className={cn(badgeVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
111
packages/ui/web/src/components/breadcrumb.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import * as React from "react";
|
||||
|
||||
import { useTranslation } from "@turbostarter/i18n";
|
||||
import { cn } from "@turbostarter/ui";
|
||||
import { Icons } from "@turbostarter/ui-web/icons";
|
||||
|
||||
function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
|
||||
return <nav aria-label="breadcrumb" data-slot="breadcrumb" {...props} />;
|
||||
}
|
||||
|
||||
function BreadcrumbList({ className, ...props }: React.ComponentProps<"ol">) {
|
||||
return (
|
||||
<ol
|
||||
data-slot="breadcrumb-list"
|
||||
className={cn(
|
||||
"text-muted-foreground flex flex-wrap items-center gap-1.5 text-sm break-words sm:gap-2.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbItem({ className, ...props }: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-item"
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbLink({
|
||||
asChild,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a"> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="breadcrumb-link"
|
||||
className={cn("hover:text-foreground transition-colors", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbPage({ className, ...props }: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-page"
|
||||
role="link"
|
||||
aria-disabled="true"
|
||||
aria-current="page"
|
||||
className={cn("text-foreground font-normal", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbSeparator({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) {
|
||||
return (
|
||||
<li
|
||||
data-slot="breadcrumb-separator"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:size-3.5", className)}
|
||||
{...props}
|
||||
>
|
||||
{children ?? <Icons.ChevronRight />}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function BreadcrumbEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
const { t } = useTranslation("common");
|
||||
return (
|
||||
<span
|
||||
data-slot="breadcrumb-ellipsis"
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<Icons.MoreHorizontal className="size-4" />
|
||||
<span className="sr-only">{t("more")}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
};
|
||||
59
packages/ui/web/src/components/built-with.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import { useTranslation } from "@turbostarter/i18n";
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import { buttonVariants } from "#components/button";
|
||||
|
||||
export const BuiltWith = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"a">) => {
|
||||
const { t } = useTranslation("common");
|
||||
|
||||
return (
|
||||
<a
|
||||
className={cn(
|
||||
buttonVariants({
|
||||
variant: "outline",
|
||||
className: "cursor-pointer gap-1.5 font-sans",
|
||||
}),
|
||||
className,
|
||||
)}
|
||||
href="https://www.turbostarter.dev"
|
||||
target="_blank"
|
||||
{...props}
|
||||
>
|
||||
{t("builtWith")}{" "}
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
<svg
|
||||
viewBox="0 0 512 517"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="text-primary h-5"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M383.309 10.9714C383.309 4.91208 388.221 0 394.28 0C400.339 0 405.251 4.91208 405.251 10.9714V122.57C405.251 138.493 398.332 153.631 386.292 164.051L367.707 180.134L391.678 204.105L428.239 190.516C441.132 185.724 449.686 173.419 449.686 159.664V58.5143C449.686 52.4549 454.598 47.5429 460.657 47.5429C466.717 47.5429 471.629 52.4549 471.629 58.5143V159.664C471.629 182.589 457.372 203.097 435.883 211.084L405.175 222.498C415.977 243.457 415.977 268.543 405.175 289.502L440.649 302.687C459.273 309.609 471.629 327.383 471.629 347.251V453.486C471.629 459.545 466.717 464.457 460.657 464.457C454.598 464.457 449.686 459.545 449.686 453.486V347.251C449.686 336.553 443.033 326.983 433.005 323.255L391.678 307.895L367.707 331.866L388.819 350.137C399.255 359.167 405.251 372.287 405.251 386.087V501.029C405.251 507.088 400.339 512 394.28 512C388.221 512 383.309 507.088 383.309 501.029V386.087C383.309 378.656 380.08 371.592 374.461 366.729L352.151 347.423L266.345 433.229C260.632 438.941 251.37 438.941 245.657 433.229L160.142 347.713L138.168 366.729C132.549 371.592 129.32 378.656 129.32 386.087V501.029C129.32 507.088 124.408 512 118.349 512C112.289 512 107.377 507.088 107.377 501.029V386.087C107.377 372.287 113.374 359.167 123.809 350.137L144.586 332.157L120.493 308.065L79.624 323.255C69.5957 326.983 62.9429 336.553 62.9429 347.251V453.486C62.9429 459.545 58.0308 464.457 51.9714 464.457C45.9121 464.457 41 459.545 41 453.486V347.251C41 327.383 53.3553 309.609 71.9792 302.687L106.928 289.698C95.991 268.634 95.991 243.366 106.928 222.303L76.7452 211.084C55.2562 203.097 41 182.589 41 159.664V58.5143C41 52.4549 45.9121 47.5429 51.9714 47.5429C58.0308 47.5429 62.9429 52.4549 62.9429 58.5143V159.664C62.9429 173.419 71.4966 185.724 84.39 190.516L120.494 203.935L144.586 179.843L126.337 164.051C114.296 153.631 107.377 138.493 107.377 122.57V10.9714C107.377 4.91208 112.289 0 118.349 0C124.408 0 129.32 4.91208 129.32 10.9714V122.57C129.32 132.124 133.472 141.206 140.696 147.458L160.142 164.287L245.657 78.7717C251.37 73.0588 260.632 73.0589 266.345 78.7717L352.151 164.577L371.933 147.458C379.157 141.206 383.309 132.124 383.309 122.57V10.9714ZM267.067 166.768C272.383 161.416 281.338 166.639 279.298 173.901L268.742 211.469C266.776 218.467 272.035 225.408 279.304 225.408H318.878C335.139 225.408 343.311 245.043 331.851 256.58L244.619 344.4C239.303 349.752 230.348 344.529 232.388 337.267L242.944 299.699C244.91 292.701 239.65 285.76 232.381 285.76H192.808C176.547 285.76 168.375 266.125 179.835 254.588L267.067 166.768Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
viewBox="0 0 667 84"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="text-foreground h-[11px]"
|
||||
>
|
||||
<path
|
||||
d="M38.887 82H26.497V12.2781H0.140091V1.12708H65.2439V12.2781H38.887V82ZM106.939 57.4453V22.3027H118.315V82H107.727V72.5385C104.573 78.7335 97.5899 83.1264 89.4801 83.1264C77.428 83.1264 69.0929 76.0303 69.0929 60.7117V22.3027H80.4692V58.459C80.4692 68.8215 85.6505 73.1017 93.0845 73.1017C100.744 73.1017 106.939 66.7941 106.939 57.4453ZM144.364 51.4755V82H132.988V22.3027H143.575V34.918C147.743 26.6955 156.754 21.5143 166.553 21.5143V33.3411C153.713 32.6653 144.364 38.2971 144.364 51.4755ZM231.424 52.1514C231.424 70.0605 221.061 83.1264 204.954 83.1264C196.507 83.1264 189.298 78.8462 185.018 71.2995V82H174.43V1.12708H185.806V32.5526C189.974 25.2313 196.845 21.1764 204.954 21.1764C220.949 21.1764 231.424 34.0169 231.424 52.1514ZM219.597 52.1514C219.597 38.635 212.501 31.201 202.702 31.201C193.24 31.201 185.806 38.5224 185.806 51.9261C185.806 65.1045 193.015 72.9891 202.702 72.9891C212.501 72.9891 219.597 65.4425 219.597 52.1514ZM266.837 83.1264C249.941 83.1264 237.551 69.8353 237.551 52.1514C237.551 34.4675 249.941 21.1764 266.837 21.1764C283.732 21.1764 296.122 34.4675 296.122 52.1514C296.122 69.8353 283.732 83.1264 266.837 83.1264ZM266.837 73.1017C276.636 73.1017 284.408 65.2172 284.408 52.1514C284.408 39.0855 276.636 31.3136 266.837 31.3136C257.037 31.3136 249.378 39.0855 249.378 52.1514C249.378 65.2172 257.037 73.1017 266.837 73.1017ZM366.628 58.3464C366.628 72.4259 355.364 83.1264 335.766 83.1264C316.28 83.1264 304.453 72.4259 303.439 56.3189H316.054C316.617 66.5688 323.15 73.2144 335.54 73.2144C345.79 73.2144 353.45 68.371 353.45 60.0359C353.45 53.2777 349.057 49.8986 339.708 47.8712L327.994 45.6185C316.617 43.3657 306.255 37.6213 306.255 23.8796C306.255 10.2506 318.194 0.000719194 334.977 0.000719194C351.76 0.000719194 364.488 10.2506 365.502 26.3576H352.886C352.211 16.6709 345.227 9.91272 335.09 9.91272C324.615 9.91272 318.87 16.1077 318.87 23.0912C318.87 30.7505 325.516 33.4537 333.062 35.0306L345.002 37.396C358.856 40.2119 366.628 46.2943 366.628 58.3464ZM412.842 70.9616V80.9863C409.351 82.5632 406.309 83.1264 402.705 83.1264C391.667 83.1264 384.007 77.1566 384.007 63.9782V31.9894H370.829V22.3027H384.007V4.61881H395.384V22.3027H413.406V31.9894H395.384V61.3875C395.384 69.61 399.326 72.5385 405.408 72.5385C408.112 72.5385 410.477 72.088 412.842 70.9616ZM459.293 82V72.7638C455.576 79.4094 448.93 83.1264 440.144 83.1264C427.754 83.1264 419.645 76.0303 419.645 65.1045C419.645 53.3904 428.993 47.308 446.79 47.308C450.282 47.308 453.098 47.4206 457.941 47.9838V43.591C457.941 35.0306 453.323 30.1873 445.438 30.1873C437.103 30.1873 432.035 35.1433 431.697 43.4784H421.334C421.897 30.0746 431.471 21.1764 445.438 21.1764C460.194 21.1764 468.754 29.5115 468.754 43.7036V82H459.293ZM430.458 64.7666C430.458 70.9616 435.076 75.0165 442.397 75.0165C451.971 75.0165 457.941 69.0468 457.941 59.9233V55.0799C453.548 54.5167 450.394 54.4041 447.466 54.4041C436.089 54.4041 430.458 57.7832 430.458 64.7666ZM494.214 51.4755V82H482.838V22.3027H493.426V34.918C497.593 26.6955 506.604 21.5143 516.404 21.5143V33.3411C503.563 32.6653 494.214 38.2971 494.214 51.4755ZM561.588 70.9616V80.9863C558.097 82.5632 555.055 83.1264 551.451 83.1264C540.413 83.1264 532.753 77.1566 532.753 63.9782V31.9894H519.575V22.3027H532.753V4.61881H544.13V22.3027H562.152V31.9894H544.13V61.3875C544.13 69.61 548.072 72.5385 554.154 72.5385C556.858 72.5385 559.223 72.088 561.588 70.9616ZM594.787 83.1264C577.554 83.1264 565.952 70.6237 565.952 51.8135C565.952 34.1295 578.004 21.1764 594.449 21.1764C612.246 21.1764 624.073 35.5938 622.045 54.9673H577.554C578.455 67.132 584.537 74.2281 594.562 74.2281C603.01 74.2281 608.867 69.61 610.781 61.8381H622.045C619.117 75.1292 608.867 83.1264 594.787 83.1264ZM594.224 29.7367C585.1 29.7367 578.905 36.2696 577.666 47.4206H609.993C609.43 36.3823 603.46 29.7367 594.224 29.7367ZM644.39 51.4755V82H633.014V22.3027H643.602V34.918C647.769 26.6955 656.78 21.5143 666.58 21.5143V33.3411C653.739 32.6653 644.39 38.2971 644.39 51.4755Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
60
packages/ui/web/src/components/button.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-']):not([class*='w-']):not([class*='h-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 dark:hover:bg-destructive/50 text-white",
|
||||
outline:
|
||||
"bg-background hover:bg-accent hover:text-accent-foreground dark:border-input dark:hover:bg-accent/50 border shadow-xs",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2 has-[>svg]:px-3.5",
|
||||
sm: "h-9 gap-1.5 rounded-md px-3.5 has-[>svg]:px-3",
|
||||
lg: "h-11 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
asChild = false,
|
||||
...props
|
||||
}: React.ComponentProps<"button"> &
|
||||
VariantProps<typeof buttonVariants> & {
|
||||
asChild?: boolean;
|
||||
}) {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
data-slot="button"
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Button, buttonVariants };
|
||||
215
packages/ui/web/src/components/calendar.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { DayPicker, getDefaultClassNames } from "react-day-picker";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
import { Icons } from "@turbostarter/ui-web/icons";
|
||||
|
||||
import type { DayButton } from "react-day-picker";
|
||||
|
||||
import { Button, buttonVariants } from "#components/button";
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
captionLayout = "label",
|
||||
buttonVariant = "ghost",
|
||||
formatters,
|
||||
components,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayPicker> & {
|
||||
buttonVariant?: React.ComponentProps<typeof Button>["variant"];
|
||||
}) {
|
||||
const defaultClassNames = getDefaultClassNames();
|
||||
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn(
|
||||
"bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
|
||||
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
||||
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
||||
className,
|
||||
)}
|
||||
captionLayout={captionLayout}
|
||||
formatters={{
|
||||
formatMonthDropdown: (date) =>
|
||||
date.toLocaleString("default", { month: "short" }),
|
||||
...formatters,
|
||||
}}
|
||||
classNames={{
|
||||
root: cn("w-fit", defaultClassNames.root),
|
||||
months: cn(
|
||||
"relative flex flex-col gap-4 md:flex-row",
|
||||
defaultClassNames.months,
|
||||
),
|
||||
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
|
||||
nav: cn(
|
||||
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
|
||||
defaultClassNames.nav,
|
||||
),
|
||||
button_previous: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
|
||||
defaultClassNames.button_previous,
|
||||
),
|
||||
button_next: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
|
||||
defaultClassNames.button_next,
|
||||
),
|
||||
month_caption: cn(
|
||||
"flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
|
||||
defaultClassNames.month_caption,
|
||||
),
|
||||
dropdowns: cn(
|
||||
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
|
||||
defaultClassNames.dropdowns,
|
||||
),
|
||||
dropdown_root: cn(
|
||||
"has-focus:border-ring border-input has-focus:ring-ring/50 relative rounded-md border shadow-xs has-focus:ring-[3px]",
|
||||
defaultClassNames.dropdown_root,
|
||||
),
|
||||
dropdown: cn(
|
||||
"bg-popover absolute inset-0 opacity-0",
|
||||
defaultClassNames.dropdown,
|
||||
),
|
||||
caption_label: cn(
|
||||
"font-medium select-none",
|
||||
captionLayout === "label"
|
||||
? "text-sm"
|
||||
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm [&>svg]:size-3.5",
|
||||
defaultClassNames.caption_label,
|
||||
),
|
||||
table: "w-full border-collapse",
|
||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||
weekday: cn(
|
||||
"text-muted-foreground flex-1 rounded-md text-[0.8rem] font-normal select-none",
|
||||
defaultClassNames.weekday,
|
||||
),
|
||||
week: cn("mt-2 flex w-full", defaultClassNames.week),
|
||||
week_number_header: cn(
|
||||
"w-(--cell-size) select-none",
|
||||
defaultClassNames.week_number_header,
|
||||
),
|
||||
week_number: cn(
|
||||
"text-muted-foreground text-[0.8rem] select-none",
|
||||
defaultClassNames.week_number,
|
||||
),
|
||||
day: cn(
|
||||
"group/day relative aspect-square h-full w-full p-0 text-center select-none [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
|
||||
defaultClassNames.day,
|
||||
),
|
||||
range_start: cn(
|
||||
"bg-accent rounded-l-md",
|
||||
defaultClassNames.range_start,
|
||||
),
|
||||
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||
range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end),
|
||||
today: cn(
|
||||
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
|
||||
defaultClassNames.today,
|
||||
),
|
||||
outside: cn(
|
||||
"text-muted-foreground aria-selected:text-muted-foreground",
|
||||
defaultClassNames.outside,
|
||||
),
|
||||
disabled: cn(
|
||||
"text-muted-foreground opacity-50",
|
||||
defaultClassNames.disabled,
|
||||
),
|
||||
hidden: cn("invisible", defaultClassNames.hidden),
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
Root: ({ className, rootRef, ...props }) => {
|
||||
return (
|
||||
<div
|
||||
data-slot="calendar"
|
||||
ref={rootRef}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
Chevron: ({ className, orientation, ...props }) => {
|
||||
if (orientation === "left") {
|
||||
return (
|
||||
<Icons.ChevronLeft
|
||||
className={cn("size-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (orientation === "right") {
|
||||
return (
|
||||
<Icons.ChevronRight
|
||||
className={cn("size-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Icons.ChevronDown className={cn("size-4", className)} {...props} />
|
||||
);
|
||||
},
|
||||
DayButton: CalendarDayButton,
|
||||
WeekNumber: ({ children, ...props }) => {
|
||||
return (
|
||||
<td {...props}>
|
||||
<div className="flex size-(--cell-size) items-center justify-center text-center">
|
||||
{children}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
},
|
||||
...components,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CalendarDayButton({
|
||||
className,
|
||||
day,
|
||||
modifiers,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayButton>) {
|
||||
const defaultClassNames = getDefaultClassNames();
|
||||
|
||||
const ref = React.useRef<HTMLButtonElement>(null);
|
||||
React.useEffect(() => {
|
||||
if (modifiers.focused) ref.current?.focus();
|
||||
}, [modifiers.focused]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
data-day={day.date.toLocaleDateString()}
|
||||
data-selected-single={
|
||||
modifiers.selected &&
|
||||
!modifiers.range_start &&
|
||||
!modifiers.range_end &&
|
||||
!modifiers.range_middle
|
||||
}
|
||||
data-range-start={modifiers.range_start}
|
||||
data-range-end={modifiers.range_end}
|
||||
data-range-middle={modifiers.range_middle}
|
||||
className={cn(
|
||||
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
|
||||
defaultClassNames.day,
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Calendar, CalendarDayButton };
|
||||
61
packages/ui/web/src/components/card.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
export function Card({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-card text-card-foreground isolate overflow-hidden rounded-lg border shadow-xs",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CardHeader({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div className={cn("flex flex-col space-y-2 p-6", className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export function CardTitle({ className, ...props }: React.ComponentProps<"h3">) {
|
||||
return (
|
||||
<h3
|
||||
className={cn(
|
||||
"text-2xl leading-none font-semibold tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CardDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"p">) {
|
||||
return (
|
||||
<p className={cn("text-muted-foreground text-sm", className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export function CardContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return <div className={cn("p-6 pt-0 overflow-hidden rounded-[inherit]", className)} {...props} />;
|
||||
}
|
||||
|
||||
export function CardFooter({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div className={cn("flex items-center p-6 pt-0", className)} {...props} />
|
||||
);
|
||||
}
|
||||
356
packages/ui/web/src/components/chart.tsx
Normal file
@@ -0,0 +1,356 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as RechartsPrimitive from "recharts";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: "", dark: ".dark" } as const;
|
||||
|
||||
export type ChartConfig = Record<
|
||||
string,
|
||||
{
|
||||
label?: React.ReactNode;
|
||||
icon?: React.ComponentType;
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
)
|
||||
>;
|
||||
|
||||
interface ChartContextProps {
|
||||
config: ChartConfig;
|
||||
}
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
function ChartContainer({
|
||||
id,
|
||||
className,
|
||||
children,
|
||||
config,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
config: ChartConfig;
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>["children"];
|
||||
}) {
|
||||
const uniqueId = React.useId();
|
||||
const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}`;
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-slot="chart"
|
||||
data-chart={chartId}
|
||||
className={cn(
|
||||
"[&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(
|
||||
([, config]) => config.theme ?? config.color,
|
||||
);
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ??
|
||||
itemConfig.color;
|
||||
return color ? ` --color-${key}: ${color};` : null;
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
`,
|
||||
)
|
||||
.join("\n"),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||
|
||||
function ChartTooltipContent({
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = "dot",
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
}: React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean;
|
||||
hideIndicator?: boolean;
|
||||
indicator?: "line" | "dot" | "dashed";
|
||||
nameKey?: string;
|
||||
labelKey?: string;
|
||||
}) {
|
||||
const { config } = useChart();
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [item] = payload;
|
||||
const key = `${labelKey ?? item?.dataKey ?? item?.name ?? "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? (config[label]?.label ?? label)
|
||||
: itemConfig?.label;
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn("font-medium", labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
]);
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot";
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey ?? item.name ?? item.dataKey ?? "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const indicatorColor = color ?? item.payload.fill ?? item.color;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5",
|
||||
indicator === "dot" && "items-center",
|
||||
)}
|
||||
>
|
||||
{formatter && item.value !== undefined && item.name ? (
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)",
|
||||
{
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
},
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center",
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label ?? item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className="text-foreground font-mono font-medium tabular-nums">
|
||||
{item.value.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend;
|
||||
|
||||
function ChartLegendContent({
|
||||
className,
|
||||
hideIcon = false,
|
||||
payload,
|
||||
verticalAlign = "bottom",
|
||||
nameKey,
|
||||
}: React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean;
|
||||
nameKey?: string;
|
||||
}) {
|
||||
const { config } = useChart();
|
||||
|
||||
if (!payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
const key = `${nameKey ?? item.dataKey ?? "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3",
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: unknown,
|
||||
key: string,
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
"payload" in payload &&
|
||||
typeof payload.payload === "object" &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined;
|
||||
|
||||
let configLabelKey: string = key;
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === "string"
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string;
|
||||
}
|
||||
|
||||
return configLabelKey in config ? config[configLabelKey] : config[key];
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
};
|
||||
32
packages/ui/web/src/components/checkbox.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import { Checkbox as CheckboxPrimitive } from "radix-ui";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
import { Icons } from "@turbostarter/ui-web/icons";
|
||||
|
||||
function Checkbox({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
data-slot="checkbox-indicator"
|
||||
className="flex items-center justify-center text-current transition-none"
|
||||
>
|
||||
<Icons.Check className="size-3.5" />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
);
|
||||
}
|
||||
|
||||
export { Checkbox };
|
||||
185
packages/ui/web/src/components/command.tsx
Normal file
@@ -0,0 +1,185 @@
|
||||
"use client";
|
||||
|
||||
import { Command as CommandPrimitive } from "cmdk";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
import { Icons } from "@turbostarter/ui-web/icons";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "#components/dialog";
|
||||
|
||||
function Command({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive>) {
|
||||
return (
|
||||
<CommandPrimitive
|
||||
data-slot="command"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandDialog({
|
||||
title = "Command Palette",
|
||||
description = "Search for a command to run...",
|
||||
children,
|
||||
className,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Dialog> & {
|
||||
title?: string;
|
||||
description?: string;
|
||||
className?: string;
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Dialog {...props}>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
<DialogDescription>{description}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogContent
|
||||
className={cn("overflow-hidden p-0", className)}
|
||||
showCloseButton={showCloseButton}
|
||||
>
|
||||
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
|
||||
{children}
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandInput({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="command-input-wrapper"
|
||||
className="flex h-10 items-center gap-2 border-b px-3"
|
||||
>
|
||||
<Icons.Search className="size-4 shrink-0 opacity-50" />
|
||||
<CommandPrimitive.Input
|
||||
data-slot="command-input"
|
||||
className={cn(
|
||||
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandList({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
||||
return (
|
||||
<CommandPrimitive.List
|
||||
data-slot="command-list"
|
||||
className={cn(
|
||||
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandEmpty({
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
||||
return (
|
||||
<CommandPrimitive.Empty
|
||||
data-slot="command-empty"
|
||||
className="py-6 text-center text-sm"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
||||
return (
|
||||
<CommandPrimitive.Group
|
||||
data-slot="command-group"
|
||||
className={cn(
|
||||
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
||||
return (
|
||||
<CommandPrimitive.Separator
|
||||
data-slot="command-separator"
|
||||
className={cn("bg-border -mx-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandItem({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
||||
return (
|
||||
<CommandPrimitive.Item
|
||||
data-slot="command-item"
|
||||
className={cn(
|
||||
"data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="command-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandShortcut,
|
||||
CommandSeparator,
|
||||
};
|
||||
@@ -0,0 +1,97 @@
|
||||
"use client";
|
||||
|
||||
import { useTranslation } from "@turbostarter/i18n";
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import type { Column } from "@tanstack/react-table";
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "#components/dropdown-menu";
|
||||
import { Icons } from "#components/icons";
|
||||
|
||||
interface DataTableColumnHeaderProps<TData, TValue>
|
||||
extends React.ComponentProps<typeof DropdownMenuTrigger> {
|
||||
column: Column<TData, TValue>;
|
||||
title: string;
|
||||
}
|
||||
|
||||
export function DataTableColumnHeader<TData, TValue>({
|
||||
column,
|
||||
title,
|
||||
className,
|
||||
...props
|
||||
}: DataTableColumnHeaderProps<TData, TValue>) {
|
||||
const { t } = useTranslation("common");
|
||||
|
||||
if (!column.getCanSort() && !column.getCanHide()) {
|
||||
return <div className={cn(className)}>{title}</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
className={cn(
|
||||
"hover:bg-accent focus:ring-ring data-[state=open]:bg-accent [&_svg]:text-muted-foreground -ml-1.5 flex h-8 items-center gap-1.5 rounded-md px-2 py-1.5 focus:ring-1 focus:outline-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{title}
|
||||
{column.getCanSort() &&
|
||||
(column.getIsSorted() === "desc" ? (
|
||||
<Icons.ChevronDown />
|
||||
) : column.getIsSorted() === "asc" ? (
|
||||
<Icons.ChevronUp />
|
||||
) : (
|
||||
<Icons.ChevronsUpDown />
|
||||
))}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="w-28 font-normal">
|
||||
{column.getCanSort() && (
|
||||
<>
|
||||
<DropdownMenuCheckboxItem
|
||||
className="[&_svg]:text-muted-foreground relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto"
|
||||
checked={column.getIsSorted() === "asc"}
|
||||
onClick={() => column.toggleSorting(false)}
|
||||
>
|
||||
<Icons.ChevronUp />
|
||||
{t("asc")}
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem
|
||||
className="[&_svg]:text-muted-foreground relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto"
|
||||
checked={column.getIsSorted() === "desc"}
|
||||
onClick={() => column.toggleSorting(true)}
|
||||
>
|
||||
<Icons.ChevronDown />
|
||||
{t("desc")}
|
||||
</DropdownMenuCheckboxItem>
|
||||
{column.getIsSorted() && (
|
||||
<DropdownMenuItem
|
||||
className="[&_svg]:text-muted-foreground pl-2"
|
||||
onClick={() => column.clearSorting()}
|
||||
>
|
||||
<Icons.X />
|
||||
{t("reset")}
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{column.getCanHide() && (
|
||||
<DropdownMenuCheckboxItem
|
||||
className="[&_svg]:text-muted-foreground relative pr-8 pl-2 [&>span:first-child]:right-2 [&>span:first-child]:left-auto"
|
||||
checked={!column.getIsVisible()}
|
||||
onClick={() => column.toggleVisibility(false)}
|
||||
>
|
||||
<Icons.EyeOff />
|
||||
{t("hide")}
|
||||
</DropdownMenuCheckboxItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import { useTranslation } from "@turbostarter/i18n";
|
||||
|
||||
import type { Column } from "@tanstack/react-table";
|
||||
import type { DateRange } from "react-day-picker";
|
||||
|
||||
import { Button } from "#components/button";
|
||||
import { Calendar } from "#components/calendar";
|
||||
import { Icons } from "#components/icons";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "#components/popover";
|
||||
import { Separator } from "#components/separator";
|
||||
|
||||
type DateSelection = Date[] | DateRange;
|
||||
|
||||
function getIsDateRange(value: DateSelection): value is DateRange {
|
||||
return typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function parseAsDate(timestamp: number | string | undefined): Date | undefined {
|
||||
if (!timestamp) return undefined;
|
||||
const numericTimestamp =
|
||||
typeof timestamp === "string" ? Number(timestamp) : timestamp;
|
||||
const date = new Date(numericTimestamp);
|
||||
return !Number.isNaN(date.getTime()) ? date : undefined;
|
||||
}
|
||||
|
||||
function parseColumnFilterValue(value: unknown) {
|
||||
if (value === null || value === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => {
|
||||
if (typeof item === "number" || typeof item === "string") {
|
||||
return item;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof value === "string" || typeof value === "number") {
|
||||
return [value];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
interface DataTableDateFilterProps<TData> {
|
||||
column: Column<TData, unknown>;
|
||||
title?: string;
|
||||
multiple?: boolean;
|
||||
}
|
||||
|
||||
export function DataTableDateFilter<TData>({
|
||||
column,
|
||||
title,
|
||||
multiple,
|
||||
}: DataTableDateFilterProps<TData>) {
|
||||
const { i18n, t } = useTranslation("common");
|
||||
const columnFilterValue = column.getFilterValue();
|
||||
|
||||
const selectedDates = React.useMemo<DateSelection>(() => {
|
||||
if (!columnFilterValue) {
|
||||
return multiple ? { from: undefined, to: undefined } : [];
|
||||
}
|
||||
|
||||
if (multiple) {
|
||||
const timestamps = parseColumnFilterValue(columnFilterValue);
|
||||
return {
|
||||
from: parseAsDate(timestamps[0]),
|
||||
to: parseAsDate(timestamps[1]),
|
||||
};
|
||||
}
|
||||
|
||||
const timestamps = parseColumnFilterValue(columnFilterValue);
|
||||
const date = parseAsDate(timestamps[0]);
|
||||
return date ? [date] : [];
|
||||
}, [columnFilterValue, multiple]);
|
||||
|
||||
const onSelect = React.useCallback(
|
||||
(date: Date | DateRange | undefined) => {
|
||||
if (!date) {
|
||||
column.setFilterValue(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
if (multiple && !("getTime" in date)) {
|
||||
const from = date.from?.getTime();
|
||||
const to = date.to?.getTime();
|
||||
column.setFilterValue(from || to ? [from, to] : undefined);
|
||||
} else if (!multiple && "getTime" in date) {
|
||||
column.setFilterValue(date.getTime());
|
||||
}
|
||||
},
|
||||
[column, multiple],
|
||||
);
|
||||
|
||||
const onReset = React.useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
column.setFilterValue(undefined);
|
||||
},
|
||||
[column],
|
||||
);
|
||||
|
||||
const hasValue = React.useMemo(() => {
|
||||
if (multiple) {
|
||||
if (!getIsDateRange(selectedDates)) return false;
|
||||
return selectedDates.from ?? selectedDates.to;
|
||||
}
|
||||
if (!Array.isArray(selectedDates)) return false;
|
||||
return selectedDates.length > 0;
|
||||
}, [multiple, selectedDates]);
|
||||
|
||||
const formatDateRange = React.useCallback(
|
||||
(range: DateRange) => {
|
||||
if (!range.from && !range.to) return "";
|
||||
if (range.from && range.to) {
|
||||
return `${range.from.toLocaleDateString(i18n.language)} - ${range.to.toLocaleDateString(i18n.language)}`;
|
||||
}
|
||||
return (range.from ?? range.to)?.toLocaleDateString(i18n.language);
|
||||
},
|
||||
[i18n.language],
|
||||
);
|
||||
|
||||
const label = React.useMemo(() => {
|
||||
if (multiple) {
|
||||
if (!getIsDateRange(selectedDates)) return null;
|
||||
|
||||
const hasSelectedDates = selectedDates.from ?? selectedDates.to;
|
||||
const dateText = hasSelectedDates
|
||||
? formatDateRange(selectedDates)
|
||||
: t("selectDateRange");
|
||||
|
||||
return (
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{title}</span>
|
||||
{hasSelectedDates && (
|
||||
<>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-0.5 data-[orientation=vertical]:h-4"
|
||||
/>
|
||||
<span>{dateText}</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (getIsDateRange(selectedDates)) return null;
|
||||
|
||||
const hasSelectedDate = selectedDates.length > 0;
|
||||
const dateText = hasSelectedDate
|
||||
? selectedDates[0]?.toLocaleDateString(i18n.language)
|
||||
: t("selectDate");
|
||||
|
||||
return (
|
||||
<span className="flex items-center gap-2">
|
||||
<span>{title}</span>
|
||||
{hasSelectedDate && (
|
||||
<>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-0.5 data-[orientation=vertical]:h-4"
|
||||
/>
|
||||
<span>{dateText}</span>
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}, [selectedDates, multiple, formatDateRange, title, t, i18n.language]);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="border-dashed">
|
||||
{hasValue ? (
|
||||
<div
|
||||
role="button"
|
||||
aria-label={`Clear ${title} filter`}
|
||||
tabIndex={0}
|
||||
onClick={onReset}
|
||||
className="focus-visible:ring-ring rounded-sm opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-1 focus-visible:outline-none"
|
||||
>
|
||||
<Icons.XCircle />
|
||||
</div>
|
||||
) : (
|
||||
<Icons.Calendar />
|
||||
)}
|
||||
{label}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0" align="start">
|
||||
{multiple ? (
|
||||
<Calendar
|
||||
captionLayout="dropdown"
|
||||
mode="range"
|
||||
selected={
|
||||
getIsDateRange(selectedDates)
|
||||
? selectedDates
|
||||
: { from: undefined, to: undefined }
|
||||
}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
) : (
|
||||
<Calendar
|
||||
captionLayout="dropdown"
|
||||
mode="single"
|
||||
selected={
|
||||
!getIsDateRange(selectedDates) ? selectedDates[0] : undefined
|
||||
}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
)}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import { useTranslation } from "@turbostarter/i18n";
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import type { Icon } from "#components/icons";
|
||||
import type { Column } from "@tanstack/react-table";
|
||||
|
||||
import { Badge } from "#components/badge";
|
||||
import { Button } from "#components/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from "#components/command";
|
||||
import { Icons } from "#components/icons";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "#components/popover";
|
||||
import { Separator } from "#components/separator";
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
value: string;
|
||||
count?: number;
|
||||
icon?: Icon;
|
||||
}
|
||||
|
||||
interface DataTableFacetedFilterProps<TData, TValue> {
|
||||
column?: Column<TData, TValue>;
|
||||
title?: string;
|
||||
options: Option[];
|
||||
multiple?: boolean;
|
||||
}
|
||||
|
||||
export function DataTableFacetedFilter<TData, TValue>({
|
||||
column,
|
||||
title,
|
||||
options,
|
||||
multiple,
|
||||
}: DataTableFacetedFilterProps<TData, TValue>) {
|
||||
const { t } = useTranslation("common");
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const columnFilterValue = column?.getFilterValue();
|
||||
|
||||
const selectedValues = React.useMemo(
|
||||
() => new Set(Array.isArray(columnFilterValue) ? columnFilterValue : []),
|
||||
[columnFilterValue],
|
||||
);
|
||||
|
||||
const onItemSelect = React.useCallback(
|
||||
(option: Option, isSelected: boolean) => {
|
||||
if (!column) return;
|
||||
|
||||
if (multiple) {
|
||||
const newSelectedValues = new Set(selectedValues);
|
||||
if (isSelected) {
|
||||
newSelectedValues.delete(option.value);
|
||||
} else {
|
||||
newSelectedValues.add(option.value);
|
||||
}
|
||||
const filterValues = Array.from(newSelectedValues);
|
||||
column.setFilterValue(filterValues.length ? filterValues : undefined);
|
||||
} else {
|
||||
column.setFilterValue(isSelected ? undefined : [option.value]);
|
||||
setOpen(false);
|
||||
}
|
||||
},
|
||||
[column, multiple, selectedValues],
|
||||
);
|
||||
|
||||
const onReset = React.useCallback(
|
||||
(event?: React.MouseEvent) => {
|
||||
event?.stopPropagation();
|
||||
column?.setFilterValue(undefined);
|
||||
},
|
||||
[column],
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="border-dashed">
|
||||
{selectedValues.size > 0 ? (
|
||||
<div
|
||||
role="button"
|
||||
aria-label={`Clear ${title} filter`}
|
||||
tabIndex={0}
|
||||
onClick={onReset}
|
||||
className="focus-visible:ring-ring rounded-sm opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-1 focus-visible:outline-none"
|
||||
>
|
||||
<Icons.XCircle />
|
||||
</div>
|
||||
) : (
|
||||
<Icons.PlusCircle />
|
||||
)}
|
||||
{title}
|
||||
{selectedValues.size > 0 && (
|
||||
<>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-0.5 data-[orientation=vertical]:h-4"
|
||||
/>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="rounded-sm px-1 font-normal lg:hidden"
|
||||
>
|
||||
{selectedValues.size}
|
||||
</Badge>
|
||||
<div className="hidden items-center gap-1 lg:flex">
|
||||
{selectedValues.size > 2 ? (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="rounded-sm px-1 font-normal"
|
||||
>
|
||||
{selectedValues.size} {t("selected")}
|
||||
</Badge>
|
||||
) : (
|
||||
options
|
||||
.filter((option) => selectedValues.has(option.value))
|
||||
.map((option) => (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
key={option.value}
|
||||
className="rounded-sm px-1 font-normal"
|
||||
>
|
||||
{option.label}
|
||||
</Badge>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[12.5rem] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder={title} />
|
||||
<CommandList className="max-h-full">
|
||||
<CommandEmpty>{t("noResults")}</CommandEmpty>
|
||||
<CommandGroup className="max-h-[18.75rem] overflow-x-hidden overflow-y-auto">
|
||||
{options.map((option) => {
|
||||
const isSelected = selectedValues.has(option.value);
|
||||
|
||||
return (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
onSelect={() => onItemSelect(option, isSelected)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"border-primary flex size-4 items-center justify-center rounded-[4px] border",
|
||||
isSelected
|
||||
? "bg-primary"
|
||||
: "opacity-50 [&_svg]:invisible",
|
||||
)}
|
||||
>
|
||||
<Icons.Check className="text-primary-foreground size-4" />
|
||||
</div>
|
||||
{option.icon && <option.icon />}
|
||||
<span className="truncate">{option.label}</span>
|
||||
{option.count && (
|
||||
<span className="ml-auto font-mono text-xs">
|
||||
{option.count}
|
||||
</span>
|
||||
)}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
{selectedValues.size > 0 && (
|
||||
<>
|
||||
<CommandSeparator />
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
onSelect={() => onReset()}
|
||||
className="justify-center text-center"
|
||||
>
|
||||
{t("clear")}
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { useTranslation } from "@turbostarter/i18n";
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import type { Table } from "@tanstack/react-table";
|
||||
|
||||
import { Button } from "#components/button";
|
||||
import { Icons } from "#components/icons";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "#components/select";
|
||||
|
||||
interface DataTablePaginationProps<TData> extends React.ComponentProps<"div"> {
|
||||
table: Table<TData>;
|
||||
pageSizeOptions?: number[];
|
||||
}
|
||||
|
||||
export function DataTablePagination<TData>({
|
||||
table,
|
||||
pageSizeOptions = [10, 20, 30, 40, 50],
|
||||
className,
|
||||
...props
|
||||
}: DataTablePaginationProps<TData>) {
|
||||
const { t } = useTranslation("common");
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full flex-col-reverse items-center justify-between gap-4 overflow-auto p-1 sm:flex-row sm:gap-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{table.options.enableRowSelection && (
|
||||
<div className="text-muted-foreground flex-1 text-sm whitespace-nowrap">
|
||||
{t("rowsSelected", {
|
||||
selected: table.getFilteredSelectedRowModel().rows.length,
|
||||
total: table.getFilteredRowModel().rows.length,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col-reverse items-center gap-4 sm:ml-auto sm:flex-row sm:gap-6 lg:gap-8">
|
||||
<div className="flex items-center space-x-2">
|
||||
<p className="text-sm font-medium whitespace-nowrap">
|
||||
{t("rowsPerPage")}
|
||||
</p>
|
||||
<Select
|
||||
value={`${table.getState().pagination.pageSize}`}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(Number(value));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-[4.5rem] [&[data-size]]:h-8">
|
||||
<SelectValue placeholder={table.getState().pagination.pageSize} />
|
||||
</SelectTrigger>
|
||||
<SelectContent side="top">
|
||||
{pageSizeOptions.map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={`${pageSize}`}>
|
||||
{pageSize}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex items-center justify-center text-sm font-medium">
|
||||
{t("pageOf", {
|
||||
page: table.getState().pagination.pageIndex + 1,
|
||||
total: table.getPageCount() || 1,
|
||||
})}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
aria-label={t("first")}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="hidden size-8 lg:flex"
|
||||
onClick={() => table.setPageIndex(0)}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<Icons.ChevronsLeft className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={t("previous")}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-8"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<Icons.ChevronLeft className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={t("next")}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-8"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<Icons.ChevronRight className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
aria-label={t("last")}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="hidden size-8 lg:flex"
|
||||
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<Icons.ChevronsRight className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import { Skeleton } from "#components/skeleton";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "#components/table";
|
||||
|
||||
interface DataTableSkeletonProps extends React.ComponentProps<"div"> {
|
||||
columnCount: number;
|
||||
rowCount?: number;
|
||||
filterCount?: number;
|
||||
cellWidths?: string[];
|
||||
withViewOptions?: boolean;
|
||||
withPagination?: boolean;
|
||||
shrinkZero?: boolean;
|
||||
}
|
||||
|
||||
export function DataTableSkeleton({
|
||||
columnCount,
|
||||
rowCount = 10,
|
||||
filterCount = 0,
|
||||
cellWidths = ["auto"],
|
||||
withViewOptions = true,
|
||||
withPagination = true,
|
||||
shrinkZero = false,
|
||||
className,
|
||||
...props
|
||||
}: DataTableSkeletonProps) {
|
||||
const cozyCellWidths = Array.from(
|
||||
{ length: columnCount },
|
||||
(_, index) => cellWidths[index % cellWidths.length] ?? "auto",
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn("flex w-full flex-col gap-2.5 overflow-auto", className)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between gap-2 overflow-auto">
|
||||
<div className="flex flex-1 items-center gap-2">
|
||||
{filterCount > 0
|
||||
? Array.from({ length: filterCount }).map((_, i) => (
|
||||
<Skeleton key={i} className="h-8 w-28 border-dashed" />
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
{withViewOptions ? (
|
||||
<Skeleton className="ml-auto hidden h-8 w-28 lg:flex" />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{Array.from({ length: 1 }).map((_, i) => (
|
||||
<TableRow key={i} className="hover:bg-transparent">
|
||||
{Array.from({ length: columnCount }).map((_, j) => (
|
||||
<TableHead
|
||||
key={j}
|
||||
style={{
|
||||
width: cozyCellWidths[j],
|
||||
minWidth: shrinkZero ? cozyCellWidths[j] : "auto",
|
||||
}}
|
||||
>
|
||||
<Skeleton className="my-1 h-7 w-full" />
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{Array.from({ length: rowCount }).map((_, i) => (
|
||||
<TableRow key={i} className="hover:bg-transparent">
|
||||
{Array.from({ length: columnCount }).map((_, j) => (
|
||||
<TableCell
|
||||
key={j}
|
||||
style={{
|
||||
width: cozyCellWidths[j],
|
||||
minWidth: shrinkZero ? cozyCellWidths[j] : "auto",
|
||||
}}
|
||||
>
|
||||
<Skeleton className="my-1 h-7 w-full" />
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
{withPagination ? (
|
||||
<div className="flex w-full items-center justify-between gap-4 overflow-auto p-1 sm:gap-8">
|
||||
<Skeleton className="h-8 w-40 shrink-0" />
|
||||
<div className="flex items-center gap-4 sm:gap-6 lg:gap-8">
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="h-8 w-24" />
|
||||
<Skeleton className="h-8 w-[4.5rem]" />
|
||||
</div>
|
||||
<div className="flex items-center justify-center text-sm font-medium">
|
||||
<Skeleton className="h-8 w-20" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Skeleton className="hidden size-8 lg:block" />
|
||||
<Skeleton className="size-8" />
|
||||
<Skeleton className="size-8" />
|
||||
<Skeleton className="hidden size-8 lg:block" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import { useTranslation } from "@turbostarter/i18n";
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import type { Column } from "@tanstack/react-table";
|
||||
|
||||
import { Button } from "#components/button";
|
||||
import { Icons } from "#components/icons";
|
||||
import { Input } from "#components/input";
|
||||
import { Label } from "#components/label";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "#components/popover";
|
||||
import { Separator } from "#components/separator";
|
||||
import { Slider } from "#components/slider";
|
||||
|
||||
interface Range {
|
||||
min: number;
|
||||
max: number;
|
||||
}
|
||||
|
||||
type RangeValue = [number, number];
|
||||
|
||||
function getIsValidRange(value: unknown): value is RangeValue {
|
||||
return (
|
||||
Array.isArray(value) &&
|
||||
value.length === 2 &&
|
||||
typeof value[0] === "number" &&
|
||||
typeof value[1] === "number"
|
||||
);
|
||||
}
|
||||
|
||||
function parseValuesAsNumbers(value: unknown): RangeValue | undefined {
|
||||
if (
|
||||
Array.isArray(value) &&
|
||||
value.length === 2 &&
|
||||
value.every(
|
||||
(v) =>
|
||||
(typeof v === "string" || typeof v === "number") && !Number.isNaN(v),
|
||||
)
|
||||
) {
|
||||
return [Number(value[0]), Number(value[1])];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
interface DataTableSliderFilterProps<TData> {
|
||||
column: Column<TData, unknown>;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export function DataTableSliderFilter<TData>({
|
||||
column,
|
||||
title,
|
||||
}: DataTableSliderFilterProps<TData>) {
|
||||
const { t } = useTranslation("common");
|
||||
const id = React.useId();
|
||||
|
||||
const columnFilterValue = parseValuesAsNumbers(column.getFilterValue());
|
||||
|
||||
const defaultRange =
|
||||
column.columnDef.meta &&
|
||||
"range" in column.columnDef.meta &&
|
||||
typeof column.columnDef.meta.range === "object"
|
||||
? column.columnDef.meta.range
|
||||
: undefined;
|
||||
const unit =
|
||||
column.columnDef.meta &&
|
||||
"unit" in column.columnDef.meta &&
|
||||
typeof column.columnDef.meta.unit === "string"
|
||||
? column.columnDef.meta.unit
|
||||
: undefined;
|
||||
|
||||
const { min, max, step } = React.useMemo<Range & { step: number }>(() => {
|
||||
let minValue = 0;
|
||||
let maxValue = 100;
|
||||
|
||||
if (defaultRange && getIsValidRange(defaultRange)) {
|
||||
[minValue, maxValue] = defaultRange;
|
||||
} else {
|
||||
const values = column.getFacetedMinMaxValues();
|
||||
if (values && Array.isArray(values)) {
|
||||
const [facetMinValue, facetMaxValue] = values;
|
||||
if (
|
||||
typeof facetMinValue === "number" &&
|
||||
typeof facetMaxValue === "number"
|
||||
) {
|
||||
minValue = facetMinValue;
|
||||
maxValue = facetMaxValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rangeSize = maxValue - minValue;
|
||||
const step =
|
||||
rangeSize <= 20
|
||||
? 1
|
||||
: rangeSize <= 100
|
||||
? Math.ceil(rangeSize / 20)
|
||||
: Math.ceil(rangeSize / 50);
|
||||
|
||||
return { min: minValue, max: maxValue, step };
|
||||
}, [column, defaultRange]);
|
||||
|
||||
const range = React.useMemo((): RangeValue => {
|
||||
return columnFilterValue ?? [min, max];
|
||||
}, [columnFilterValue, min, max]);
|
||||
|
||||
const formatValue = React.useCallback((value: number) => {
|
||||
return value.toLocaleString(undefined, { maximumFractionDigits: 0 });
|
||||
}, []);
|
||||
|
||||
const onFromInputChange = React.useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const numValue = Number(event.target.value);
|
||||
if (!Number.isNaN(numValue) && numValue >= min && numValue <= range[1]) {
|
||||
column.setFilterValue([numValue, range[1]]);
|
||||
}
|
||||
},
|
||||
[column, min, range],
|
||||
);
|
||||
|
||||
const onToInputChange = React.useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const numValue = Number(event.target.value);
|
||||
if (!Number.isNaN(numValue) && numValue <= max && numValue >= range[0]) {
|
||||
column.setFilterValue([range[0], numValue]);
|
||||
}
|
||||
},
|
||||
[column, max, range],
|
||||
);
|
||||
|
||||
const onSliderValueChange = React.useCallback(
|
||||
(value: RangeValue) => {
|
||||
if (Array.isArray(value)) {
|
||||
column.setFilterValue(value);
|
||||
}
|
||||
},
|
||||
[column],
|
||||
);
|
||||
|
||||
const onReset = React.useCallback(
|
||||
(event: React.MouseEvent) => {
|
||||
if (event.target instanceof HTMLDivElement) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
column.setFilterValue(undefined);
|
||||
},
|
||||
[column],
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="border-dashed">
|
||||
{columnFilterValue ? (
|
||||
<div
|
||||
role="button"
|
||||
aria-label={`Clear ${title} filter`}
|
||||
tabIndex={0}
|
||||
className="focus-visible:ring-ring rounded-sm opacity-70 transition-opacity hover:opacity-100 focus-visible:ring-1 focus-visible:outline-none"
|
||||
onClick={onReset}
|
||||
>
|
||||
<Icons.XCircle />
|
||||
</div>
|
||||
) : (
|
||||
<Icons.PlusCircle />
|
||||
)}
|
||||
<span>{title}</span>
|
||||
{columnFilterValue ? (
|
||||
<>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-0.5 data-[orientation=vertical]:h-4"
|
||||
/>
|
||||
{formatValue(columnFilterValue[0])} -{" "}
|
||||
{formatValue(columnFilterValue[1])}
|
||||
{unit ? ` ${unit}` : ""}
|
||||
</>
|
||||
) : null}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="start" className="flex w-auto flex-col gap-4">
|
||||
<div className="flex flex-col gap-3">
|
||||
<p className="leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
{title}
|
||||
</p>
|
||||
<div className="flex items-center gap-4">
|
||||
<Label htmlFor={`${id}-from`} className="sr-only">
|
||||
{t("from")}
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id={`${id}-from`}
|
||||
type="number"
|
||||
aria-valuemin={min}
|
||||
aria-valuemax={max}
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
placeholder={min.toString()}
|
||||
min={min}
|
||||
max={max}
|
||||
value={range[0].toString()}
|
||||
onChange={onFromInputChange}
|
||||
className={cn("h-8 w-24", unit && "pr-8")}
|
||||
/>
|
||||
{unit && (
|
||||
<span className="bg-accent text-muted-foreground absolute top-0 right-0 bottom-0 flex items-center rounded-r-md px-2 text-sm">
|
||||
{unit}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Label htmlFor={`${id}-to`} className="sr-only">
|
||||
{t("to")}
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
id={`${id}-to`}
|
||||
type="number"
|
||||
aria-valuemin={min}
|
||||
aria-valuemax={max}
|
||||
inputMode="numeric"
|
||||
pattern="[0-9]*"
|
||||
placeholder={max.toString()}
|
||||
min={min}
|
||||
max={max}
|
||||
value={range[1].toString()}
|
||||
onChange={onToInputChange}
|
||||
className={cn("h-8 w-24", unit && "pr-8")}
|
||||
/>
|
||||
{unit && (
|
||||
<span className="bg-accent text-muted-foreground absolute top-0 right-0 bottom-0 flex items-center rounded-r-md px-2 text-sm">
|
||||
{unit}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Label htmlFor={`${id}-slider`} className="sr-only">
|
||||
{title} {t("slider")}
|
||||
</Label>
|
||||
<Slider
|
||||
id={`${id}-slider`}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={range}
|
||||
onValueChange={onSliderValueChange}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
aria-label={`${t("clear")} ${title}`}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onReset}
|
||||
>
|
||||
{t("clear")}
|
||||
</Button>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
224
packages/ui/web/src/components/data-table/data-table-toolbar.tsx
Normal file
@@ -0,0 +1,224 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useTranslation } from "@turbostarter/i18n";
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import { DataTableDateFilter } from "./data-table-date-filter";
|
||||
import { DataTableFacetedFilter } from "./data-table-faceted-filter";
|
||||
import { DataTableSliderFilter } from "./data-table-slider-filter";
|
||||
import { DataTableViewOptions } from "./data-table-view-options";
|
||||
|
||||
import type { Icon } from "#components/icons";
|
||||
import type { Column, Table } from "@tanstack/react-table";
|
||||
|
||||
import { Button } from "#components/button";
|
||||
import { Icons } from "#components/icons";
|
||||
import { Input } from "#components/input";
|
||||
|
||||
interface DataTableToolbarProps<TData> extends React.ComponentProps<"div"> {
|
||||
table: Table<TData>;
|
||||
}
|
||||
|
||||
export function DataTableToolbar<TData>({
|
||||
table,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: DataTableToolbarProps<TData>) {
|
||||
const { t } = useTranslation("common");
|
||||
const isFiltered = table.getState().columnFilters.length > 0;
|
||||
|
||||
const columns = React.useMemo(
|
||||
() => table.getAllColumns().filter((column) => column.getCanFilter()),
|
||||
[table],
|
||||
);
|
||||
|
||||
const onReset = React.useCallback(() => {
|
||||
table.resetColumnFilters();
|
||||
}, [table]);
|
||||
|
||||
return (
|
||||
<div
|
||||
role="toolbar"
|
||||
aria-orientation="horizontal"
|
||||
className={cn("flex w-full items-start justify-between gap-2", className)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex flex-1 flex-wrap items-center gap-2">
|
||||
{columns.map((column) => (
|
||||
<DataTableToolbarFilter key={column.id} column={column} />
|
||||
))}
|
||||
{isFiltered && (
|
||||
<Button
|
||||
aria-label="Reset filters"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-dashed"
|
||||
onClick={onReset}
|
||||
>
|
||||
<Icons.X />
|
||||
{t("reset")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{children}
|
||||
<DataTableViewOptions table={table} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
interface DataTableToolbarFilterProps<TData> {
|
||||
column: Column<TData>;
|
||||
}
|
||||
|
||||
const optionSchema = z
|
||||
.object({
|
||||
label: z.string(),
|
||||
value: z.any(),
|
||||
count: z.number().optional(),
|
||||
icon: z.unknown().optional(),
|
||||
})
|
||||
.transform((opt) => {
|
||||
let parsedIcon: Icon | undefined;
|
||||
if (typeof opt.icon === "string" && opt.icon in Icons) {
|
||||
parsedIcon = Icons[opt.icon as keyof typeof Icons] as Icon;
|
||||
} else if (typeof opt.icon === "function") {
|
||||
parsedIcon = opt.icon as Icon;
|
||||
}
|
||||
return { ...opt, icon: parsedIcon };
|
||||
});
|
||||
|
||||
const metaSchema = z.object({
|
||||
variant: z
|
||||
.enum([
|
||||
"text",
|
||||
"number",
|
||||
"range",
|
||||
"date",
|
||||
"dateRange",
|
||||
"select",
|
||||
"multiSelect",
|
||||
])
|
||||
.optional(),
|
||||
label: z.string().optional(),
|
||||
placeholder: z.string().optional(),
|
||||
unit: z.string().optional(),
|
||||
range: z.tuple([z.number(), z.number()]).optional(),
|
||||
options: z.array(optionSchema).optional(),
|
||||
});
|
||||
|
||||
function parseMeta(meta: unknown): {
|
||||
variant?:
|
||||
| "text"
|
||||
| "number"
|
||||
| "range"
|
||||
| "date"
|
||||
| "dateRange"
|
||||
| "select"
|
||||
| "multiSelect";
|
||||
label?: string;
|
||||
placeholder?: string;
|
||||
unit?: string;
|
||||
options: { label: string; value: string; count?: number; icon?: Icon }[];
|
||||
} {
|
||||
const result = metaSchema.safeParse(meta ?? {});
|
||||
if (!result.success) {
|
||||
return { options: [] };
|
||||
}
|
||||
const { variant, label, placeholder, unit, options } = result.data;
|
||||
return {
|
||||
variant,
|
||||
label,
|
||||
placeholder,
|
||||
unit,
|
||||
options: options ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
function DataTableToolbarFilter<TData>({
|
||||
column,
|
||||
}: DataTableToolbarFilterProps<TData>) {
|
||||
{
|
||||
const onFilterRender = React.useCallback(() => {
|
||||
const { variant, label, placeholder, unit, options } = parseMeta(
|
||||
column.columnDef.meta,
|
||||
);
|
||||
|
||||
if (!variant) return null;
|
||||
|
||||
switch (variant) {
|
||||
case "text":
|
||||
return (
|
||||
<Input
|
||||
placeholder={placeholder ?? label}
|
||||
value={(() => {
|
||||
const value: unknown = column.getFilterValue();
|
||||
return typeof value === "string" ? value : "";
|
||||
})()}
|
||||
onChange={(event) => column.setFilterValue(event.target.value)}
|
||||
className="h-9 w-52 lg:w-72"
|
||||
/>
|
||||
);
|
||||
|
||||
case "number":
|
||||
return (
|
||||
<div className="relative">
|
||||
<Input
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
placeholder={placeholder ?? label}
|
||||
value={(() => {
|
||||
const value: unknown = column.getFilterValue();
|
||||
return typeof value === "number" || typeof value === "string"
|
||||
? String(value)
|
||||
: "";
|
||||
})()}
|
||||
onChange={(event) => column.setFilterValue(event.target.value)}
|
||||
className={cn("h-8 w-[120px]", unit ? "pr-8" : undefined)}
|
||||
/>
|
||||
{unit && (
|
||||
<span className="bg-accent text-muted-foreground absolute top-0 right-0 bottom-0 flex items-center rounded-r-md px-2 text-sm">
|
||||
{unit}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
case "range":
|
||||
return (
|
||||
<DataTableSliderFilter column={column} title={label ?? column.id} />
|
||||
);
|
||||
|
||||
case "date":
|
||||
case "dateRange":
|
||||
return (
|
||||
<DataTableDateFilter
|
||||
column={column}
|
||||
title={label ?? column.id}
|
||||
multiple={variant === "dateRange"}
|
||||
/>
|
||||
);
|
||||
|
||||
case "select":
|
||||
case "multiSelect":
|
||||
return (
|
||||
<DataTableFacetedFilter
|
||||
column={column}
|
||||
title={label ?? column.id}
|
||||
options={options}
|
||||
multiple={variant === "multiSelect"}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}, [column]);
|
||||
|
||||
return onFilterRender();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
|
||||
import { useTranslation } from "@turbostarter/i18n";
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import type { Table } from "@tanstack/react-table";
|
||||
|
||||
import { Button } from "#components/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "#components/command";
|
||||
import { Icons } from "#components/icons";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "#components/popover";
|
||||
|
||||
interface DataTableViewOptionsProps<TData> {
|
||||
table: Table<TData>;
|
||||
}
|
||||
|
||||
export function DataTableViewOptions<TData>({
|
||||
table,
|
||||
}: DataTableViewOptionsProps<TData>) {
|
||||
const { t } = useTranslation("common");
|
||||
const columns = React.useMemo(
|
||||
() =>
|
||||
table
|
||||
.getAllColumns()
|
||||
.filter(
|
||||
(column) =>
|
||||
typeof column.accessorFn !== "undefined" && column.getCanHide(),
|
||||
),
|
||||
[table],
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
aria-label="Toggle columns"
|
||||
role="combobox"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="ml-auto hidden gap-1.5 lg:flex"
|
||||
>
|
||||
<Icons.Settings2 className="size-4" />
|
||||
{t("view")}
|
||||
<Icons.ChevronsUpDown className="ml-auto size-4 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="w-44 p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder={`${t("searchColumns")}...`} />
|
||||
<CommandList>
|
||||
<CommandEmpty>{t("noResults")}</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{columns.map((column) => (
|
||||
<CommandItem
|
||||
key={column.id}
|
||||
onSelect={() =>
|
||||
column.toggleVisibility(!column.getIsVisible())
|
||||
}
|
||||
>
|
||||
<span className="truncate">
|
||||
{column.columnDef.meta &&
|
||||
"label" in column.columnDef.meta &&
|
||||
typeof column.columnDef.meta.label === "string"
|
||||
? column.columnDef.meta.label
|
||||
: column.id}
|
||||
</span>
|
||||
<Icons.Check
|
||||
className={cn(
|
||||
"ml-auto size-4 shrink-0",
|
||||
column.getIsVisible() ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
88
packages/ui/web/src/components/data-table/data-table.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import { flexRender } from "@tanstack/react-table";
|
||||
|
||||
import { useTranslation } from "@turbostarter/i18n";
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
import { DataTablePagination } from "./data-table-pagination";
|
||||
|
||||
import type { Table as TanstackTable } from "@tanstack/react-table";
|
||||
import type * as React from "react";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "#components/table";
|
||||
|
||||
interface DataTableProps<TData> extends React.ComponentProps<"div"> {
|
||||
table: TanstackTable<TData>;
|
||||
}
|
||||
|
||||
export function DataTable<TData>({
|
||||
table,
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: DataTableProps<TData>) {
|
||||
const { t } = useTranslation("common");
|
||||
return (
|
||||
<div
|
||||
className={cn("flex w-full flex-col gap-2.5 overflow-auto", className)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<div className="overflow-hidden rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id} colSpan={header.colSpan}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{table.getRowModel().rows.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={table.getAllColumns().length}
|
||||
className="h-24 text-center"
|
||||
>
|
||||
{t("noResults")}.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<DataTablePagination table={table} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
148
packages/ui/web/src/components/dialog.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
"use client";
|
||||
|
||||
import { Dialog as DialogPrimitive } from "radix-ui";
|
||||
import * as React from "react";
|
||||
|
||||
import { useTranslation } from "@turbostarter/i18n";
|
||||
import { cn } from "@turbostarter/ui";
|
||||
import { Icons } from "@turbostarter/ui-web/icons";
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />;
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation("common");
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<Icons.X />
|
||||
<span className="sr-only">{t("close")}</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-1 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn(
|
||||
"text-lg leading-none font-semibold tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
};
|
||||
138
packages/ui/web/src/components/drawer.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Drawer as DrawerPrimitive } from "vaul";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
|
||||
function Drawer({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {
|
||||
return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
|
||||
}
|
||||
|
||||
function DrawerTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Trigger>) {
|
||||
return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
|
||||
}
|
||||
|
||||
function DrawerPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Portal>) {
|
||||
return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
|
||||
}
|
||||
|
||||
function DrawerClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Close>) {
|
||||
return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
|
||||
}
|
||||
|
||||
function DrawerOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Overlay>) {
|
||||
return (
|
||||
<DrawerPrimitive.Overlay
|
||||
data-slot="drawer-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Content>) {
|
||||
return (
|
||||
<DrawerPortal data-slot="drawer-portal">
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
data-slot="drawer-content"
|
||||
className={cn(
|
||||
"group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
|
||||
"data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
|
||||
"data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
|
||||
"data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
|
||||
"data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-header"
|
||||
className={cn(
|
||||
"flex flex-col items-start gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="drawer-footer"
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Title>) {
|
||||
return (
|
||||
<DrawerPrimitive.Title
|
||||
data-slot="drawer-title"
|
||||
className={cn(
|
||||
"text-foreground text-xl leading-tight font-semibold tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DrawerDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Description>) {
|
||||
return (
|
||||
<DrawerPrimitive.Description
|
||||
data-slot="drawer-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
};
|
||||
255
packages/ui/web/src/components/dropdown-menu.tsx
Normal file
@@ -0,0 +1,255 @@
|
||||
"use client";
|
||||
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@turbostarter/ui";
|
||||
import { Icons } from "@turbostarter/ui-web/icons";
|
||||
|
||||
function DropdownMenu({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
|
||||
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuContent({
|
||||
className,
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Content
|
||||
data-slot="dropdown-menu-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuItem({
|
||||
className,
|
||||
inset,
|
||||
variant = "default",
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
|
||||
inset?: boolean;
|
||||
variant?: "default" | "destructive";
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Item
|
||||
data-slot="dropdown-menu-item"
|
||||
data-inset={inset}
|
||||
data-variant={variant}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuCheckboxItem({
|
||||
className,
|
||||
children,
|
||||
checked,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Icons.Check className="size-4" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.CheckboxItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioGroup({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioGroup
|
||||
data-slot="dropdown-menu-radio-group"
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuRadioItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
||||
<DropdownMenuPrimitive.ItemIndicator>
|
||||
<Icons.Circle className="size-2 fill-current" />
|
||||
</DropdownMenuPrimitive.ItemIndicator>
|
||||
</span>
|
||||
{children}
|
||||
</DropdownMenuPrimitive.RadioItem>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuLabel({
|
||||
className,
|
||||
inset,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Label
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuShortcut({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"text-muted-foreground ml-auto text-xs tracking-widest",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSub({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
|
||||
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
|
||||
}
|
||||
|
||||
function DropdownMenuSubTrigger({
|
||||
className,
|
||||
inset,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
|
||||
inset?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubTrigger
|
||||
data-slot="dropdown-menu-sub-trigger"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<Icons.ChevronRight className="ml-auto size-4" />
|
||||
</DropdownMenuPrimitive.SubTrigger>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownMenuSubContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||
return (
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuSubContent,
|
||||
};
|
||||