refactor(web): share markdown element styles between viewer and editor
Extract the Tailwind classes for common markdown elements (paragraph, blockquote, lists, inline code, link, hr, headings) into a single markdownStyles.ts consumed by both the read-only MemoContent components and the WYSIWYG editor, replacing the duplicated per-element strings and the .memo-wysiwyg CSS block. Heading classes are precomputed per level so the hot renderHTML path is a lookup, not a cn() merge. Also require at least one character after `#` before opening the tag suggestion menu so a bare `#` (or `# ` heading) no longer conflicts with markdown headings.
This commit is contained in:
parent
2c0efeba7c
commit
00225db922
15 changed files with 139 additions and 130 deletions
|
|
@ -37,6 +37,7 @@
|
|||
"@tanstack/react-query": "^5.100.9",
|
||||
"@tanstack/react-query-devtools": "^5.100.9",
|
||||
"@tiptap/core": "3.26.0",
|
||||
"@tiptap/extension-heading": "3.26.0",
|
||||
"@tiptap/extension-list": "3.26.0",
|
||||
"@tiptap/extensions": "3.26.0",
|
||||
"@tiptap/markdown": "3.26.0",
|
||||
|
|
|
|||
|
|
@ -68,6 +68,9 @@ importers:
|
|||
'@tiptap/core':
|
||||
specifier: 3.26.0
|
||||
version: 3.26.0(@tiptap/pm@3.26.0)
|
||||
'@tiptap/extension-heading':
|
||||
specifier: 3.26.0
|
||||
version: 3.26.0(@tiptap/core@3.26.0(@tiptap/pm@3.26.0))
|
||||
'@tiptap/extension-list':
|
||||
specifier: 3.26.0
|
||||
version: 3.26.0(@tiptap/core@3.26.0(@tiptap/pm@3.26.0))(@tiptap/pm@3.26.0)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useAuth } from "@/contexts/AuthContext";
|
|||
import { type MemoFilter, stringifyFilters, useMemoFilterContext } from "@/contexts/MemoFilterContext";
|
||||
import useNavigateTo from "@/hooks/useNavigateTo";
|
||||
import { colorToHex } from "@/lib/color";
|
||||
import { tagStyles } from "@/lib/markdownStyles";
|
||||
import { findTagMetadata } from "@/lib/tag";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Routes } from "@/router";
|
||||
|
|
@ -66,11 +67,7 @@ export const Tag: React.FC<TagProps> = ({ "data-tag": dataTag, children, classNa
|
|||
|
||||
return (
|
||||
<span
|
||||
className={cn(
|
||||
"inline-flex items-center align-baseline px-1.5 py-0.5 text-[0.9em] leading-none font-normal rounded-full border cursor-pointer transition-opacity hover:opacity-75",
|
||||
!bgHex && "border-primary text-primary bg-primary/15",
|
||||
className,
|
||||
)}
|
||||
className={cn(tagStyles.base, "cursor-pointer transition-opacity hover:opacity-75", !bgHex && tagStyles.defaultColor, className)}
|
||||
style={tagStyle}
|
||||
data-tag={tag}
|
||||
{...props}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { markdownStyles } from "@/lib/markdownStyles";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { NestedMarkdownRenderContext } from "../MarkdownRenderContext";
|
||||
import type { ReactMarkdownProps } from "./types";
|
||||
|
|
@ -11,7 +12,7 @@ interface BlockquoteProps extends React.BlockquoteHTMLAttributes<HTMLQuoteElemen
|
|||
*/
|
||||
export const Blockquote = ({ children, className, node: _node, ...props }: BlockquoteProps) => {
|
||||
return (
|
||||
<blockquote className={cn("my-0 mb-2 border-l-4 border-primary/30 pl-3 text-muted-foreground italic", className)} {...props}>
|
||||
<blockquote className={cn(markdownStyles.blockquote, className)} {...props}>
|
||||
<NestedMarkdownRenderContext>{children}</NestedMarkdownRenderContext>
|
||||
</blockquote>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { headingClass } from "@/lib/markdownStyles";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ReactMarkdownProps } from "./types";
|
||||
|
||||
|
|
@ -14,17 +15,8 @@ interface HeadingProps extends React.HTMLAttributes<HTMLHeadingElement>, ReactMa
|
|||
export const Heading = ({ level, children, className, node: _node, ...props }: HeadingProps) => {
|
||||
const Component = `h${level}` as const;
|
||||
|
||||
const levelClasses = {
|
||||
1: "text-3xl font-bold border-b border-border pb-2",
|
||||
2: "text-2xl font-semibold border-b border-border pb-1.5",
|
||||
3: "text-xl font-semibold",
|
||||
4: "text-lg font-semibold",
|
||||
5: "text-base font-semibold",
|
||||
6: "text-base font-medium text-muted-foreground",
|
||||
};
|
||||
|
||||
return (
|
||||
<Component className={cn("mt-3 mb-2 leading-tight", levelClasses[level], className)} {...props}>
|
||||
<Component className={cn(headingClass(level), className)} {...props}>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { markdownStyles } from "@/lib/markdownStyles";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ReactMarkdownProps } from "./types";
|
||||
|
||||
|
|
@ -7,5 +8,5 @@ interface HorizontalRuleProps extends React.HTMLAttributes<HTMLHRElement>, React
|
|||
* Horizontal rule separator
|
||||
*/
|
||||
export const HorizontalRule = ({ className, node: _node, ...props }: HorizontalRuleProps) => {
|
||||
return <hr className={cn("my-2 h-0 border-0 border-b border-border", className)} {...props} />;
|
||||
return <hr className={cn(markdownStyles.horizontalRule, className)} {...props} />;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { markdownStyles } from "@/lib/markdownStyles";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ReactMarkdownProps } from "./types";
|
||||
|
||||
|
|
@ -10,7 +11,7 @@ interface InlineCodeProps extends React.HTMLAttributes<HTMLElement>, ReactMarkdo
|
|||
*/
|
||||
export const InlineCode = ({ children, className, node: _node, ...props }: InlineCodeProps) => {
|
||||
return (
|
||||
<code className={cn("font-mono text-sm bg-muted px-1 py-0.5 rounded-md", className)} {...props}>
|
||||
<code className={cn(markdownStyles.inlineCode, className)} {...props}>
|
||||
{children}
|
||||
</code>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { markdownStyles } from "@/lib/markdownStyles";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { ReactMarkdownProps } from "./types";
|
||||
|
||||
|
|
@ -11,16 +12,7 @@ interface LinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement>, React
|
|||
*/
|
||||
export const Link = ({ children, className, href, node: _node, ...props }: LinkProps) => {
|
||||
return (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={cn(
|
||||
"text-primary underline decoration-primary/50 underline-offset-2 transition-colors hover:decoration-primary",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<a href={href} target="_blank" rel="noopener noreferrer" className={cn(markdownStyles.link, className)} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Children, cloneElement, isValidElement, type ReactElement, type ReactNode } from "react";
|
||||
import { markdownStyles } from "@/lib/markdownStyles";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TASK_LIST_CLASS, TASK_LIST_ITEM_CLASS } from "../constants";
|
||||
import { NestedMarkdownRenderContext } from "../MarkdownRenderContext";
|
||||
|
|
@ -72,20 +73,12 @@ interface ListProps extends React.HTMLAttributes<HTMLUListElement | HTMLOListEle
|
|||
export const List = ({ ordered, children, className, node: _node, ...domProps }: ListProps) => {
|
||||
const Component = ordered ? "ol" : "ul";
|
||||
const isTaskList = className?.includes(TASK_LIST_CLASS);
|
||||
// Task list indentation is handled by task item grid columns; regular lists
|
||||
// use the shared token (padding + list style).
|
||||
const listClass = isTaskList ? "my-0 mb-2 list-outside list-none" : ordered ? markdownStyles.orderedList : markdownStyles.bulletList;
|
||||
|
||||
return (
|
||||
<Component
|
||||
className={cn(
|
||||
"my-0 mb-2 list-outside",
|
||||
isTaskList
|
||||
? // Task list indentation is handled by task item grid columns.
|
||||
"list-none"
|
||||
: // Regular list: standard padding and list style
|
||||
cn("pl-6", ordered ? "list-decimal" : "list-disc"),
|
||||
className,
|
||||
)}
|
||||
{...domProps}
|
||||
>
|
||||
<Component className={cn(listClass, className)} {...domProps}>
|
||||
{children}
|
||||
</Component>
|
||||
);
|
||||
|
|
@ -122,7 +115,7 @@ export const ListItem = ({ children, className, node: _node, ...domProps }: List
|
|||
}
|
||||
|
||||
return (
|
||||
<li className={cn("mt-0.5 leading-6", className)} {...domProps}>
|
||||
<li className={cn(markdownStyles.listItem, className)} {...domProps}>
|
||||
<NestedMarkdownRenderContext>{children}</NestedMarkdownRenderContext>
|
||||
</li>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import type { Element } from "hast";
|
||||
import { markdownStyles } from "@/lib/markdownStyles";
|
||||
import { cn } from "@/lib/utils";
|
||||
import LinkMetadataCard from "../LinkMetadataCard";
|
||||
import { useMarkdownRenderContext } from "../MarkdownRenderContext";
|
||||
|
|
@ -47,7 +48,7 @@ export const Paragraph = ({ children, className, node, ...props }: ParagraphProp
|
|||
const { blockDepth } = useMarkdownRenderContext();
|
||||
const href = blockDepth === 0 ? getSingleLinkHref(node) : undefined;
|
||||
const paragraph = (
|
||||
<p className={cn("my-0 mb-2 leading-6", className)} {...props}>
|
||||
<p className={cn(markdownStyles.paragraph, className)} {...props}>
|
||||
{children}
|
||||
</p>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,6 +2,11 @@ import type { MarkdownToken } from "@tiptap/core";
|
|||
import { InputRule, Mark, mergeAttributes } from "@tiptap/core";
|
||||
import type { TokenizerThis, Tokens } from "marked";
|
||||
import { marked } from "marked";
|
||||
import { tagStyles } from "@/lib/markdownStyles";
|
||||
|
||||
// Default tag pill, shared with the read-only view (MemoContent/Tag.tsx).
|
||||
// Computed once — renderHTML runs on every view update.
|
||||
const TAG_CLASS = `${tagStyles.base} ${tagStyles.defaultColor}`;
|
||||
|
||||
// Mirrors the renderer's tag lexer (web/src/utils/remark-plugins/remark-tag.ts):
|
||||
// letters, numbers, symbols, plus _ - / &, max 100 chars. Keep the two in sync.
|
||||
|
|
@ -123,7 +128,7 @@ export const Tag = Mark.create({
|
|||
return [
|
||||
"span",
|
||||
mergeAttributes(HTMLAttributes, {
|
||||
class: "inline-block rounded bg-accent text-accent-foreground px-1",
|
||||
class: TAG_CLASS,
|
||||
}),
|
||||
0,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -27,10 +27,16 @@ export const TagSuggestion = Extension.create<TagSuggestionOptions>({
|
|||
char: "#",
|
||||
allowSpaces: false,
|
||||
items: ({ query }) => {
|
||||
const tags = this.options.getTags();
|
||||
// Require at least one char after `#` so a bare `#` (or `# ` heading)
|
||||
// doesn't pop the tag menu and conflict with markdown headings.
|
||||
if (query.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const q = query.toLowerCase();
|
||||
const filtered = q ? tags.filter((tag) => tag.toLowerCase().includes(q)) : tags;
|
||||
return filtered.slice(0, MAX_SUGGESTIONS);
|
||||
return this.options
|
||||
.getTags()
|
||||
.filter((tag) => tag.toLowerCase().includes(q))
|
||||
.slice(0, MAX_SUGGESTIONS);
|
||||
},
|
||||
command: ({ editor, range, props: tag }) => {
|
||||
editor
|
||||
|
|
|
|||
|
|
@ -1,10 +1,26 @@
|
|||
import type { AnyExtension } from "@tiptap/core";
|
||||
import { type AnyExtension, mergeAttributes } from "@tiptap/core";
|
||||
import { Heading } from "@tiptap/extension-heading";
|
||||
import { TaskItem, TaskList } from "@tiptap/extension-list";
|
||||
import { Markdown } from "@tiptap/markdown";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import { type HeadingLevel, headingClass, markdownStyles } from "@/lib/markdownStyles";
|
||||
import { preservedExtensions } from "./PreservedBlock";
|
||||
import { Tag } from "./Tag";
|
||||
|
||||
/**
|
||||
* StarterKit's Heading is bundled and cannot vary classes by level via static
|
||||
* HTMLAttributes, so we disable it (heading: false) and render headings here.
|
||||
* renderHTML only affects the editable DOM — heading markdown serialization is
|
||||
* unchanged — so the round-trip codec is unaffected.
|
||||
*/
|
||||
const StyledHeading = Heading.extend({
|
||||
renderHTML({ node, HTMLAttributes }) {
|
||||
const { levels } = this.options;
|
||||
const level = (levels.includes(node.attrs.level) ? node.attrs.level : levels[0]) as HeadingLevel;
|
||||
return [`h${level}`, mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { class: headingClass(level) }), 0];
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* The canonical schema-relevant extension set, shared by the live editor and
|
||||
* the headless markdown codec so that parse/serialize behavior is identical
|
||||
|
|
@ -14,12 +30,20 @@ import { Tag } from "./Tag";
|
|||
export function buildExtensions(): AnyExtension[] {
|
||||
return [
|
||||
StarterKit.configure({
|
||||
heading: { levels: [1, 2, 3, 4, 5, 6] },
|
||||
link: { openOnClick: false },
|
||||
heading: false,
|
||||
link: { openOnClick: false, HTMLAttributes: { class: markdownStyles.link } },
|
||||
// Markdown has no underline syntax; keeping the extension would let
|
||||
// Ctrl+U create marks that cannot serialize. Out of the schema entirely.
|
||||
underline: false,
|
||||
paragraph: { HTMLAttributes: { class: markdownStyles.paragraph } },
|
||||
blockquote: { HTMLAttributes: { class: markdownStyles.blockquote } },
|
||||
bulletList: { HTMLAttributes: { class: markdownStyles.bulletList } },
|
||||
orderedList: { HTMLAttributes: { class: markdownStyles.orderedList } },
|
||||
listItem: { HTMLAttributes: { class: markdownStyles.listItem } },
|
||||
code: { HTMLAttributes: { class: markdownStyles.inlineCode } },
|
||||
horizontalRule: { HTMLAttributes: { class: markdownStyles.horizontalRule } },
|
||||
}),
|
||||
StyledHeading.configure({ levels: [1, 2, 3, 4, 5, 6] }),
|
||||
TaskList,
|
||||
TaskItem.configure({ nested: true }),
|
||||
Markdown,
|
||||
|
|
|
|||
|
|
@ -41,73 +41,10 @@
|
|||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Tailwind's preflight resets h1–h6 to inherit; restore the same heading
|
||||
scale the memo view uses (MemoContent/markdown/Heading.tsx) so live
|
||||
conversion is visible while editing. */
|
||||
.memo-wysiwyg h1,
|
||||
.memo-wysiwyg h2,
|
||||
.memo-wysiwyg h3,
|
||||
.memo-wysiwyg h4,
|
||||
.memo-wysiwyg h5,
|
||||
.memo-wysiwyg h6 {
|
||||
margin: 0.75rem 0 0.5rem;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.memo-wysiwyg h1:first-child,
|
||||
.memo-wysiwyg h2:first-child,
|
||||
.memo-wysiwyg h3:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.memo-wysiwyg h1 {
|
||||
font-size: 1.875rem;
|
||||
font-weight: 700;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.memo-wysiwyg h2 {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 0.375rem;
|
||||
}
|
||||
|
||||
.memo-wysiwyg h3 {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.memo-wysiwyg h4 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.memo-wysiwyg h5 {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.memo-wysiwyg h6 {
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
.memo-wysiwyg ul,
|
||||
.memo-wysiwyg ol {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.memo-wysiwyg ul {
|
||||
list-style: disc;
|
||||
}
|
||||
|
||||
.memo-wysiwyg ol {
|
||||
list-style: decimal;
|
||||
}
|
||||
|
||||
/* Editor-only element styles: task lists and code blocks have no shared markup
|
||||
with the read-only memo view (MemoContent renders those via TaskListItem.tsx
|
||||
and CodeBlock.tsx), so unlike the elements in markdownStyles.ts they stay
|
||||
local to the editor here rather than in the shared style token. */
|
||||
.memo-wysiwyg ul[data-type="taskList"] {
|
||||
list-style: none;
|
||||
padding-left: 0;
|
||||
|
|
@ -127,12 +64,6 @@
|
|||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.memo-wysiwyg blockquote {
|
||||
border-left: 2px solid var(--border);
|
||||
padding-left: 0.75rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.memo-wysiwyg pre {
|
||||
background: var(--muted);
|
||||
border-radius: 0.375rem;
|
||||
|
|
@ -142,14 +73,6 @@
|
|||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.memo-wysiwyg code {
|
||||
background: var(--muted);
|
||||
border-radius: 0.25rem;
|
||||
padding: 0 0.25rem;
|
||||
font-family: var(--font-mono, monospace);
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
.memo-wysiwyg pre code {
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
|
|
|
|||
69
web/src/lib/markdownStyles.ts
Normal file
69
web/src/lib/markdownStyles.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import { cn } from "@/lib/utils";
|
||||
|
||||
export type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
||||
|
||||
/** Per-level heading classes (size / weight / border), matching MemoContent. */
|
||||
const headingLevelClasses: Record<HeadingLevel, string> = {
|
||||
1: "text-3xl font-bold border-b border-border pb-2",
|
||||
2: "text-2xl font-semibold border-b border-border pb-1.5",
|
||||
3: "text-xl font-semibold",
|
||||
4: "text-lg font-semibold",
|
||||
5: "text-base font-semibold",
|
||||
6: "text-base font-medium text-muted-foreground",
|
||||
};
|
||||
|
||||
/** Shared base classes applied to every heading level. */
|
||||
const headingBaseClasses = "mt-3 mb-2 leading-tight";
|
||||
|
||||
/**
|
||||
* Complete heading class per level, precomputed once at module load (base +
|
||||
* per-level). headingClass is a hot path — MemoContent renders it per heading
|
||||
* and the editor's renderHTML runs it on essentially every keystroke — so the
|
||||
* cn() merge happens here, not per call.
|
||||
*/
|
||||
const headingClasses: Record<HeadingLevel, string> = {
|
||||
1: cn(headingBaseClasses, headingLevelClasses[1]),
|
||||
2: cn(headingBaseClasses, headingLevelClasses[2]),
|
||||
3: cn(headingBaseClasses, headingLevelClasses[3]),
|
||||
4: cn(headingBaseClasses, headingLevelClasses[4]),
|
||||
5: cn(headingBaseClasses, headingLevelClasses[5]),
|
||||
6: cn(headingBaseClasses, headingLevelClasses[6]),
|
||||
};
|
||||
|
||||
/**
|
||||
* Single source of truth for the styling of common markdown elements rendered
|
||||
* by BOTH the read-only memo view (MemoContent) and the WYSIWYG editor
|
||||
* (MemoEditor). Each value is a complete, standalone Tailwind class string so it
|
||||
* can be dropped onto a DOM element as-is (the editor sets these via Tiptap
|
||||
* `HTMLAttributes`; MemoContent merges them with `cn`).
|
||||
*
|
||||
* These are static string literals so Tailwind's JIT scanner detects them.
|
||||
*/
|
||||
export const markdownStyles = {
|
||||
paragraph: "my-0 mb-2 leading-6",
|
||||
blockquote: "my-0 mb-2 border-l-4 border-primary/30 pl-3 text-muted-foreground italic",
|
||||
bulletList: "my-0 mb-2 list-outside pl-6 list-disc",
|
||||
orderedList: "my-0 mb-2 list-outside pl-6 list-decimal",
|
||||
listItem: "mt-0.5 leading-6",
|
||||
inlineCode: "font-mono text-sm bg-muted px-1 py-0.5 rounded-md",
|
||||
link: "text-primary underline decoration-primary/50 underline-offset-2 transition-colors hover:decoration-primary",
|
||||
horizontalRule: "my-2 h-0 border-0 border-b border-border",
|
||||
} as const;
|
||||
|
||||
/** Complete heading class for a given level (shared base + per-level classes). */
|
||||
export const headingClass = (level: HeadingLevel): string => headingClasses[level];
|
||||
|
||||
/**
|
||||
* Tag pill styling, shared by the read-only memo view (MemoContent/Tag.tsx) and
|
||||
* the editor's tag mark (MemoEditor/Editor/Tag.ts) so a `#tag` looks identical
|
||||
* while typing and after saving. Split into two tokens so the viewer can swap
|
||||
* `defaultColor` for an inline custom color, and so the editor — which is never
|
||||
* custom-colored and is not a filter button — takes the shape + default color
|
||||
* without the viewer's click/hover affordances.
|
||||
*/
|
||||
export const tagStyles = {
|
||||
/** Shape, padding, and typography — always applied. */
|
||||
base: "inline-flex items-center align-baseline px-1.5 py-0.5 text-[0.9em] leading-none font-normal rounded-full border",
|
||||
/** Default theme color, used when no custom tag color is set. */
|
||||
defaultColor: "border-primary text-primary bg-primary/15",
|
||||
} as const;
|
||||
Loading…
Reference in a new issue