From e6fa01d514c1f26e44563d2d3f728691b8c2aaec Mon Sep 17 00:00:00 2001 From: Marco Beretta <81851188+berry-13@users.noreply.github.com> Date: Thu, 7 Aug 2025 22:24:42 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=92=AC=20style:=20Enhance=20Tooltip=20wit?= =?UTF-8?q?h=20HTML=20support=20and=20Improve=20Styling=20(#8915)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ feat: Enhance Tooltip component with HTML support and styling improvements * ✨ feat: Integrate DOMPurify for HTML sanitization in Tooltip component --- .../components/MCP/CustomUserVarsSection.tsx | 3 +- package-lock.json | 61 ++++--------------- packages/client/package.json | 3 +- packages/client/src/components/Tooltip.css | 10 +++ packages/client/src/components/Tooltip.tsx | 48 +++++++++++++-- 5 files changed, 71 insertions(+), 54 deletions(-) diff --git a/client/src/components/MCP/CustomUserVarsSection.tsx b/client/src/components/MCP/CustomUserVarsSection.tsx index 051634249e..98e392554a 100644 --- a/client/src/components/MCP/CustomUserVarsSection.tsx +++ b/client/src/components/MCP/CustomUserVarsSection.tsx @@ -32,13 +32,14 @@ function AuthField({ name, config, hasValue, control, errors }: AuthFieldProps)
- +
} /> diff --git a/package-lock.json b/package-lock.json index 102f984482..dede8c7c84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2236,20 +2236,6 @@ "node": ">= 0.8.0" } }, - "api/node_modules/express-rate-limit": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.1.tgz", - "integrity": "sha512-KS3efpnpIDVIXopMc65EMbWbUht7qvTCdtCR2dD/IZmi9MIkopYESwyRqLgv8Pfu589+KqDqOdzJWW7AHoACeg==", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://github.com/sponsors/express-rate-limit" - }, - "peerDependencies": { - "express": "4 || 5 || ^5.0.0-beta.1" - } - }, "api/node_modules/express-session": { "version": "1.18.2", "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.2.tgz", @@ -2510,38 +2496,6 @@ } } }, - "api/node_modules/mongodb-connection-string-url": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz", - "integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==", - "dependencies": { - "@types/whatwg-url": "^11.0.2", - "whatwg-url": "^13.0.0" - } - }, - "api/node_modules/mongodb-connection-string-url/node_modules/tr46": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", - "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", - "dependencies": { - "punycode": "^2.3.0" - }, - "engines": { - "node": ">=14" - } - }, - "api/node_modules/mongodb-connection-string-url/node_modules/whatwg-url": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", - "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", - "dependencies": { - "tr46": "^4.1.1", - "webidl-conversions": "^7.0.0" - }, - "engines": { - "node": ">=16" - } - }, "api/node_modules/mongoose": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.12.1.tgz", @@ -29450,7 +29404,7 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true + "devOptional": true }, "node_modules/@types/unist": { "version": "2.0.10", @@ -33161,6 +33115,16 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", + "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", @@ -51507,7 +51471,7 @@ }, "packages/client": { "name": "@librechat/client", - "version": "0.2.3", + "version": "0.2.4", "devDependencies": { "@rollup/plugin-alias": "^5.1.0", "@rollup/plugin-commonjs": "^25.0.2", @@ -51560,6 +51524,7 @@ "@tanstack/react-virtual": "^3.0.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dompurify": "^3.2.6", "framer-motion": "^12.23.6", "i18next": "^24.2.2 || ^25.3.2", "i18next-browser-languagedetector": "^8.2.0", diff --git a/packages/client/package.json b/packages/client/package.json index 0e6a19aeb8..189363f521 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@librechat/client", - "version": "0.2.3", + "version": "0.2.4", "description": "React components for LibreChat", "main": "dist/index.js", "module": "dist/index.es.js", @@ -54,6 +54,7 @@ "@react-spring/web": "^10.0.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dompurify": "^3.2.6", "framer-motion": "^12.23.6", "i18next-browser-languagedetector": "^8.2.0", "input-otp": "^1.4.2", diff --git a/packages/client/src/components/Tooltip.css b/packages/client/src/components/Tooltip.css index 92e52037c4..46ecb37cb8 100644 --- a/packages/client/src/components/Tooltip.css +++ b/packages/client/src/components/Tooltip.css @@ -11,6 +11,16 @@ line-height: 1.5rem; color: black; box-shadow: 0 2px 4px 0 rgb(0 0 0 / 0.25); + /* Enhanced layout for longer descriptions */ + max-width: 320px; + word-wrap: break-word; + text-align: left; +} + +@media (max-width: 640px) { + .tooltip { + max-width: 200px; + } } .tooltip:where(.dark, .dark *) { diff --git a/packages/client/src/components/Tooltip.tsx b/packages/client/src/components/Tooltip.tsx index b83452d654..2f7675d10f 100644 --- a/packages/client/src/components/Tooltip.tsx +++ b/packages/client/src/components/Tooltip.tsx @@ -1,6 +1,7 @@ +import DOMPurify from 'dompurify'; import * as Ariakit from '@ariakit/react'; +import { forwardRef, useId, useMemo } from 'react'; import { AnimatePresence, motion } from 'framer-motion'; -import { forwardRef, useMemo } from 'react'; import { cn } from '~/utils'; import './Tooltip.css'; @@ -8,18 +9,47 @@ interface TooltipAnchorProps extends Ariakit.TooltipAnchorProps { description: string; side?: 'top' | 'bottom' | 'left' | 'right'; className?: string; - focusable?: boolean; role?: string; + enableHTML?: boolean; } export const TooltipAnchor = forwardRef(function TooltipAnchor( - { description, side = 'top', className, role, ...props }, + { description, side = 'top', className, role, enableHTML = false, ...props }, ref, ) { const tooltip = Ariakit.useTooltipStore({ placement: side }); const mounted = Ariakit.useStoreState(tooltip, (state) => state.mounted); const placement = Ariakit.useStoreState(tooltip, (state) => state.placement); + const id = useId(); + const sanitizer = useMemo(() => { + const instance = DOMPurify(); + instance.addHook('afterSanitizeAttributes', (node) => { + if (node.tagName && node.tagName === 'A') { + node.setAttribute('target', '_blank'); + node.setAttribute('rel', 'noopener noreferrer'); + } + }); + return instance; + }, []); + + const sanitizedHTML = useMemo(() => { + if (!enableHTML) { + return ''; + } + try { + return sanitizer.sanitize(description, { + ALLOWED_TAGS: ['a', 'strong', 'b', 'em', 'i', 'br', 'code'], + ALLOWED_ATTR: ['href', 'class', 'target', 'rel'], + ALLOW_DATA_ATTR: false, + ALLOW_ARIA_ATTR: false, + }); + } catch (error) { + console.error('Sanitization failed', error); + return description; + } + }, [enableHTML, description, sanitizer]); + const { x, y } = useMemo(() => { const dir = placement.split('-')[0]; switch (dir) { @@ -49,6 +79,7 @@ export const TooltipAnchor = forwardRef(func {...props} ref={ref} role={role} + aria-describedby={id} onKeyDown={handleKeyDown} className={cn('cursor-pointer', className)} /> @@ -58,6 +89,7 @@ export const TooltipAnchor = forwardRef(func gutter={4} alwaysVisible className="tooltip" + id={id} render={ (func } > - {description} + {enableHTML ? ( +
+ ) : ( + description + )} )}