fixed editor missing peaces
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 0s

This commit is contained in:
mr-shortman 2025-03-15 23:48:44 +01:00
parent d9ed115e56
commit 1597d4f113
29 changed files with 203 additions and 141 deletions

View File

@ -7,13 +7,4 @@ jobs:
runs-on: linux
steps:
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Check out repository code
uses: actions/checkout@v4
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ gitea.workspace }}
- run: echo "🍏 This job's status is ${{ job.status }}."
- run: Test "🎉 job completed! 🚀"

View File

@ -67,6 +67,7 @@
"superjson": "^2.2.1",
"tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7",
"tiptap-extension-resize-image": "^1.2.1",
"use-debounce": "^10.0.4",
"winston": "^3.17.0",
"zod": "^3.24.2"

16
pnpm-lock.yaml generated
View File

@ -149,6 +149,9 @@ importers:
tailwindcss-animate:
specifier: ^1.0.7
version: 1.0.7(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.17.23)(typescript@5.8.2)))
tiptap-extension-resize-image:
specifier: ^1.2.1
version: 1.2.1(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/extension-image@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5)))(@tiptap/pm@2.11.5)
use-debounce:
specifier: ^10.0.4
version: 10.0.4(react@18.3.1)
@ -4095,6 +4098,13 @@ packages:
tiptap-extension-global-drag-handle@0.1.18:
resolution: {integrity: sha512-jwFuy1K8DP3a4bFy76Hpc63w1Sil0B7uZ3mvhQomVvUFCU787Lg2FowNhn7NFzeyok761qY2VG+PZ/FDthWUdg==}
tiptap-extension-resize-image@1.2.1:
resolution: {integrity: sha512-SLMAujDa+0LN/6Iv2HtU4Uk0BL6LMh4b/r85frpdnjFDW2i6pIOfTVG8jzJQ8T1EgYHNn2YG1U2HoVAGuwLc3Q==}
peerDependencies:
'@tiptap/core': ^2.0.0
'@tiptap/extension-image': ^2.0.0
'@tiptap/pm': ^2.0.0
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@ -8492,6 +8502,12 @@ snapshots:
tiptap-extension-global-drag-handle@0.1.18: {}
tiptap-extension-resize-image@1.2.1(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/extension-image@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5)))(@tiptap/pm@2.11.5):
dependencies:
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
'@tiptap/extension-image': 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))
'@tiptap/pm': 2.11.5
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 569 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -1,5 +1,6 @@
import RenderArticle from "@/components/article/render-article";
import BreadNavigator from "@/components/bread-navigator";
import RenderContent from "@/components/editor/render-content";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { appRoutes } from "@/config";
import { hasPermission, Role } from "@/lib/validation/permissions";
@ -21,8 +22,9 @@ async function Page({ params }: { params: Promise<{ slug: string }> }) {
return (
<div className="space-y-2">
<div className="flex w-full items-center justify-between">
<div className="w-full">
<div className="flex w-full items-center gap-4">
<BreadNavigator
className="w-full"
links={[
...(article?.category
? [
@ -38,7 +40,14 @@ async function Page({ params }: { params: Promise<{ slug: string }> }) {
/>
</div>
{isEditor && (
<div className="flex w-full justify-end space-x-2">
<div className="flex w-full items-center justify-end space-x-2">
<Badge
className="size-max"
variant={article.published ? "outline" : "destructive"}
>
{article.published ? "Veröffentlicht" : "Draft"}
</Badge>
<Button asChild variant={"outline"}>
<Link href={appRoutes.editArticle(article.slug)}>
<Edit className="size-4" />
@ -52,10 +61,7 @@ async function Page({ params }: { params: Promise<{ slug: string }> }) {
)}
</div>
<h1 className="text-4xl font-bold">{article.title}</h1>
{article?.content?.length ? (
<RenderArticle content={article.content} />
) : null}
{article.content && <RenderContent content={article.content} />}
</div>
);
}

View File

@ -13,14 +13,17 @@ import {
} from "@/components/ui/card";
import Avatar from "../avatar";
import { Icons } from "../icons";
import { Badge } from "../ui/badge";
function ArticleCard({
title,
slug,
author,
published,
createdAt,
}: Pick<Article, "title" | "slug" | "createdAt" | "author">) {
}: Pick<Article, "title" | "slug" | "createdAt" | "author" | "published">) {
const authorName = author?.name ?? `${appConfig.name} Team`;
return (
<Link href={appRoutes.article(slug)}>
<Card className="group flex h-full flex-col justify-between">
@ -37,11 +40,21 @@ function ArticleCard({
)}
<span className="text-sm text-muted-foreground">{authorName}</span>
</div>
<div className="flex items-center space-x-2">
{typeof published === "boolean" && (
<Badge
className="size-max px-2 py-px text-xs"
variant={published ? "outline" : "destructive"}
>
{published ? "Veröffentlicht" : "Draft"}
</Badge>
)}
<p className="text-sm text-muted-foreground">
{createdAt.toLocaleDateString("de-DE", {
dateStyle: "long",
})}
</p>
</div>
</CardFooter>
</Card>
</Link>

View File

@ -85,6 +85,7 @@ function ArticleFilterBar({
<CategorySelect
className="w-full"
initialValue={filter.category}
onSelect={(category) => {
onFilterChange({
category: category?.length ? category : undefined,

View File

@ -26,6 +26,7 @@ import { CheckCircle, XCircle } from "lucide-react";
import PublishArticleAlertDialog from "./publish-article-alert-dialog";
import { Label } from "@/components/ui/label";
import dynamic from "next/dynamic";
import { appRoutes } from "@/config";
const Editor = dynamic(() => import("../editor"), { ssr: false });
export default ({ server_article }: { server_article: Article }) => {
@ -78,7 +79,7 @@ export default ({ server_article }: { server_article: Article }) => {
<FormControl>
<TextareaAutosize
cols={1}
className="h-max w-full resize-none text-4xl font-bold focus-visible:outline-none"
className="h-max w-full resize-none bg-transparent text-4xl font-bold focus-visible:outline-none"
value={field.value}
onChange={(e) => {
field.onChange(e);
@ -117,7 +118,7 @@ export default ({ server_article }: { server_article: Article }) => {
// loading && "border-t-blue-600",
)}
>
<div className="relative w-full space-y-4 overflow-hidden rounded-md border-t-2 bg-muted p-4">
<div className="relative w-full space-y-4 overflow-hidden rounded-md border bg-background p-4">
<div
className={cn(
"saving absolute left-0 right-0 top-0 h-0.5 rounded-full bg-primary",
@ -152,11 +153,11 @@ export default ({ server_article }: { server_article: Article }) => {
</span>
</div>
<Link
href={"/editoren-hilfe"}
href={appRoutes.article(server_article.slug)}
target="_blank"
className="size-max scale-90 p-0 text-xs text-muted-foreground"
>
<span>? Hilfe</span>
<span>Ansehen</span>
</Link>
</div>
<FormField
@ -188,6 +189,7 @@ export default ({ server_article }: { server_article: Article }) => {
<FormItem className="w-full">
<FormControl>
<CategorySelect
className="w-full"
initialValue={field.value}
onSelect={(categoryId) => {
field.onChange(categoryId);

View File

@ -7,6 +7,7 @@ import ArticleCard from "../article-card";
import { useInfiniteItemsObserver } from "@/lib/hooks/infinite-items-observer-hook";
import { Skeleton } from "@/components/ui/skeleton";
import ArticleFilterBar, { ArticleFilter } from "../article-filter-bar";
import { Article } from "@/server/db/schema";
function InfiniteArticlesGrid() {
const [filter, setFilter] = React.useState<ArticleFilter | undefined>(
@ -19,7 +20,7 @@ function InfiniteArticlesGrid() {
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
staleTime: 60 * 4 * 1000, // 4 minutes stale time
// staleTime: 60 * 4 * 1000, // 4 minutes stale time
refetchOnMount: false, // Prevents unnecessary refetching
refetchOnWindowFocus: false, // Avoids refetch when switching tabs
},
@ -46,7 +47,7 @@ function InfiniteArticlesGrid() {
{data?.pages?.length
? allItems.map((article, idx) => (
<li key={`article-${idx}`}>
<ArticleCard {...article} />
<ArticleCard {...(article as Article)} />
</li>
))
: null}

View File

@ -1,7 +0,0 @@
import { JSONContent } from "novel";
function RenderArticle({ content }: { content: JSONContent }) {
return "render article: in work"; //<Editor readOnly editorProviderProps={{ content: content }} />;
}
export default RenderArticle;

View File

@ -16,7 +16,8 @@ function CategorySelect(props: Partial<ComboboxProps>) {
empty: "Keine Kategorien gefunden",
}}
data={
categories?.map(({ name, id }) => ({ label: name!, value: id! })) ?? []
categories?.map(({ name, slug }) => ({ label: name, value: slug })) ??
[]
}
/>
);

View File

@ -50,6 +50,7 @@ export function Combobox({
const [open, setOpen] = React.useState(false);
const [value, setValue] = React.useState(initialValue ?? "");
const selectedItem = data.find((item) => item.value === initialValue)!;
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>

View File

@ -18,15 +18,15 @@ import {
Youtube,
} from "novel";
import LinkPreview from "./link-preview";
import { Heading } from "@tiptap/extension-heading";
import { cx } from "class-variance-authority";
import { slashCommand } from "./slash-commands";
import ImageResize from "tiptap-extension-resize-image";
const placeholder = Placeholder;
const tiptapLink = TiptapLink.configure({
HTMLAttributes: {
class: cx(
"text-muted-foreground underline underline-offset-[3px] hover:text-primary transition-colors cursor-pointer",
"underline text-foreground underline-offset-[3px] hover:text-primary transition-colors cursor-pointer",
),
},
});
@ -46,6 +46,12 @@ const tiptapImage = TiptapImage.extend({
},
});
const resizeImage = ImageResize.configure({
HTMLAttributes: {
class: cx("rounded-lg border border-muted"),
},
});
const updatedImage = UpdatedImage.configure({
HTMLAttributes: {
class: cx("rounded-lg border border-muted"),
@ -108,6 +114,7 @@ export const defaultExtensions = [
tiptapLink,
tiptapImage,
updatedImage,
resizeImage,
taskList,
taskItem,
horizontalRule,

View File

@ -36,7 +36,7 @@ const InputLink = ({ onSubmit }: { onSubmit: (link: string) => void }) => {
placeholder="Enter a link"
value={link}
onChange={(e) => setLink(e.target.value)}
className="flex-1 focus-visible:ring-transparent border-0"
className="flex-1 border-0 focus-visible:ring-transparent"
/>
<Button
type="submit"
@ -63,7 +63,8 @@ const Preview = ({
return (
<a href={href} target="_blank">
<div className=" flex gap-4 flex-col-reverse md:flex-row">
<div className="flex flex-col-reverse gap-4 md:flex-row">
<div className="flex w-full flex-col justify-between">
<div className="w-full space-y-2">
<h2
className="text-xl"
@ -74,7 +75,8 @@ const Preview = ({
>
{title}
</h2>
<p className="text-sm mt-2">{description}</p>
<p className="mt-2 text-sm">{description}</p>
</div>
<span className="text-xs text-muted-foreground">{href}</span>
</div>
@ -98,13 +100,13 @@ export const LinkPreviewComponent: React.FC<NodeViewProps> = ({
extension,
}) => {
const [preview, setPreview] = React.useState<LinkPreviewData | undefined>(
(node.attrs?.href?.length && (node.attrs as LinkPreviewData)) ?? undefined
(node.attrs?.href?.length && (node.attrs as LinkPreviewData)) ?? undefined,
);
const [loading, setLoading] = React.useState(false);
return (
<NodeViewWrapper as="div" className="p-4 rounded-md bg-background border ">
<NodeViewWrapper as="div" className="rounded-md border bg-background p-4">
{loading ? (
<Skeleton className="h-8 w-full rounded" />
) : preview ? (

View File

@ -14,6 +14,7 @@ import {
import { Command, renderItems, createSuggestionItems } from "novel";
import { selectionItems } from "../../selector/selection-items";
import { uploadFile } from "@/server/actions/image";
// const items = selectionItems.filter((item) => !item.inline);
// const defaultSuggestionItems = items.map((item) => (
@ -148,10 +149,19 @@ export const suggestionItems = createSuggestionItems([
input.type = "file";
input.accept = "image/*";
input.onchange = async () => {
if (input.files?.length) {
if (input.files?.[0]) {
const file = input.files[0];
const pos = editor.view.state.selection.from;
// uploadFn(file, editor.view, pos);
const formData = new FormData();
formData.append("file", file);
const url = await uploadFile(formData);
console.log("URL", url);
if (!url) return;
editor
.chain()
.focus()
.setImage({ src: url, alt: file.name, title: file.name })
.run();
}
};
input.click();

View File

@ -14,23 +14,26 @@ import { MenuBar } from "./menu/menu-bar";
const Editor = ({
onContentChange,
initialContent,
readOnly,
}: {
initialContent: JSONContent | null;
onContentChange: (content: JSONContent) => void;
onContentChange?: (content: JSONContent) => void;
readOnly?: boolean;
}) => {
return (
<EditorRoot>
<EditorContent
slotBefore={<MenuBar />}
slotBefore={!readOnly && <MenuBar />}
extensions={defaultExtensions}
editorProps={{
handleDOMEvents: {
keydown: (_view, event) => handleCommandNavigation(event),
},
}}
editable={!readOnly}
initialContent={initialContent ?? { type: "doc" }}
onUpdate={({ editor }) => {
onContentChange(editor.getJSON());
onContentChange?.(editor.getJSON());
}}
>
<SlashCommandComponent />

View File

@ -0,0 +1,11 @@
"use client";
import dynamic from "next/dynamic";
import { JSONContent } from "novel";
import React from "react";
const Editor = dynamic(() => import("."), { ssr: false });
function RenderContent({ content }: { content: JSONContent }) {
return <Editor readOnly initialContent={content} />;
}
export default RenderContent;

View File

@ -15,80 +15,43 @@ export interface BubbleColorMenuItem {
const TEXT_COLORS: BubbleColorMenuItem[] = [
{
name: "Default",
color: "var(--novel-black)",
color: "var(--foreground)",
},
{
name: "Purple",
color: "#9333EA",
color: "var(--color-purple-600)",
},
{
name: "Red",
color: "#E00000",
color: "var(--color-red-600)",
},
{
name: "Yellow",
color: "#EAB308",
color: "var(--color-yellow-600)",
},
{
name: "Blue",
color: "#2563EB",
color: "var(--color-blue-600)",
},
{
name: "Green",
color: "#008A00",
color: "var(--color-emerald-600)",
},
{
name: "Orange",
color: "#FFA500",
color: "var(--color-orange-600)",
},
{
name: "Pink",
color: "#BA4081",
color: "var(--color-pink-600)",
},
{
name: "Gray",
color: "#A8A29E",
color: "var(--color-gray-600)",
},
];
const HIGHLIGHT_COLORS: BubbleColorMenuItem[] = [
{
name: "Default",
color: "var(--novel-highlight-default)",
},
{
name: "Purple",
color: "var(--novel-highlight-purple)",
},
{
name: "Red",
color: "var(--novel-highlight-red)",
},
{
name: "Yellow",
color: "var(--novel-highlight-yellow)",
},
{
name: "Blue",
color: "var(--novel-highlight-blue)",
},
{
name: "Green",
color: "var(--novel-highlight-green)",
},
{
name: "Orange",
color: "var(--novel-highlight-orange)",
},
{
name: "Pink",
color: "var(--novel-highlight-pink)",
},
{
name: "Gray",
color: "var(--novel-highlight-gray)",
},
];
const HIGHLIGHT_COLORS: BubbleColorMenuItem[] = TEXT_COLORS;
interface ColorSelectorProps {
open: boolean;
@ -100,11 +63,11 @@ export const ColorSelector = ({ open, onOpenChange }: ColorSelectorProps) => {
if (!editor) return null;
const activeColorItem = TEXT_COLORS.find(({ color }) =>
editor.isActive("textStyle", { color })
editor.isActive("textStyle", { color }),
);
const activeHighlightItem = HIGHLIGHT_COLORS.find(({ color }) =>
editor.isActive("highlight", { color })
editor.isActive("highlight", { color }),
);
return (

View File

@ -9,12 +9,11 @@ import {
Heading3,
Heading4,
ListOrdered,
TextIcon,
TextQuote,
SeparatorHorizontalIcon,
Heading5,
Heading6,
QuoteIcon,
TypeIcon,
} from "lucide-react";
import { EditorInstance } from "novel";
@ -64,7 +63,7 @@ export const selectionItems: SelectorItem[] = [
// blocks
{
name: "Text",
icon: TextIcon,
icon: TypeIcon,
command: (editor) => editor.chain().focus().clearNodes().run(),
isActive: (editor) => editor.isActive("paragraph"), //node?.type?.name === "paragraph",
},

View File

@ -8,9 +8,6 @@ const items = selectionItems.filter((item) => item.inline);
export const TextButtons = () => {
const { editor } = useEditor();
if (!editor) return null;
editor.on("selectionUpdate", () => {
editor.view.dispatch(editor.state.tr);
});
return (
<div className="flex">

View File

@ -72,36 +72,40 @@
}
.tiptap hr {
@apply border-border;
/* border-color: var(--border); */
margin: 0.25rem 0;
}
::selection {
background-color: #5abbf7;
border-radius: 0.375rem;
}
.menu-bar button {
transition-duration: 0s;
}
.menu-bar .is-active {
border-color: var(--primary-color);
img {
@apply rounded-md;
}
/* Task List */
ul[data-type="taskList"] li {
@apply m-0 my-3 flex items-center;
}
ul[data-type="taskList"] li p {
@apply m-0 my-0 flex items-center;
}
ul[data-type="taskList"] li > label input[type="checkbox"] {
@apply size-4 border border-border bg-muted;
-webkit-appearance: none;
appearance: none;
margin: 0;
cursor: pointer;
width: 1.2em;
height: 1.2em;
position: relative;
top: 5px;
border: 2px solid var(--border);
margin-right: 0.3rem;
display: grid;
border-radius: 0.25rem;
place-content: center;
@ -126,7 +130,7 @@ ul[data-type="taskList"] li > label input[type="checkbox"]:checked::before {
transform: scale(1);
}
ul[data-type="taskList"] li[data-checked="true"] > div > p {
color: var(--muted-foreground);
@apply text-foreground/75;
text-decoration: line-through;
text-decoration-thickness: 2px;
}

View File

@ -28,6 +28,8 @@ export async function updateArticle(
article: { ...article, content: JSON.parse(article.content) },
articleId,
});
revalidatePath("/");
// if (!result[0]?.id?.length) return false;
// return revalidatePath(`/artikel/${result[0]?.slug}/edit`);
}
@ -35,5 +37,7 @@ export async function deleteArticle(articleId: string) {
const result = await api.article.delete({
articleId,
});
revalidatePath("/");
// if (!result[0]?.id?.length) return false;
}

View File

@ -6,16 +6,25 @@ import { hasPermission, Role } from "@/lib/validation/permissions";
export async function uploadFile(formData: FormData) {
const session = await auth();
if (!session || hasPermission(session.user.role, Role.EDITOR)) return false;
if (!session || !hasPermission(session.user.role, Role.EDITOR)) return false;
console.log("Starting upload");
const file = formData.get("file") as File;
const arrayBuffer = await file.arrayBuffer();
const buffer = new Uint8Array(arrayBuffer);
const filename = `upload-${Date.now()}-${file.name}.png`;
console.log("Filename", filename);
try {
await fs.writeFile(`./public/uploads/${filename}`, buffer);
console.log("File uploaded successfully");
revalidatePath("/");
return `/uploads/${filename}`;
} catch (e) {
console.error("Error uploading file:", e);
return false;
}
}

View File

@ -1,4 +1,6 @@
import { desc, eq } from "drizzle-orm";
import { createTRPCRouter, publicProcedure } from "../trpc";
import { articles as articlesTable } from "@/server/db/schema";
export const appRouter = createTRPCRouter({
getSidebarMain: publicProcedure.query(async ({ ctx }) => {
@ -11,6 +13,8 @@ export const appRouter = createTRPCRouter({
});
const articles = await ctx.db.query.articles.findMany({
limit: 3,
where: eq(articlesTable.published, true),
orderBy: desc(articlesTable.createdAt),
columns: {
slug: true,
title: true,

View File

@ -71,8 +71,11 @@ export const articleRouter = createTRPCRouter({
get: publicProcedure
.input(z.object({ slug: z.string() }))
.query(async ({ ctx, input }) => {
const user = ctx?.session?.user;
const isEditor = user ? hasPermission(user.role, Role.EDITOR) : false;
const publishedArg = !isEditor ? eq(articles.published, true) : undefined;
return (await ctx.db.query.articles.findFirst({
where: eq(articles.slug, input.slug),
where: and(eq(articles.slug, input.slug), publishedArg),
with: { category: true },
})) as Article;
}),
@ -112,20 +115,19 @@ export const articleRouter = createTRPCRouter({
sortConfig,
cursorObj,
);
const user = ctx?.session?.user;
const isEditor = user ? hasPermission(user.role, Role.EDITOR) : false;
const publishedArg = !isEditor ? eq(articles.published, true) : undefined;
const items = await ctx.db.query.articles.findMany({
where: and(
cursorArg,
categoryArg,
queryFilterArg,
eq(articles.published, true),
),
where: and(cursorArg, categoryArg, queryFilterArg, publishedArg),
limit: limit + 1,
orderBy,
columns: {
title: true,
slug: true,
createdAt: true,
published: isEditor ? true : false,
},
});

View File

@ -89,6 +89,7 @@ const timingMiddleware = t.middleware(async ({ next, path }) => {
if (t._config.isDev) {
// artificial delay in dev
// const waitMs = 1000 * 5; // 5 seconds
const waitMs = Math.floor(Math.random() * 400) + 100;
await new Promise((resolve) => setTimeout(resolve, waitMs));
}

View File

@ -50,7 +50,7 @@
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted: 240 3.7% 5.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
@ -96,7 +96,7 @@
transform: scaleX(1); /* Slight overshoot for bounce effect */
}
100% {
transform: scaleX(0.2);
transform: scaleX(0);
}
}

View File

@ -69,5 +69,25 @@ export default {
},
},
},
plugins: [require("tailwindcss-animate")],
plugins: [
require("tailwindcss-animate"),
function ({ addBase, theme }: any) {
function extractColorVars(colorObj: any, colorGroup = "") {
return Object.keys(colorObj).reduce((vars: any, colorKey: any) => {
const value = colorObj[colorKey];
const newVars: any =
typeof value === "string"
? { [`--color${colorGroup}-${colorKey}`]: value }
: extractColorVars(value, `-${colorKey}`);
return { ...vars, ...newVars };
}, {});
}
addBase({
":root": extractColorVars(theme("colors")),
});
},
],
} satisfies Config;