mirror of
https://github.com/Nezumi-2711/next-gdrive-index.git
synced 2026-09-22 13:38:38 +00:00
Add theme customizer
This commit is contained in:
+28
-6
@@ -2,6 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import nProgress from "nprogress";
|
||||
import { useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { z } from "zod";
|
||||
@@ -38,8 +39,9 @@ import { CreateDownloadToken } from "./actions";
|
||||
|
||||
type Props = {
|
||||
data: z.infer<typeof Schema_File>;
|
||||
disabled?: boolean;
|
||||
};
|
||||
export default function FileGrid({ data }: Props) {
|
||||
export default function FileGrid({ data, disabled }: Props) {
|
||||
const pathname = usePathname();
|
||||
const filePath = useMemo<string>(() => {
|
||||
// const currentPath = pathname.startsWith("/e") ? pathname : `/e${pathname}`;
|
||||
@@ -120,7 +122,10 @@ export default function FileGrid({ data }: Props) {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end'>
|
||||
<DropdownMenuItem onClick={onCopy}>
|
||||
<DropdownMenuItem
|
||||
onClick={onCopy}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icon
|
||||
name='Link'
|
||||
className='mr-3'
|
||||
@@ -129,7 +134,10 @@ export default function FileGrid({ data }: Props) {
|
||||
Copy link
|
||||
</DropdownMenuItem>
|
||||
{data.mimeType.includes("folder") ? null : (
|
||||
<DropdownMenuItem onClick={onDownload}>
|
||||
<DropdownMenuItem
|
||||
onClick={onDownload}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icon
|
||||
name='Download'
|
||||
className='mr-3'
|
||||
@@ -167,7 +175,10 @@ export default function FileGrid({ data }: Props) {
|
||||
</DrawerHeader>
|
||||
<div className='grid gap-1.5 px-4'>
|
||||
<DrawerClose asChild>
|
||||
<Button onClick={onCopy}>
|
||||
<Button
|
||||
onClick={onCopy}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icon
|
||||
name='Link'
|
||||
className='mr-3'
|
||||
@@ -178,7 +189,10 @@ export default function FileGrid({ data }: Props) {
|
||||
</DrawerClose>
|
||||
{data.mimeType.includes("folder") ? null : (
|
||||
<DrawerClose asChild>
|
||||
<Button onClick={onDownload}>
|
||||
<Button
|
||||
onClick={onDownload}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icon
|
||||
name='Download'
|
||||
className='mr-3'
|
||||
@@ -199,6 +213,14 @@ export default function FileGrid({ data }: Props) {
|
||||
</Drawer>
|
||||
)}
|
||||
<Link
|
||||
onClick={async (e) => {
|
||||
if (disabled) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
nProgress.done(true);
|
||||
}
|
||||
}}
|
||||
href={filePath}
|
||||
className={cn(
|
||||
"h-full w-full",
|
||||
@@ -291,7 +313,7 @@ export default function FileGrid({ data }: Props) {
|
||||
>
|
||||
{data.mimeType.includes("folder")
|
||||
? "folder"
|
||||
: data.fileExtension}
|
||||
: data.fileExtension || "unknown"}
|
||||
</span>
|
||||
{!data.mimeType.includes("folder") && (
|
||||
<>
|
||||
|
||||
+28
-6
@@ -2,6 +2,7 @@
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import nProgress from "nprogress";
|
||||
import { useMemo, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { z } from "zod";
|
||||
@@ -38,8 +39,9 @@ import { CreateDownloadToken } from "./actions";
|
||||
|
||||
type Props = {
|
||||
data: z.infer<typeof Schema_File>;
|
||||
disabled?: boolean;
|
||||
};
|
||||
export default function FileList({ data }: Props) {
|
||||
export default function FileList({ data, disabled }: Props) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const filePath = useMemo<string>(() => {
|
||||
@@ -121,7 +123,10 @@ export default function FileList({ data }: Props) {
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end'>
|
||||
<DropdownMenuItem onClick={onCopy}>
|
||||
<DropdownMenuItem
|
||||
onClick={onCopy}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icon
|
||||
name='Link'
|
||||
className='mr-3'
|
||||
@@ -130,7 +135,10 @@ export default function FileList({ data }: Props) {
|
||||
Copy link
|
||||
</DropdownMenuItem>
|
||||
{data.mimeType.includes("folder") ? null : (
|
||||
<DropdownMenuItem onClick={onDownload}>
|
||||
<DropdownMenuItem
|
||||
onClick={onDownload}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icon
|
||||
name='Download'
|
||||
className='mr-3'
|
||||
@@ -169,7 +177,10 @@ export default function FileList({ data }: Props) {
|
||||
</DrawerHeader>
|
||||
<div className='grid gap-1.5 px-4'>
|
||||
<DrawerClose asChild>
|
||||
<Button onClick={onCopy}>
|
||||
<Button
|
||||
onClick={onCopy}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icon
|
||||
name='Link'
|
||||
className='mr-3'
|
||||
@@ -180,7 +191,10 @@ export default function FileList({ data }: Props) {
|
||||
</DrawerClose>
|
||||
{data.mimeType.includes("folder") ? null : (
|
||||
<DrawerClose asChild>
|
||||
<Button onClick={onDownload}>
|
||||
<Button
|
||||
onClick={onDownload}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Icon
|
||||
name='Download'
|
||||
className='mr-3'
|
||||
@@ -202,6 +216,14 @@ export default function FileList({ data }: Props) {
|
||||
)}
|
||||
</div>
|
||||
<Link
|
||||
onClick={async (e) => {
|
||||
if (disabled) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
nProgress.done(true);
|
||||
}
|
||||
}}
|
||||
href={filePath}
|
||||
className={cn(
|
||||
"relative",
|
||||
@@ -279,7 +301,7 @@ export default function FileList({ data }: Props) {
|
||||
>
|
||||
{data.mimeType.includes("folder")
|
||||
? "folder"
|
||||
: data.fileExtension}
|
||||
: data.fileExtension || "unknown"}
|
||||
</span>
|
||||
{!data.mimeType.includes("folder") && (
|
||||
<>
|
||||
|
||||
+1
-1
@@ -42,7 +42,7 @@ export default function Footer({ content }: Props) {
|
||||
{content}
|
||||
</ReactMarkdown>
|
||||
{isDev && (
|
||||
<div className='fixed bottom-0 z-[999] rounded-t-[var(--radius)] border border-border bg-primary px-3 py-1 text-xs text-primary-foreground'>
|
||||
<div className='fixed bottom-0 z-[999] rounded-t-[var(--radius)] border border-b-0 border-border bg-primary px-3 py-1 text-xs text-primary-foreground'>
|
||||
Dev Mode
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -23,7 +23,7 @@ import { Separator } from "~/components/ui/separator";
|
||||
import { decryptData } from "~/utils/encryptionHelper/hash";
|
||||
import { parseConfigFile } from "~/utils/parseConfigFile";
|
||||
|
||||
import ConfigInput from "./@form.input";
|
||||
import ConfigInput from "./@form.input-config";
|
||||
|
||||
type Props = {
|
||||
state: {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { Button } from "~/components/ui/button";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Separator } from "~/components/ui/separator";
|
||||
|
||||
import ConfigInput from "./@form.input";
|
||||
import ConfigInput from "./@form.input-config";
|
||||
|
||||
type Props = {
|
||||
state: {
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
"use client";
|
||||
|
||||
import { PopoverTrigger } from "@radix-ui/react-popover";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { HslColor, HslColorPicker } from "react-colorful";
|
||||
import { z } from "zod";
|
||||
import { Schema_Theme } from "~/schema";
|
||||
|
||||
import Icon from "~/components/Icon";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Label } from "~/components/ui/label";
|
||||
import { Popover, PopoverContent } from "~/components/ui/popover";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "~/components/ui/tooltip";
|
||||
|
||||
type Props = {
|
||||
key: keyof z.infer<typeof Schema_Theme>;
|
||||
title: string;
|
||||
description?: string;
|
||||
value: HslColor;
|
||||
onChange: (color: HslColor) => void;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
export default function ThemeInput(props: PropsWithChildren<Props>) {
|
||||
// const [value, setValue] = useState<HslColor>(props.value);
|
||||
// const [debouncedValue, setDebouncedValue] = useState<HslColor>(props.value);
|
||||
|
||||
// useEffect(() => {
|
||||
// setValue(props.value);
|
||||
|
||||
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// }, [props.value]);
|
||||
|
||||
// useEffect(() => {
|
||||
// const timeout = setTimeout(() => {
|
||||
// setDebouncedValue(value);
|
||||
// }, 250);
|
||||
// return () => clearTimeout(timeout);
|
||||
|
||||
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// }, [value]);
|
||||
|
||||
// useEffect(() => {
|
||||
// props.onChange(debouncedValue);
|
||||
|
||||
// // eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// }, [debouncedValue]);
|
||||
|
||||
return (
|
||||
<div
|
||||
id={props.key}
|
||||
slot={`input-${props.key}`}
|
||||
// className='flex w-full items-center justify-between gap-6'
|
||||
className='grid w-full grid-cols-3 gap-6'
|
||||
>
|
||||
<div
|
||||
slot='label'
|
||||
className='flex items-center gap-3'
|
||||
>
|
||||
<span className='text-base'>{props.title}</span>
|
||||
{props.description && (
|
||||
<Tooltip>
|
||||
<TooltipTrigger
|
||||
onClick={(e) => e.preventDefault()}
|
||||
className='cursor-default'
|
||||
>
|
||||
<Icon
|
||||
name='Info'
|
||||
size='1rem'
|
||||
className='text-muted-foreground'
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side='right'
|
||||
className='max-w-screen-sm'
|
||||
>
|
||||
<p className='max-w-screen-sm !whitespace-pre-wrap'>
|
||||
{props.description}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
slot='input'
|
||||
className='col-span-2 flex flex-grow items-center justify-end gap-3'
|
||||
>
|
||||
{props.children ? (
|
||||
props.children
|
||||
) : (
|
||||
<>
|
||||
<span className='flex-grow text-sm text-muted-foreground'>
|
||||
hsl({props.value.h}, {props.value.s}%, {props.value.l}%)
|
||||
</span>
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<div className='size-8 rounded-[var(--radius)] border border-border p-0.5'>
|
||||
<div
|
||||
className='size-full rounded-[calc(var(--radius)-0.125rem)]'
|
||||
style={{
|
||||
background: `hsl(${props.value.h}, ${props.value.s}%, ${props.value.l}%)`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className='flex gap-6'>
|
||||
<HslColorPicker
|
||||
color={props.value}
|
||||
onChange={props.onChange}
|
||||
/>
|
||||
<div className='flex flex-grow flex-col items-center justify-between'>
|
||||
<div className='flex flex-col gap-1.5'>
|
||||
<Label htmlFor={`${props.key}-h`}>Hue</Label>
|
||||
<Input
|
||||
id={`${props.key}-h`}
|
||||
name={`${props.key}-h`}
|
||||
type='number'
|
||||
value={props.value.h}
|
||||
min={0}
|
||||
max={360}
|
||||
onChange={(e) => {
|
||||
const value = props.value;
|
||||
props.onChange({
|
||||
h: Number(e.target.value),
|
||||
s: value.s,
|
||||
l: value.l,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-1.5'>
|
||||
<Label htmlFor={`${props.key}-s`}>Saturation</Label>
|
||||
<Input
|
||||
id={`${props.key}-s`}
|
||||
name={`${props.key}-s`}
|
||||
type='number'
|
||||
value={props.value.s}
|
||||
min={0}
|
||||
max={100}
|
||||
onChange={(e) => {
|
||||
const value = props.value;
|
||||
props.onChange({
|
||||
h: value.h,
|
||||
s: Number(e.target.value),
|
||||
l: value.l,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className='flex flex-col gap-1.5'>
|
||||
<Label htmlFor={`${props.key}-l`}>Lightness</Label>
|
||||
<Input
|
||||
id={`${props.key}-l`}
|
||||
name={`${props.key}-l`}
|
||||
type='number'
|
||||
value={props.value.l}
|
||||
min={0}
|
||||
max={100}
|
||||
onChange={(e) => {
|
||||
const value = props.value;
|
||||
props.onChange({
|
||||
h: value.h,
|
||||
s: value.s,
|
||||
l: Number(e.target.value),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import { parseConfigFile } from "~/utils/parseConfigFile";
|
||||
|
||||
import config from "~/config/gIndex.config";
|
||||
|
||||
import ConfigInput from "./@form.input";
|
||||
import ConfigInput from "./@form.input-config";
|
||||
|
||||
type Props = {
|
||||
state: {
|
||||
|
||||
@@ -0,0 +1,235 @@
|
||||
"use client";
|
||||
|
||||
import { z } from "zod";
|
||||
import { Schema_Theme, ThemeKeys } from "~/schema";
|
||||
|
||||
import { Input } from "~/components/ui/input";
|
||||
import { Separator } from "~/components/ui/separator";
|
||||
import { Slider } from "~/components/ui/slider";
|
||||
|
||||
import { parseThemeValue } from "~/utils/parseConfigFile";
|
||||
|
||||
import ThemeInput from "./@form.input-theme";
|
||||
|
||||
type Props = {
|
||||
currentTheme: "light" | "dark";
|
||||
state: {
|
||||
get: {
|
||||
light: z.input<typeof Schema_Theme>;
|
||||
dark: z.input<typeof Schema_Theme>;
|
||||
};
|
||||
set: (theme: "light" | "dark", key: ThemeKeys, value: string) => void;
|
||||
};
|
||||
};
|
||||
export default function ThemeForm({
|
||||
currentTheme,
|
||||
state: { get, set },
|
||||
}: Props) {
|
||||
return (
|
||||
<div className='flex flex-col gap-1.5'>
|
||||
<ThemeInput
|
||||
key='background'
|
||||
title='Background'
|
||||
value={parseThemeValue(get[currentTheme].background)}
|
||||
onChange={(val) => {
|
||||
set(currentTheme, "background", `${val.h} ${val.s} ${val.l}`);
|
||||
}}
|
||||
/>
|
||||
<ThemeInput
|
||||
key='foreground'
|
||||
title='Text'
|
||||
value={parseThemeValue(get[currentTheme].foreground)}
|
||||
onChange={(val) => {
|
||||
set(currentTheme, "foreground", `${val.h} ${val.s} ${val.l}`);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Separator className='my-1.5' />
|
||||
|
||||
<ThemeInput
|
||||
key='card'
|
||||
title='Card'
|
||||
value={parseThemeValue(get[currentTheme].card)}
|
||||
onChange={(val) => {
|
||||
set(currentTheme, "card", `${val.h} ${val.s} ${val.l}`);
|
||||
}}
|
||||
/>
|
||||
<ThemeInput
|
||||
key='card-foreground'
|
||||
title='Card Text'
|
||||
value={parseThemeValue(get[currentTheme]["card-foreground"])}
|
||||
onChange={(val) => {
|
||||
set(currentTheme, "card-foreground", `${val.h} ${val.s} ${val.l}`);
|
||||
}}
|
||||
/>
|
||||
|
||||
<ThemeInput
|
||||
key='popover'
|
||||
title='Popover'
|
||||
value={parseThemeValue(get[currentTheme].popover)}
|
||||
onChange={(val) => {
|
||||
set(currentTheme, "popover", `${val.h} ${val.s} ${val.l}`);
|
||||
}}
|
||||
/>
|
||||
<ThemeInput
|
||||
key='popover-foreground'
|
||||
title='Popover Text'
|
||||
value={parseThemeValue(get[currentTheme]["popover-foreground"])}
|
||||
onChange={(val) => {
|
||||
set(currentTheme, "popover-foreground", `${val.h} ${val.s} ${val.l}`);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Separator className='my-1.5' />
|
||||
|
||||
<ThemeInput
|
||||
key='primary'
|
||||
title='Primary'
|
||||
value={parseThemeValue(get[currentTheme].primary)}
|
||||
onChange={(val) => {
|
||||
set(currentTheme, "primary", `${val.h} ${val.s} ${val.l}`);
|
||||
}}
|
||||
/>
|
||||
<ThemeInput
|
||||
key='primary-foreground'
|
||||
title='Primary Text'
|
||||
value={parseThemeValue(get[currentTheme]["primary-foreground"])}
|
||||
onChange={(val) => {
|
||||
set(currentTheme, "primary-foreground", `${val.h} ${val.s} ${val.l}`);
|
||||
}}
|
||||
/>
|
||||
<ThemeInput
|
||||
key='secondary'
|
||||
title='Secondary'
|
||||
value={parseThemeValue(get[currentTheme].secondary)}
|
||||
onChange={(val) => {
|
||||
set(currentTheme, "secondary", `${val.h} ${val.s} ${val.l}`);
|
||||
}}
|
||||
/>
|
||||
<ThemeInput
|
||||
key='secondary-foreground'
|
||||
title='Secondary Text'
|
||||
value={parseThemeValue(get[currentTheme]["secondary-foreground"])}
|
||||
onChange={(val) => {
|
||||
set(
|
||||
currentTheme,
|
||||
"secondary-foreground",
|
||||
`${val.h} ${val.s} ${val.l}`,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<ThemeInput
|
||||
key='accent'
|
||||
title='Accent'
|
||||
value={parseThemeValue(get[currentTheme].accent)}
|
||||
onChange={(val) => {
|
||||
set(currentTheme, "accent", `${val.h} ${val.s} ${val.l}`);
|
||||
}}
|
||||
/>
|
||||
<ThemeInput
|
||||
key='accent-foreground'
|
||||
title='Accent Text'
|
||||
value={parseThemeValue(get[currentTheme]["accent-foreground"])}
|
||||
onChange={(val) => {
|
||||
set(currentTheme, "accent-foreground", `${val.h} ${val.s} ${val.l}`);
|
||||
}}
|
||||
/>
|
||||
<ThemeInput
|
||||
key='muted'
|
||||
title='Muted'
|
||||
value={parseThemeValue(get[currentTheme].muted)}
|
||||
onChange={(val) => {
|
||||
set(currentTheme, "muted", `${val.h} ${val.s} ${val.l}`);
|
||||
}}
|
||||
/>
|
||||
<ThemeInput
|
||||
key='muted-foreground'
|
||||
title='Muted Text'
|
||||
value={parseThemeValue(get[currentTheme]["muted-foreground"])}
|
||||
onChange={(val) => {
|
||||
set(currentTheme, "muted-foreground", `${val.h} ${val.s} ${val.l}`);
|
||||
}}
|
||||
/>
|
||||
<ThemeInput
|
||||
key='destructive'
|
||||
title='Destructive'
|
||||
value={parseThemeValue(get[currentTheme].destructive)}
|
||||
onChange={(val) => {
|
||||
set(currentTheme, "destructive", `${val.h} ${val.s} ${val.l}`);
|
||||
}}
|
||||
/>
|
||||
<ThemeInput
|
||||
key='destructive-foreground'
|
||||
title='Destructive Text'
|
||||
value={parseThemeValue(get[currentTheme]["destructive-foreground"])}
|
||||
onChange={(val) => {
|
||||
set(
|
||||
currentTheme,
|
||||
"destructive-foreground",
|
||||
`${val.h} ${val.s} ${val.l}`,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Separator className='my-1.5' />
|
||||
|
||||
<ThemeInput
|
||||
key='border'
|
||||
title='Element Border'
|
||||
value={parseThemeValue(get[currentTheme].border)}
|
||||
onChange={(val) => {
|
||||
set(currentTheme, "border", `${val.h} ${val.s} ${val.l}`);
|
||||
}}
|
||||
/>
|
||||
<ThemeInput
|
||||
key='input'
|
||||
title='Input Border'
|
||||
value={parseThemeValue(get[currentTheme].input)}
|
||||
onChange={(val) => {
|
||||
set(currentTheme, "input", `${val.h} ${val.s} ${val.l}`);
|
||||
}}
|
||||
/>
|
||||
<ThemeInput
|
||||
key='ring'
|
||||
title='Focus Ring'
|
||||
value={parseThemeValue(get[currentTheme].ring)}
|
||||
onChange={(val) => {
|
||||
set(currentTheme, "ring", `${val.h} ${val.s} ${val.l}`);
|
||||
}}
|
||||
/>
|
||||
<ThemeInput
|
||||
key='radius'
|
||||
title='Roundness'
|
||||
value={{ h: 0, s: 0, l: 0 }}
|
||||
onChange={(val) => {}}
|
||||
>
|
||||
<div className='flex w-full items-center gap-3'>
|
||||
<Slider
|
||||
className='w-full'
|
||||
defaultValue={[parseFloat(get[currentTheme].radius) * 16 || 0]}
|
||||
min={0}
|
||||
max={24}
|
||||
value={[parseFloat(get[currentTheme].radius) * 16 || 0]}
|
||||
onValueChange={(val) => {
|
||||
const value = val[0] / 16;
|
||||
set("light", "radius", `${value}rem`);
|
||||
set("dark", "radius", `${value}rem`);
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
value={parseFloat(get[currentTheme].radius) * 16 || 0}
|
||||
onChange={(e) => {
|
||||
const value = parseFloat(e.currentTarget.value) / 16;
|
||||
set("light", "radius", `${value}rem`);
|
||||
set("dark", "radius", `${value}rem`);
|
||||
}}
|
||||
type='number'
|
||||
className='w-16 min-w-0 '
|
||||
min={0}
|
||||
max={24}
|
||||
/>
|
||||
</div>
|
||||
</ThemeInput>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
"use client";
|
||||
|
||||
import nProgress from "nprogress";
|
||||
import { CSSProperties, PropsWithChildren } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { z } from "zod";
|
||||
import { Schema_Theme } from "~/schema";
|
||||
|
||||
import FileGrid from "~/app/@file.grid";
|
||||
import FileList from "~/app/@file.list";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "~/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "~/components/ui/dialog";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerFooter,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "~/components/ui/drawer";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "~/components/ui/dropdown-menu";
|
||||
import { Input } from "~/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "~/components/ui/popover";
|
||||
import { Separator } from "~/components/ui/separator";
|
||||
import {
|
||||
Sheet,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "~/components/ui/sheet";
|
||||
import { Textarea } from "~/components/ui/textarea";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "~/components/ui/tooltip";
|
||||
|
||||
type Props = {
|
||||
theme: z.input<typeof Schema_Theme>;
|
||||
};
|
||||
export default function ThemePreview({ theme }: Props) {
|
||||
return (
|
||||
<div
|
||||
className='flex flex-col gap-3 rounded-[var(--radius)] border border-border bg-background p-3 text-foreground'
|
||||
style={
|
||||
{
|
||||
"--background": theme.background,
|
||||
"--foreground": theme.foreground,
|
||||
"--card": theme.card,
|
||||
"--card-foreground": theme["card-foreground"],
|
||||
"--popover": theme.popover,
|
||||
"--popover-foreground": theme["popover-foreground"],
|
||||
"--primary": theme.primary,
|
||||
"--primary-foreground": theme["primary-foreground"],
|
||||
"--secondary": theme.secondary,
|
||||
"--secondary-foreground": theme["secondary-foreground"],
|
||||
"--muted": theme.muted,
|
||||
"--muted-foreground": theme["muted-foreground"],
|
||||
"--accent": theme.accent,
|
||||
"--accent-foreground": theme["accent-foreground"],
|
||||
"--destructive": theme.destructive,
|
||||
"--destructive-foreground": theme["destructive-foreground"],
|
||||
"--border": theme.border,
|
||||
"--input": theme.input,
|
||||
"--ring": theme.ring,
|
||||
"--radius": theme.radius,
|
||||
} as CSSProperties
|
||||
}
|
||||
>
|
||||
<Wrapper title='Button'>
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={"default"}
|
||||
>
|
||||
Default
|
||||
</Button>
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={"secondary"}
|
||||
>
|
||||
Secondary
|
||||
</Button>
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={"destructive"}
|
||||
>
|
||||
Destructive
|
||||
</Button>
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={"outline"}
|
||||
>
|
||||
Outline
|
||||
</Button>
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={"ghost"}
|
||||
>
|
||||
Ghost
|
||||
</Button>
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={"link"}
|
||||
>
|
||||
Link
|
||||
</Button>
|
||||
</Wrapper>
|
||||
|
||||
<Wrapper title='Card'>
|
||||
<Card className='w-full'>
|
||||
<CardHeader>
|
||||
<CardTitle>Title</CardTitle>
|
||||
<CardDescription>Description</CardDescription>
|
||||
<Separator />
|
||||
</CardHeader>
|
||||
<CardContent>Content</CardContent>
|
||||
<CardFooter>Footer</CardFooter>
|
||||
</Card>
|
||||
</Wrapper>
|
||||
|
||||
<Wrapper title='Input'>
|
||||
<Input placeholder='Input' />
|
||||
<Textarea placeholder='Textarea' />
|
||||
</Wrapper>
|
||||
|
||||
<Wrapper title='Popover'>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={"outline"}
|
||||
>
|
||||
Hover me
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>Tooltip content</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={"outline"}
|
||||
>
|
||||
Click me
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<p>Popover content</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={"outline"}
|
||||
>
|
||||
Click me
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>Item 1</DropdownMenuItem>
|
||||
<DropdownMenuItem>Item 2</DropdownMenuItem>
|
||||
<DropdownMenuItem disabled>Item 3</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={"outline"}
|
||||
onClick={async () => {
|
||||
toast.loading("Loading...", {
|
||||
duration: 3000,
|
||||
});
|
||||
toast.success("Success", {
|
||||
duration: 3000,
|
||||
});
|
||||
toast.error("Error", {
|
||||
duration: 3000,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Show toast
|
||||
</Button>
|
||||
</Wrapper>
|
||||
|
||||
<Wrapper title='Overlay'>
|
||||
<Dialog>
|
||||
<DialogTrigger>
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={"outline"}
|
||||
>
|
||||
Open Dialog
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Title</DialogTitle>
|
||||
<DialogDescription>Description</DialogDescription>
|
||||
<Separator />
|
||||
</DialogHeader>
|
||||
<div>Content</div>
|
||||
<DialogFooter>
|
||||
<DialogClose>
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={"secondary"}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Drawer>
|
||||
<DrawerTrigger>
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={"outline"}
|
||||
>
|
||||
Open Drawer
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>Title</DrawerTitle>
|
||||
<DrawerDescription>Description</DrawerDescription>
|
||||
<Separator />
|
||||
</DrawerHeader>
|
||||
<div className='p-6'>Content</div>
|
||||
<DrawerFooter>
|
||||
<DrawerClose>
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={"secondary"}
|
||||
className='w-full'
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</DrawerClose>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<Sheet>
|
||||
<SheetTrigger>
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={"outline"}
|
||||
>
|
||||
Open Sheet
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Title</SheetTitle>
|
||||
<SheetDescription>Description</SheetDescription>
|
||||
<Separator />
|
||||
</SheetHeader>
|
||||
<div>Content</div>
|
||||
<SheetFooter>
|
||||
<SheetClose>
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={"secondary"}
|
||||
className='w-full'
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</SheetClose>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={"outline"}
|
||||
onClick={async () => {
|
||||
nProgress.start();
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
nProgress.done();
|
||||
}}
|
||||
>
|
||||
Test Loading
|
||||
</Button>
|
||||
</Wrapper>
|
||||
|
||||
<Wrapper title='File'>
|
||||
<div className='flex w-full flex-col gap-3'>
|
||||
<FileList
|
||||
data={{
|
||||
encryptedId: "encryptedId",
|
||||
name: "File Name",
|
||||
modifiedTime: "2022-01-01T00:00:00Z",
|
||||
mimeType: "text/plain",
|
||||
trashed: false,
|
||||
size: 1024 * 1024 * 100,
|
||||
}}
|
||||
disabled
|
||||
/>
|
||||
<div className='grid w-full'>
|
||||
<FileGrid
|
||||
data={{
|
||||
encryptedId: "encryptedId",
|
||||
name: "File Name",
|
||||
modifiedTime: "2022-01-01T00:00:00Z",
|
||||
mimeType: "text/plain",
|
||||
trashed: false,
|
||||
size: 1024 * 1024 * 100,
|
||||
}}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Wrapper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type WrapperProps = {
|
||||
title: string;
|
||||
};
|
||||
function Wrapper({ title, children }: PropsWithChildren<WrapperProps>) {
|
||||
return (
|
||||
<div className='flex w-full flex-col gap-1.5'>
|
||||
<h2 className='text-lg font-semibold'>{title}</h2>
|
||||
<div className={"flex w-full flex-wrap items-center gap-1.5"}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { z } from "zod";
|
||||
@@ -9,6 +10,8 @@ import {
|
||||
ConfigurationKeys,
|
||||
ConfigurationValue,
|
||||
Schema_App_Configuration,
|
||||
Schema_Theme,
|
||||
ThemeKeys,
|
||||
} from "~/schema";
|
||||
import { cn } from "~/utils";
|
||||
|
||||
@@ -16,15 +19,29 @@ import Markdown from "~/app/@markdown";
|
||||
import Icon from "~/components/Icon";
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "~/components/ui/dialog";
|
||||
import { Separator } from "~/components/ui/separator";
|
||||
import { Textarea } from "~/components/ui/textarea";
|
||||
|
||||
import { encryptData } from "~/utils/encryptionHelper/hash";
|
||||
import { parseThemeValue } from "~/utils/parseConfigFile";
|
||||
|
||||
import config from "~/config/gIndex.config";
|
||||
|
||||
import ApiConfig from "./@form.api-config";
|
||||
import EnvironmentConfig from "./@form.env-config";
|
||||
import SiteConfig from "./@form.site-config";
|
||||
import ThemeForm from "./@form.theme";
|
||||
import ThemePreview from "./@theme-preview";
|
||||
|
||||
export const getting_started = `Welcome to the deployment guide! This guide will help you to deploy the application to Vercel or similar services.
|
||||
|
||||
@@ -198,6 +215,23 @@ const initialConfiguration: z.input<typeof Schema_App_Configuration> = {
|
||||
},
|
||||
};
|
||||
|
||||
function downloadBlob(blob: Blob, filename: string) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = filename;
|
||||
|
||||
const afterClick = () => {
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
removeEventListener("click", afterClick);
|
||||
}, 100);
|
||||
};
|
||||
anchor.addEventListener("click", afterClick, false);
|
||||
|
||||
anchor.click();
|
||||
}
|
||||
|
||||
export function Configuration() {
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [configuration, setConfiguration] =
|
||||
@@ -282,23 +316,6 @@ export function Configuration() {
|
||||
);
|
||||
}
|
||||
|
||||
function downloadBlob(blob: Blob, filename: string) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const anchor = document.createElement("a");
|
||||
anchor.href = url;
|
||||
anchor.download = filename;
|
||||
|
||||
const afterClick = () => {
|
||||
setTimeout(() => {
|
||||
URL.revokeObjectURL(url);
|
||||
removeEventListener("click", afterClick);
|
||||
}, 100);
|
||||
};
|
||||
anchor.addEventListener("click", afterClick, false);
|
||||
|
||||
anchor.click();
|
||||
}
|
||||
|
||||
let envContent = Object.entries(configuration.environment)
|
||||
.map(([key, value]) => `${key}=${value}`)
|
||||
.join("\n");
|
||||
@@ -750,29 +767,589 @@ If you're migrating from previous version, you can load your old environment and
|
||||
}
|
||||
|
||||
export function CustomizeTheme() {
|
||||
const [colors, setColors] = useState({
|
||||
primary: { h: 200, s: 50, l: 50 },
|
||||
secondary: { h: 200, s: 50, l: 50 },
|
||||
const [initialTheme] = useState<{
|
||||
light: z.input<typeof Schema_Theme>;
|
||||
dark: z.input<typeof Schema_Theme>;
|
||||
}>({
|
||||
light: {
|
||||
"background": "0 0% 100%",
|
||||
"foreground": "0 0% 3.9%",
|
||||
"card": "0 0% 100%",
|
||||
"card-foreground": "0 0% 3.9%",
|
||||
"popover": "0 0% 100%",
|
||||
"popover-foreground": "0 0% 3.9%",
|
||||
"primary": "0 0% 9%",
|
||||
"primary-foreground": "0 0% 98%",
|
||||
"secondary": "0 0% 96.1%",
|
||||
"secondary-foreground": "0 0% 9%",
|
||||
"muted": "0 0% 96.1%",
|
||||
"muted-foreground": "0 0% 45.1%",
|
||||
"accent": "0 0% 96.1%",
|
||||
"accent-foreground": "0 0% 9%",
|
||||
"destructive": "0 84.2% 60.2%",
|
||||
"destructive-foreground": "0 0% 98%",
|
||||
"border": "0 0% 89.8%",
|
||||
"input": "0 0% 89.8%",
|
||||
"ring": "0 0% 89.8%",
|
||||
"radius": "0.5rem",
|
||||
},
|
||||
dark: {
|
||||
"background": "0 0% 3.9%",
|
||||
"foreground": "0 0% 98%",
|
||||
"card": "0 0% 3.9%",
|
||||
"card-foreground": "0 0% 98%",
|
||||
"popover": "0 0% 3.9%",
|
||||
"popover-foreground": "0 0% 98%",
|
||||
"primary": "0 0% 98%",
|
||||
"primary-foreground": "0 0% 9%",
|
||||
"secondary": "0 0% 14.9%",
|
||||
"secondary-foreground": "0 0% 98%",
|
||||
"muted": "0 0% 14.9%",
|
||||
"muted-foreground": "0 0% 63.9%",
|
||||
"accent": "0 0% 14.9%",
|
||||
"accent-foreground": "0 0% 98%",
|
||||
"destructive": "0 62.8% 30.6%",
|
||||
"destructive-foreground": "0 0% 98%",
|
||||
"border": "0 0% 14.9%",
|
||||
"input": "0 0% 14.9%",
|
||||
"ring": "0 0% 14.9%",
|
||||
"radius": "0.5rem",
|
||||
},
|
||||
});
|
||||
const [currentTheme, setCurrentTheme] = useState<"light" | "dark">("light");
|
||||
const [theme, setTheme] = useState<{
|
||||
light: z.input<typeof Schema_Theme>;
|
||||
dark: z.input<typeof Schema_Theme>;
|
||||
}>(initialTheme);
|
||||
|
||||
const [dialog, setDialog] = useState<boolean>(false);
|
||||
const [cssLoading, setCssLoading] = useState<boolean>(false);
|
||||
|
||||
function onThemeChange(
|
||||
theme: "light" | "dark",
|
||||
key: ThemeKeys,
|
||||
value: string,
|
||||
) {
|
||||
setTheme((prev) => ({
|
||||
...prev,
|
||||
[theme]: {
|
||||
...prev[theme],
|
||||
[key]: value,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
function onSelectedThemeReset(theme: "light" | "dark", key: ThemeKeys) {
|
||||
setTheme((prev) => ({
|
||||
...prev,
|
||||
[theme]: {
|
||||
...prev[theme],
|
||||
[key]: initialTheme[theme][key],
|
||||
},
|
||||
}));
|
||||
}
|
||||
function onThemeReset() {
|
||||
setTheme(initialTheme);
|
||||
}
|
||||
function isChanged(t: "light" | "dark", key: ThemeKeys): boolean {
|
||||
return t === "light"
|
||||
? theme.light[key] !== initialTheme.light[key]
|
||||
: theme.dark[key] !== initialTheme.dark[key];
|
||||
}
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className='pb-0'>
|
||||
<CardTitle
|
||||
className='text-3xl'
|
||||
id='theme'
|
||||
>
|
||||
Customize Theme
|
||||
</CardTitle>
|
||||
<div className='flex flex-col gap-3 tablet:flex-row tablet:items-center tablet:justify-between'>
|
||||
<CardTitle
|
||||
className='text-3xl'
|
||||
id='theme'
|
||||
>
|
||||
Customize Theme
|
||||
</CardTitle>
|
||||
|
||||
<div className='flex w-full items-center gap-3 tablet:w-fit'>
|
||||
<Button
|
||||
size='sm'
|
||||
variant={"outline"}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
const fileInput = document.createElement("input");
|
||||
fileInput.type = "file";
|
||||
fileInput.accept = ".css";
|
||||
fileInput.onchange = async (fileEvent) => {
|
||||
const file = (fileEvent.target as HTMLInputElement)
|
||||
.files?.[0];
|
||||
if (!file) return toast.error("No file selected");
|
||||
const fileName = file.name;
|
||||
if (fileName !== "globals.css")
|
||||
return toast.error("Please select globals.css file");
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (read) => {
|
||||
const result = read.target?.result as string;
|
||||
if (!result) return toast.error("Failed to read file");
|
||||
const themeRegex =
|
||||
/\/\* Shadcn\/ui theme \*\/.*?\:root \{(.*?)\}.*?.dark \{(.*?) \}/gm;
|
||||
const themeMatch = themeRegex.exec(
|
||||
result.replace(/\r\n/g, ""),
|
||||
);
|
||||
if (!themeMatch)
|
||||
return toast.error(
|
||||
"Can't find theme, are you sure it's the right file?",
|
||||
);
|
||||
const lightTheme = themeMatch[1]
|
||||
.replace(/\s{2,4}/g, "\n")
|
||||
.split("\n")
|
||||
.map((v) => v.trim())
|
||||
.filter(
|
||||
(v) =>
|
||||
v.length &&
|
||||
(v.startsWith("/*") && v.endsWith("*/")
|
||||
? false
|
||||
: true),
|
||||
);
|
||||
const darkTheme = themeMatch[2]
|
||||
.replace(/\s{2,4}/g, "\n")
|
||||
.split("\n")
|
||||
.map((v) => v.trim())
|
||||
.filter(
|
||||
(v) =>
|
||||
v.length &&
|
||||
(v.startsWith("/*") && v.endsWith("*/")
|
||||
? false
|
||||
: true),
|
||||
);
|
||||
|
||||
// Loop through the theme and set it
|
||||
for (const key in lightTheme) {
|
||||
const [k, v] = lightTheme[key]
|
||||
.split(":")
|
||||
.map((v) => v.replace("--", "").trim());
|
||||
const hslRegex = /\d\s\d.*?\%\s\d.*?\%;/g;
|
||||
if (hslRegex.test(v)) {
|
||||
const color = parseThemeValue(v);
|
||||
onThemeChange(
|
||||
"light",
|
||||
k as ThemeKeys,
|
||||
`${color.h} ${color.s}% ${color.l}%`,
|
||||
);
|
||||
} else {
|
||||
onThemeChange("light", k as ThemeKeys, v);
|
||||
}
|
||||
}
|
||||
for (const key in darkTheme) {
|
||||
const [k, v] = darkTheme[key]
|
||||
.split(":")
|
||||
.map((v) => v.replace("--", "").trim());
|
||||
const hslRegex = /\d\s\d.*?\%\s\d.*?\%;/g;
|
||||
if (hslRegex.test(v)) {
|
||||
const color = parseThemeValue(v);
|
||||
onThemeChange(
|
||||
"dark",
|
||||
k as ThemeKeys,
|
||||
`${color.h} ${color.s}% ${color.l}%`,
|
||||
);
|
||||
} else {
|
||||
onThemeChange("dark", k as ThemeKeys, v);
|
||||
}
|
||||
}
|
||||
|
||||
fileInput.value = "";
|
||||
toast.success("Theme loaded successfully");
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
fileInput.click();
|
||||
} catch (error) {
|
||||
const e = error as Error;
|
||||
console.error(e);
|
||||
toast.error(e.message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Load CSS
|
||||
</Button>
|
||||
<Dialog
|
||||
open={dialog}
|
||||
onOpenChange={setDialog}
|
||||
>
|
||||
<DialogTrigger>
|
||||
<Button
|
||||
size='sm'
|
||||
variant={"outline"}
|
||||
>
|
||||
Paste Code
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Paste Theme</DialogTitle>
|
||||
<DialogDescription>
|
||||
Paste the CSS code from your theme file
|
||||
<br />
|
||||
<Link
|
||||
href={"https://ui.shadcn.com/themes"}
|
||||
target='_blank'
|
||||
className='text-blue-600 opacity-80 transition-all duration-300 hover:opacity-100 dark:text-blue-400'
|
||||
>
|
||||
Get themes from shadcn/ui
|
||||
</Link>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form
|
||||
className='space-y-3'
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
setCssLoading(true);
|
||||
|
||||
try {
|
||||
const formData = new FormData(
|
||||
e.target as HTMLFormElement,
|
||||
);
|
||||
const css = formData.get("css-paste") as string;
|
||||
if (!css) throw new Error("No CSS code provided");
|
||||
|
||||
const flatten = css.trim().replace(/\n/g, "");
|
||||
const themeRegex =
|
||||
/\@layer base.*?\:root \{(.*?)\}.*?.dark \{(.*?) \}/gm;
|
||||
const themeMatch = themeRegex.exec(
|
||||
flatten.replace(/\r\n/g, ""),
|
||||
);
|
||||
if (!themeMatch)
|
||||
throw new Error(
|
||||
"Unknown format, please refer to shadcn/ui theme",
|
||||
);
|
||||
|
||||
const lightTheme = themeMatch[1]
|
||||
.replace(/\s{2,4}/g, "\n")
|
||||
.split("\n")
|
||||
.map((v) => v.trim())
|
||||
.filter(
|
||||
(v) =>
|
||||
v.length &&
|
||||
(v.startsWith("/*") && v.endsWith("*/")
|
||||
? false
|
||||
: true),
|
||||
);
|
||||
const darkTheme = themeMatch[2]
|
||||
.replace(/\s{2,4}/g, "\n")
|
||||
.split("\n")
|
||||
.map((v) => v.trim())
|
||||
.filter(
|
||||
(v) =>
|
||||
v.length &&
|
||||
(v.startsWith("/*") && v.endsWith("*/")
|
||||
? false
|
||||
: true),
|
||||
);
|
||||
|
||||
for (const key in lightTheme) {
|
||||
const [k, v] = lightTheme[key]
|
||||
.split(":")
|
||||
.map((v) => v.replace("--", "").trim());
|
||||
const color = parseThemeValue(v);
|
||||
const hslRegex = /\d\s\d.*?\%\s\d.*?\%;/g;
|
||||
if (hslRegex.test(v)) {
|
||||
const color = parseThemeValue(v);
|
||||
onThemeChange(
|
||||
"light",
|
||||
k as ThemeKeys,
|
||||
`${color.h} ${color.s}% ${color.l}%`,
|
||||
);
|
||||
} else {
|
||||
onThemeChange("light", k as ThemeKeys, v);
|
||||
}
|
||||
}
|
||||
for (const key in darkTheme) {
|
||||
const [k, v] = darkTheme[key]
|
||||
.split(":")
|
||||
.map((v) => v.replace("--", "").trim());
|
||||
const color = parseThemeValue(v);
|
||||
const hslRegex = /\d\s\d.*?\%\s\d.*?\%;/g;
|
||||
if (hslRegex.test(v)) {
|
||||
const color = parseThemeValue(v);
|
||||
onThemeChange(
|
||||
"dark",
|
||||
k as ThemeKeys,
|
||||
`${color.h} ${color.s}% ${color.l}%`,
|
||||
);
|
||||
} else {
|
||||
onThemeChange("dark", k as ThemeKeys, v);
|
||||
}
|
||||
}
|
||||
|
||||
toast.success("Theme loaded successfully");
|
||||
setDialog(false);
|
||||
} catch (error) {
|
||||
const e = error as Error;
|
||||
console.error(e);
|
||||
toast.error(e.message);
|
||||
} finally {
|
||||
setCssLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Textarea
|
||||
id='css-paste'
|
||||
name='css-paste'
|
||||
className='w-full'
|
||||
rows={10}
|
||||
placeholder='Please refer to shadcn/ui theme for the theme code'
|
||||
/>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={"destructive"}
|
||||
type='reset'
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button
|
||||
size={"sm"}
|
||||
disabled={cssLoading}
|
||||
type='submit'
|
||||
>
|
||||
<div className='relative flex w-full items-center justify-center'>
|
||||
<span className='relative transition-all duration-300 ease-in-out'>
|
||||
Load Theme
|
||||
</span>
|
||||
<Icon
|
||||
name='LoaderCircle'
|
||||
className={cn(
|
||||
"animate-spin transition-all",
|
||||
cssLoading
|
||||
? "ml-1.5 size-4 opacity-100"
|
||||
: "ml-0 size-0 opacity-0",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Button
|
||||
size='sm'
|
||||
variant={"destructive"}
|
||||
onClick={onThemeReset}
|
||||
>
|
||||
Reset All
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Separator />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Markdown
|
||||
content={`#### Under Construction
|
||||
<div className='grid grid-cols-1 gap-6 py-3 tablet:grid-cols-2'>
|
||||
<div className='col-span-full flex w-full items-center'>
|
||||
<Button
|
||||
size='sm'
|
||||
variant={currentTheme === "light" ? "default" : "outline"}
|
||||
onClick={() => setCurrentTheme("light")}
|
||||
className='w-full rounded-r-none'
|
||||
>
|
||||
Light Theme
|
||||
</Button>
|
||||
<Button
|
||||
size='sm'
|
||||
variant={currentTheme === "dark" ? "default" : "outline"}
|
||||
onClick={() => setCurrentTheme("dark")}
|
||||
className='w-full rounded-l-none'
|
||||
>
|
||||
Dark Theme
|
||||
</Button>
|
||||
</div>
|
||||
<div className='flex flex-col gap-3'>
|
||||
<ThemeForm
|
||||
currentTheme={currentTheme}
|
||||
state={{
|
||||
get: theme,
|
||||
set: onThemeChange,
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
size={"sm"}
|
||||
variant={"secondary"}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
const otherTheme = currentTheme === "light" ? "dark" : "light";
|
||||
setTheme((prev) => ({
|
||||
...prev,
|
||||
[otherTheme]: {
|
||||
...prev[currentTheme],
|
||||
radius: "0.5rem",
|
||||
},
|
||||
}));
|
||||
toast.success(
|
||||
`${currentTheme
|
||||
.slice(0, 1)
|
||||
.toUpperCase()}${currentTheme.slice(
|
||||
1,
|
||||
)} theme copied to ${otherTheme} theme`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Copy to {currentTheme === "light" ? "Dark" : "Light"} Theme
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
While I'm working on the theme customization, you can check [shadcn UI theme](https://ui.shadcn.com/themes) for now`}
|
||||
view='markdown'
|
||||
/>
|
||||
<ThemePreview
|
||||
theme={currentTheme === "light" ? theme.light : theme.dark}
|
||||
/>
|
||||
|
||||
<Separator className='col-span-full' />
|
||||
|
||||
<div className='col-span-full flex w-full flex-col items-end justify-center gap-1.5'>
|
||||
<div className='flex items-center gap-3'>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
size={"sm"}
|
||||
onClick={async (e) => {
|
||||
try {
|
||||
const style = `@layer base {
|
||||
:root {
|
||||
${Object.entries(theme.light)
|
||||
.map(([key, value]) => ` --${key}: ${value};`.replace(";;", ";"))
|
||||
.join("\n")}
|
||||
}
|
||||
|
||||
.dark {
|
||||
${Object.entries(theme.dark)
|
||||
.map(([key, value]) => ` --${key}: ${value};`.replace(";;", ";"))
|
||||
.join("\n")}
|
||||
}
|
||||
}`;
|
||||
|
||||
await navigator.clipboard.writeText(style);
|
||||
toast.success("CSS code copied to clipboard");
|
||||
} catch (error) {
|
||||
const e = error as Error;
|
||||
console.error(e);
|
||||
toast.error(e.message);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Copy CSS Code
|
||||
</Button>
|
||||
<Button
|
||||
size={"sm"}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
toast.loading("Creating CSS file...", {
|
||||
id: "download-css",
|
||||
});
|
||||
try {
|
||||
const css = `@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* Shadcn/ui theme */
|
||||
@layer base {
|
||||
:root {
|
||||
${Object.entries(theme.light)
|
||||
.map(([key, value]) => ` --${key}: ${value};`.replace(";;", ";"))
|
||||
.join("\n")}
|
||||
}
|
||||
|
||||
.dark {
|
||||
${Object.entries(theme.dark)
|
||||
.map(([key, value]) => ` --${key}: ${value};`.replace(";;", ";"))
|
||||
.join("\n")}
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@layer base {
|
||||
:root {
|
||||
/* @apply text-[14px] tablet:text-[16px]; */
|
||||
@apply text-[100%];
|
||||
}
|
||||
* {
|
||||
@apply border-border;
|
||||
/* @apply outline outline-1 outline-red-500; */
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@apply h-1.5 w-1.5;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
@apply bg-primary/5;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply rounded-full bg-primary/25 hover:bg-primary/50;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
h1 {
|
||||
@apply scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl;
|
||||
}
|
||||
h2 {
|
||||
@apply scroll-m-20 text-3xl font-semibold tracking-tight first:mt-0;
|
||||
}
|
||||
h3 {
|
||||
@apply scroll-m-20 text-2xl font-semibold tracking-tight;
|
||||
}
|
||||
h4 {
|
||||
@apply scroll-m-20 text-xl font-semibold tracking-tight;
|
||||
}
|
||||
.paragraph {
|
||||
@apply leading-7 [&:not(:first-child)]:mt-6;
|
||||
}
|
||||
blockquote {
|
||||
@apply mt-6 border-l-2 pl-6 italic;
|
||||
}
|
||||
ul {
|
||||
@apply my-6 ml-6 list-disc [&>li]:mt-2;
|
||||
}
|
||||
.lead {
|
||||
@apply text-xl text-muted-foreground;
|
||||
}
|
||||
.large {
|
||||
@apply text-lg font-semibold;
|
||||
}
|
||||
.muted {
|
||||
@apply text-sm text-muted-foreground;
|
||||
}
|
||||
small {
|
||||
@apply text-sm font-medium leading-none;
|
||||
}
|
||||
}`;
|
||||
const blob = new Blob([css], { type: "text/css" });
|
||||
downloadBlob(blob, "globals.css");
|
||||
toast.success("CSS downloaded successfully", {
|
||||
id: "download-css",
|
||||
});
|
||||
} catch (error) {
|
||||
const e = error as Error;
|
||||
console.error(e);
|
||||
toast.error(e.message, {
|
||||
id: "download-css",
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Download CSS
|
||||
</Button>
|
||||
</div>
|
||||
<span className='text-sm text-muted-foreground'>
|
||||
It is recommended to use "Copy CSS Code" button to copy the CSS
|
||||
code
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as SliderPrimitive from "@radix-ui/react-slider"
|
||||
|
||||
import { cn } from "~/utils"
|
||||
|
||||
const Slider = React.forwardRef<
|
||||
React.ElementRef<typeof SliderPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SliderPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full touch-none select-none items-center",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
|
||||
<SliderPrimitive.Range className="absolute h-full bg-primary" />
|
||||
</SliderPrimitive.Track>
|
||||
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
|
||||
</SliderPrimitive.Root>
|
||||
))
|
||||
Slider.displayName = SliderPrimitive.Root.displayName
|
||||
|
||||
export { Slider }
|
||||
@@ -236,3 +236,4 @@ export const Schema_Theme = z.object({
|
||||
"ring": z.string(),
|
||||
"radius": z.string(),
|
||||
});
|
||||
export type ThemeKeys = keyof z.infer<typeof Schema_Theme>;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// Used for configuration on deploy guide page
|
||||
import { HslColor } from "react-colorful";
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Schema_Config,
|
||||
@@ -77,3 +78,12 @@ export function parseConfigFile(config: string):
|
||||
return { success: false, message: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
export function parseThemeValue(color: string): HslColor {
|
||||
const [h, s, l] = color.split(" ");
|
||||
return {
|
||||
h: parseFloat(h),
|
||||
s: parseFloat(s),
|
||||
l: parseFloat(l),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user