diff --git a/frontend/src/components/SettingsSidebar/MenuOption/index.jsx b/frontend/src/components/SettingsSidebar/MenuOption/index.jsx new file mode 100644 index 000000000..20924d53d --- /dev/null +++ b/frontend/src/components/SettingsSidebar/MenuOption/index.jsx @@ -0,0 +1,168 @@ +import React, { useEffect, useState } from "react"; +import { CaretRight } from "@phosphor-icons/react"; +import { Link } from "react-router-dom"; + +export default function MenuOption({ + btnText, + icon, + href, + childOptions = [], + flex = false, + user = null, + roles = [], + hidden = false, + isChild = false, +}) { + const storageKey = generateStorageKey({ key: btnText }); + const location = window.location.pathname; + const hasChildren = childOptions.length > 0; + const hasVisibleChildren = hasVisibleOptions(user, childOptions); + const { isExpanded, setIsExpanded } = useIsExpanded({ + storageKey, + hasVisibleChildren, + childOptions, + location, + }); + + if (hidden) return null; + + // If this option is a parent level option + if (!isChild) { + // and has no children then use its flex props and roles prop directly + if (!hasChildren) { + if (!flex && !roles.includes(user?.role)) return null; + if (flex && !!user && !roles.includes(user?.role)) return null; + } + + // if has children and no visible children - remove it. + if (hasChildren && !hasVisibleChildren) return null; + } else { + // is a child so we use it's permissions + if (!flex && !roles.includes(user?.role)) return null; + if (flex && !!user && !roles.includes(user?.role)) return null; + } + + const isActive = hasChildren + ? (!isExpanded && childOptions.some((child) => child.href === location)) || + location === href + : location === href; + + const handleClick = (e) => { + if (hasChildren) { + e.preventDefault(); + const newExpandedState = !isExpanded; + setIsExpanded(newExpandedState); + localStorage.setItem(storageKey, JSON.stringify(newExpandedState)); + } + }; + + return ( + <div> + <div + className={` + flex items-center justify-between w-full + transition-all duration-300 + rounded-[6px] + ${ + isActive + ? "bg-white/5 font-medium border-outline" + : "hover:bg-white/5" + } + `} + > + <Link + to={href} + className={`flex flex-grow items-center px-[12px] h-[32px] font-medium ${ + isChild ? "text-white/70 hover:text-white" : "text-white" + }`} + onClick={hasChildren ? handleClick : undefined} + > + {icon} + <p + className={`${ + isChild ? "text-xs" : "text-sm" + } leading-loose whitespace-nowrap overflow-hidden ml-2 ${ + isActive ? "text-white" : "" + } ${!icon && "pl-5"}`} + > + {btnText} + </p> + </Link> + {hasChildren && ( + <button onClick={handleClick} className="p-2 text-white"> + <CaretRight + size={16} + weight="bold" + className={`transition-transform ${ + isExpanded ? "rotate-90" : "" + }`} + /> + </button> + )} + </div> + {isExpanded && hasChildren && ( + <div className="mt-1 rounded-r-lg w-full"> + {childOptions.map((childOption, index) => ( + <MenuOption + key={index} + {...childOption} // flex and roles go here. + user={user} + isChild={true} + /> + ))} + </div> + )} + </div> + ); +} + +function useIsExpanded({ + storageKey = "", + hasVisibleChildren = false, + childOptions = [], + location = null, +}) { + const [isExpanded, setIsExpanded] = useState(() => { + if (hasVisibleChildren) { + const storedValue = localStorage.getItem(storageKey); + if (storedValue !== null) { + return JSON.parse(storedValue); + } + return childOptions.some((child) => child.href === location); + } + return false; + }); + + useEffect(() => { + if (hasVisibleChildren) { + const shouldExpand = childOptions.some( + (child) => child.href === location + ); + if (shouldExpand && !isExpanded) { + setIsExpanded(true); + localStorage.setItem(storageKey, JSON.stringify(true)); + } + } + }, [location]); + + return { isExpanded, setIsExpanded }; +} + +function hasVisibleOptions(user = null, childOptions = []) { + if (!Array.isArray(childOptions) || childOptions?.length === 0) return false; + + function isVisible({ roles = [], user = null, flex = false }) { + if (!flex && !roles.includes(user?.role)) return false; + if (flex && !!user && !roles.includes(user?.role)) return false; + return true; + } + + return childOptions.some((opt) => + isVisible({ roles: opt.roles, user, flex: opt.flex }) + ); +} + +function generateStorageKey({ key = "" }) { + const _key = key.replace(/\s+/g, "_").toLowerCase(); + return `anything_llm_menu_${_key}_expanded`; +} diff --git a/frontend/src/components/SettingsSidebar/index.jsx b/frontend/src/components/SettingsSidebar/index.jsx index 4f0ea1b96..723867e2e 100644 --- a/frontend/src/components/SettingsSidebar/index.jsx +++ b/frontend/src/components/SettingsSidebar/index.jsx @@ -2,28 +2,15 @@ import React, { useEffect, useRef, useState } from "react"; import paths from "@/utils/paths"; import useLogo from "@/hooks/useLogo"; import { - EnvelopeSimple, - SquaresFour, - Users, - BookOpen, - ChatCenteredText, - Eye, - Key, - ChatText, - Database, - Lock, House, List, - FileCode, - Notepad, - CodeBlock, - Barcode, - ClosedCaptioning, - EyeSlash, - SplitVertical, - Microphone, Robot, Flask, + Gear, + UserCircleGear, + PencilSimpleLine, + Nut, + Toolbox, } from "@phosphor-icons/react"; import useUser from "@/hooks/useUser"; import { USER_BACKGROUND_COLOR } from "@/utils/constants"; @@ -32,6 +19,8 @@ import Footer from "../Footer"; import { Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; import showToast from "@/utils/toast"; +import System from "@/models/system"; +import Option from "./MenuOption"; export default function SettingsSidebar() { const { t } = useTranslation(); @@ -118,6 +107,17 @@ export default function SettingsSidebar() { <div className="h-auto md:sidebar-items md:dark:sidebar-items"> <div className="flex flex-col gap-y-4 pb-[60px] overflow-y-scroll no-scroll"> <SidebarOptions user={user} t={t} /> + <div className="h-[1.5px] bg-[#3D4147] mx-3 mt-[14px]" /> + <SupportEmail /> + <Link + hidden={ + user?.hasOwnProperty("role") && user.role !== "admin" + } + to={paths.settings.privacy()} + className="text-darker hover:text-white text-xs leading-[18px] mx-3" + > + Privacy & Data + </Link> </div> </div> </div> @@ -156,6 +156,15 @@ export default function SettingsSidebar() { <div className="h-auto sidebar-items"> <div className="flex flex-col gap-y-2 pb-[60px] overflow-y-scroll no-scroll"> <SidebarOptions user={user} t={t} /> + <div className="h-[1.5px] bg-[#3D4147] mx-3 mt-[14px]" /> + <SupportEmail /> + <Link + hidden={user?.hasOwnProperty("role") && user.role !== "admin"} + to={paths.settings.privacy()} + className="text-darker hover:text-white text-xs leading-[18px] mx-3" + > + Privacy & Data + </Link> </div> </div> </div> @@ -168,233 +177,173 @@ export default function SettingsSidebar() { ); } -const Option = ({ - btnText, - icon, - href, - childLinks = [], - flex = false, - user = null, - allowedRole = [], - subOptions = null, - hidden = false, -}) => { - if (hidden) return null; +function SupportEmail() { + const [supportEmail, setSupportEmail] = useState(paths.mailToMintplex()); - const hasActiveChild = childLinks.includes(window.location.pathname); - const isActive = window.location.pathname === href; - - // Option only for multi-user - if (!flex && !allowedRole.includes(user?.role)) return null; - - // Option is dual-mode, but user exists, we need to check permissions - if (flex && !!user && !allowedRole.includes(user?.role)) return null; + useEffect(() => { + const fetchSupportEmail = async () => { + const supportEmail = await System.fetchSupportEmail(); + setSupportEmail( + supportEmail?.email + ? `mailto:${supportEmail.email}` + : paths.mailToMintplex() + ); + }; + fetchSupportEmail(); + }, []); return ( - <> - <div className="flex gap-x-2 items-center justify-between"> - <Link - to={href} - className={` - transition-all duration-[200ms] - flex flex-grow w-[75%] gap-x-2 py-[6px] px-[12px] rounded-[4px] justify-start items-center - hover:bg-workspace-item-selected-gradient hover:text-white hover:font-medium - ${ - isActive - ? "bg-menu-item-selected-gradient font-medium border-outline text-white" - : "hover:bg-menu-item-selected-gradient text-zinc-200" - } - `} - > - {React.cloneElement(icon, { weight: isActive ? "fill" : "regular" })} - <p className="text-sm leading-loose whitespace-nowrap overflow-hidden "> - {btnText} - </p> - </Link> - </div> - {!!subOptions && (isActive || hasActiveChild) && ( - <div - className={`ml-4 ${ - hasActiveChild ? "" : "border-l-2 border-slate-400" - } rounded-r-lg`} - > - {subOptions} - </div> - )} - </> + <Link + to={supportEmail} + className="text-darker hover:text-white text-xs leading-[18px] mx-3 mt-1" + > + Contact Support + </Link> ); -}; +} const SidebarOptions = ({ user = null, t }) => ( <> <Option - href={paths.settings.system()} - btnText={t("settings.system")} - icon={<SquaresFour className="h-5 w-5 flex-shrink-0" />} + btnText={t("settings.ai-providers")} + icon={<Gear className="h-5 w-5 flex-shrink-0" />} user={user} - allowedRole={["admin", "manager"]} + childOptions={[ + { + btnText: t("settings.llm"), + href: paths.settings.llmPreference(), + flex: true, + roles: ["admin"], + }, + { + btnText: t("settings.vector-database"), + href: paths.settings.vectorDatabase(), + flex: true, + roles: ["admin"], + }, + { + btnText: t("settings.embedder"), + href: paths.settings.embedder.modelPreference(), + flex: true, + roles: ["admin"], + }, + { + btnText: t("settings.text-splitting"), + href: paths.settings.embedder.chunkingPreference(), + flex: true, + roles: ["admin"], + }, + { + btnText: "Voice & Speech", + href: paths.settings.audioPreference(), + flex: true, + roles: ["admin"], + }, + { + btnText: t("settings.transcription"), + href: paths.settings.transcriptionPreference(), + flex: true, + roles: ["admin"], + }, + ]} /> <Option - href={paths.settings.invites()} - btnText={t("settings.invites")} - icon={<EnvelopeSimple className="h-5 w-5 flex-shrink-0" />} + btnText={t("settings.admin")} + icon={<UserCircleGear className="h-5 w-5 flex-shrink-0" />} user={user} - allowedRole={["admin", "manager"]} + childOptions={[ + { + btnText: t("settings.users"), + href: paths.settings.users(), + roles: ["admin", "manager"], + }, + { + btnText: t("settings.workspaces"), + href: paths.settings.workspaces(), + roles: ["admin", "manager"], + }, + { + btnText: t("settings.workspace-chats"), + href: paths.settings.chats(), + flex: true, + roles: ["admin", "manager"], + }, + { + btnText: t("settings.invites"), + href: paths.settings.invites(), + roles: ["admin", "manager"], + }, + { + btnText: t("settings.system"), + href: paths.settings.system(), + roles: ["admin", "manager"], + }, + ]} /> <Option - href={paths.settings.users()} - btnText={t("settings.users")} - icon={<Users className="h-5 w-5 flex-shrink-0" />} - user={user} - allowedRole={["admin", "manager"]} - /> - <Option - href={paths.settings.workspaces()} - btnText={t("settings.workspaces")} - icon={<BookOpen className="h-5 w-5 flex-shrink-0" />} - user={user} - allowedRole={["admin", "manager"]} - /> - <Option - href={paths.settings.chats()} - btnText={t("settings.workspace-chats")} - icon={<ChatCenteredText className="h-5 w-5 flex-shrink-0" />} - user={user} - flex={true} - allowedRole={["admin", "manager"]} - /> - - <Option - href={paths.settings.agentSkills()} - btnText="Agent Skills" + btnText={t("settings.agent-skills")} icon={<Robot className="h-5 w-5 flex-shrink-0" />} + href={paths.settings.agentSkills()} user={user} flex={true} - allowedRole={["admin", "manager"]} + roles={["admin"]} /> <Option + btnText={t("settings.customization")} + icon={<PencilSimpleLine className="h-5 w-5 flex-shrink-0" />} href={paths.settings.appearance()} - btnText={t("settings.appearance")} - icon={<Eye className="h-5 w-5 flex-shrink-0" />} user={user} flex={true} - allowedRole={["admin", "manager"]} + roles={["admin", "manager"]} /> <Option - href={paths.settings.apiKeys()} - btnText={t("settings.api-keys")} - icon={<Key className="h-5 w-5 flex-shrink-0" />} + btnText={t("settings.tools")} + icon={<Toolbox className="h-5 w-5 flex-shrink-0" />} user={user} - flex={true} - allowedRole={["admin"]} + childOptions={[ + { + btnText: t("settings.embed-chats"), + href: paths.settings.embedChats(), + flex: true, + roles: ["admin"], + }, + { + btnText: t("settings.embeds"), + href: paths.settings.embedSetup(), + flex: true, + roles: ["admin"], + }, + { + btnText: t("settings.event-logs"), + href: paths.settings.logs(), + flex: true, + roles: ["admin"], + }, + { + btnText: t("settings.api-keys"), + href: paths.settings.apiKeys(), + flex: true, + roles: ["admin"], + }, + ]} /> <Option - href={paths.settings.llmPreference()} - btnText={t("settings.llm")} - icon={<ChatText className="h-5 w-5 flex-shrink-0" />} - user={user} - flex={true} - allowedRole={["admin"]} - /> - <Option - href={paths.settings.audioPreference()} - btnText="Voice and Speech Support" - icon={<Microphone className="h-5 w-5 flex-shrink-0" />} - user={user} - flex={true} - allowedRole={["admin"]} - /> - <Option - href={paths.settings.transcriptionPreference()} - btnText={t("settings.transcription")} - icon={<ClosedCaptioning className="h-5 w-5 flex-shrink-0" />} - user={user} - flex={true} - allowedRole={["admin"]} - /> - <Option - href={paths.settings.embedder.modelPreference()} - childLinks={[paths.settings.embedder.chunkingPreference()]} - btnText={t("settings.embedder")} - icon={<FileCode className="h-5 w-5 flex-shrink-0" />} - user={user} - flex={true} - allowedRole={["admin"]} - subOptions={ - <> - <Option - href={paths.settings.embedder.chunkingPreference()} - btnText={t("settings.text-splitting")} - icon={<SplitVertical className="h-5 w-5 flex-shrink-0" />} - user={user} - flex={true} - allowedRole={["admin"]} - /> - </> - } - /> - <Option - href={paths.settings.vectorDatabase()} - btnText={t("settings.vector-database")} - icon={<Database className="h-5 w-5 flex-shrink-0" />} - user={user} - flex={true} - allowedRole={["admin"]} - /> - <Option - href={paths.settings.embedSetup()} - childLinks={[paths.settings.embedChats()]} - btnText={t("settings.embeds")} - icon={<CodeBlock className="h-5 w-5 flex-shrink-0" />} - user={user} - flex={true} - allowedRole={["admin"]} - subOptions={ - <> - <Option - href={paths.settings.embedChats()} - btnText={t("settings.embed-chats")} - icon={<Barcode className="h-5 w-5 flex-shrink-0" />} - user={user} - flex={true} - allowedRole={["admin"]} - /> - </> - } - /> - <Option - href={paths.settings.security()} btnText={t("settings.security")} - icon={<Lock className="h-5 w-5 flex-shrink-0" />} + icon={<Nut className="h-5 w-5 flex-shrink-0" />} + href={paths.settings.security()} user={user} flex={true} - allowedRole={["admin", "manager"]} + roles={["admin", "manager"]} hidden={user?.role} /> - <Option - href={paths.settings.logs()} - btnText={t("settings.event-logs")} - icon={<Notepad className="h-5 w-5 flex-shrink-0" />} - user={user} - flex={true} - allowedRole={["admin"]} - /> - <Option - href={paths.settings.privacy()} - btnText={t("settings.privacy")} - icon={<EyeSlash className="h-5 w-5 flex-shrink-0" />} - user={user} - flex={true} - allowedRole={["admin"]} - /> <HoldToReveal key="exp_features"> <Option - href={paths.settings.experimental()} btnText="Experimental Features" icon={<Flask className="h-5 w-5 flex-shrink-0" />} + href={paths.settings.experimental()} user={user} flex={true} - allowedRole={["admin"]} + roles={["admin"]} /> </HoldToReveal> </> diff --git a/frontend/src/locales/en/common.js b/frontend/src/locales/en/common.js index 6d4aa2ba1..f54521da6 100644 --- a/frontend/src/locales/en/common.js +++ b/frontend/src/locales/en/common.js @@ -14,23 +14,27 @@ const TRANSLATIONS = { // Setting Sidebar menu items. settings: { title: "Instance Settings", - system: "System Preferences", - invites: "Invitation", + system: "General Settings", + invites: "Invites", users: "Users", workspaces: "Workspaces", - "workspace-chats": "Workspace Chat", - appearance: "Appearance", - "api-keys": "API Keys", - llm: "LLM Preference", - transcription: "Transcription Model", - embedder: "Embedding Preferences", + "workspace-chats": "Workspace Chats", + customization: "Customization", + "api-keys": "Developer API", + llm: "LLM", + transcription: "Transcription", + embedder: "Embedder", "text-splitting": "Text Splitter & Chunking", "vector-database": "Vector Database", - embeds: "Chat Embed Widgets", + embeds: "Chat Embed", "embed-chats": "Chat Embed History", security: "Security", "event-logs": "Event Logs", privacy: "Privacy & Data", + "ai-providers": "AI Providers", + "agent-skills": "Agent Skills", + admin: "Admin", + tools: "Tools", }, // Page Definitions diff --git a/frontend/src/locales/es/common.js b/frontend/src/locales/es/common.js index 5dc2daad9..4430a3cb9 100644 --- a/frontend/src/locales/es/common.js +++ b/frontend/src/locales/es/common.js @@ -18,7 +18,7 @@ const TRANSLATIONS = { users: "Usuarios", workspaces: "Espacios de trabajo", "workspace-chats": "Chat del espacio de trabajo", - appearance: "Apariencia", + customization: "Apariencia", "api-keys": "Claves API", llm: "Preferencia de LLM", transcription: "Modelo de transcripción", @@ -30,6 +30,10 @@ const TRANSLATIONS = { security: "Seguridad", "event-logs": "Registros de eventos", privacy: "Privacidad y datos", + "ai-providers": "Proveedores de IA", + "agent-skills": "Habilidades del agente", + admin: "Administrador", + tools: "Herramientas", }, login: { diff --git a/frontend/src/locales/fr/common.js b/frontend/src/locales/fr/common.js index 71c37a7ef..84a27f614 100644 --- a/frontend/src/locales/fr/common.js +++ b/frontend/src/locales/fr/common.js @@ -19,7 +19,7 @@ const TRANSLATIONS = { users: "Utilisateurs", workspaces: "Espaces de travail", "workspace-chats": "Chat de l'espace de travail", - appearance: "Apparence", + customization: "Apparence", "api-keys": "Clés API", llm: "Préférence LLM", transcription: "Modèle de transcription", @@ -31,6 +31,10 @@ const TRANSLATIONS = { security: "Sécurité", "event-logs": "Journaux d'événements", privacy: "Confidentialité et données", + "ai-providers": "Fournisseurs d'IA", + "agent-skills": "Compétences de l'agent", + admin: "Admin", + tools: "Outils", }, // Page Definitions diff --git a/frontend/src/locales/ru/common.js b/frontend/src/locales/ru/common.js index da3b49f10..a9cbdbc3c 100644 --- a/frontend/src/locales/ru/common.js +++ b/frontend/src/locales/ru/common.js @@ -17,7 +17,7 @@ const TRANSLATIONS = { users: "Пользователи", workspaces: "Рабочие пространства", "workspace-chats": "Чат рабочего пространства", - appearance: "Внешний вид", + customization: "Внешний вид", "api-keys": "API ключи", llm: "Предпочтение LLM", transcription: "Модель транскрипции", @@ -29,6 +29,10 @@ const TRANSLATIONS = { security: "Безопасность", "event-logs": "Журналы событий", privacy: "Конфиденциальность и данные", + "ai-providers": "Поставщики ИИ", + "agent-skills": "Навыки агента", + admin: "Администратор", + tools: "Инструменты", }, login: { "multi-user": { diff --git a/frontend/src/locales/zh/common.js b/frontend/src/locales/zh/common.js index 476533d12..8723f7ec9 100644 --- a/frontend/src/locales/zh/common.js +++ b/frontend/src/locales/zh/common.js @@ -20,7 +20,7 @@ const TRANSLATIONS = { users: "用户", workspaces: "工作区", "workspace-chats": "对话历史记录", // "workspace-chats" should be "对话历史记录", means "chat history",or "chat history records" - appearance: "外观", + customization: "外观", "api-keys": "API 密钥", llm: "LLM 首选项", transcription: "Transcription 模型", @@ -32,6 +32,10 @@ const TRANSLATIONS = { security: "用户与安全", "event-logs": "事件日志", privacy: "隐私与数据", + "ai-providers": "人工智能提供商", + "agent-skills": "代理技能", + admin: "管理员", + tools: "工具", }, // Page Definitions diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 098486b23..e6b5baa86 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1,6 +1,6 @@ /** @type {import('tailwindcss').Config} */ export default { - darkMode: 'false', + darkMode: "false", content: { relative: true, files: [ @@ -11,7 +11,7 @@ export default { "./src/utils/**/*.js", "./src/*.jsx", "./index.html", - './node_modules/@tremor/**/*.{js,ts,jsx,tsx}' + "./node_modules/@tremor/**/*.{js,ts,jsx,tsx}" ] }, theme: { @@ -35,7 +35,8 @@ export default { "dark-highlight": "#1C1E21", "dark-text": "#222628", description: "#D2D5DB", - "x-button": "#9CA3AF" + "x-button": "#9CA3AF", + darker: "#F4F4F4" }, backgroundImage: { "preference-gradient": @@ -101,30 +102,30 @@ export default { { pattern: /^(bg-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, - variants: ['hover', 'ui-selected'], + variants: ["hover", "ui-selected"] }, { pattern: /^(text-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, - variants: ['hover', 'ui-selected'], + variants: ["hover", "ui-selected"] }, { pattern: /^(border-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, - variants: ['hover', 'ui-selected'], + variants: ["hover", "ui-selected"] }, { pattern: - /^(ring-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, + /^(ring-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/ }, { pattern: - /^(stroke-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, + /^(stroke-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/ }, { pattern: - /^(fill-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/, - }, + /^(fill-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950))$/ + } ], plugins: [] }