{
diff --git a/apps/desktop/src/components/main/body/sessions/note-input/transcript/search-bar.tsx b/apps/desktop/src/components/main/body/sessions/note-input/transcript/search-bar.tsx
index e1a36d3439..e671bfb8d2 100644
--- a/apps/desktop/src/components/main/body/sessions/note-input/transcript/search-bar.tsx
+++ b/apps/desktop/src/components/main/body/sessions/note-input/transcript/search-bar.tsx
@@ -1,17 +1,109 @@
-import { ChevronDownIcon, ChevronUpIcon, XIcon } from "lucide-react";
+import {
+ ALargeSmallIcon,
+ ChevronDownIcon,
+ ChevronUpIcon,
+ ReplaceAllIcon,
+ ReplaceIcon,
+ WholeWordIcon,
+ XIcon,
+} from "lucide-react";
import { useEffect, useRef } from "react";
+import { Kbd } from "@hypr/ui/components/ui/kbd";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@hypr/ui/components/ui/tooltip";
import { cn } from "@hypr/utils";
import { useTranscriptSearch } from "./search-context";
+function ToggleButton({
+ active,
+ onClick,
+ tooltip,
+ children,
+}: {
+ active: boolean;
+ onClick: () => void;
+ tooltip: React.ReactNode;
+ children: React.ReactNode;
+}) {
+ return (
+
+
+
+
+
+ {tooltip}
+
+
+ );
+}
+
+function IconButton({
+ onClick,
+ disabled,
+ tooltip,
+ children,
+}: {
+ onClick: () => void;
+ disabled?: boolean;
+ tooltip: React.ReactNode;
+ children: React.ReactNode;
+}) {
+ const btn = (
+
+ );
+
+ if (disabled) return btn;
+
+ return (
+
+ {btn}
+
+ {tooltip}
+
+
+ );
+}
+
export function SearchBar() {
const search = useTranscriptSearch();
- const inputRef = useRef
(null);
+ const searchInputRef = useRef(null);
+ const replaceInputRef = useRef(null);
useEffect(() => {
- inputRef.current?.focus();
- }, [inputRef]);
+ searchInputRef.current?.focus();
+ }, []);
+
+ useEffect(() => {
+ if (search?.showReplace) {
+ replaceInputRef.current?.focus();
+ }
+ }, [search?.showReplace]);
if (!search) {
return null;
@@ -25,9 +117,19 @@ export function SearchBar() {
onNext,
onPrev,
close,
+ caseSensitive,
+ wholeWord,
+ showReplace,
+ replaceQuery,
+ toggleCaseSensitive,
+ toggleWholeWord,
+ toggleReplace,
+ setReplaceQuery,
+ replaceCurrent,
+ replaceAll,
} = search;
- const handleKeyDown = (e: React.KeyboardEvent) => {
+ const handleSearchKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter") {
e.preventDefault();
if (e.shiftKey) {
@@ -38,67 +140,139 @@ export function SearchBar() {
}
};
+ const handleReplaceKeyDown = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ if (e.metaKey || e.ctrlKey) {
+ replaceAll();
+ } else {
+ replaceCurrent();
+ }
+ }
+ };
+
const displayCount =
totalMatches > 0 ? `${currentMatchIndex + 1}/${totalMatches}` : "0/0";
return (
-
-
+
+
setQuery(e.target.value)}
- onKeyDown={handleKeyDown}
- placeholder="Search in transcript..."
- className={cn([
- "flex-1 h-full px-2 text-sm",
- "bg-neutral-100 border border-neutral-200 rounded-xs",
- "focus:outline-hidden focus:border-neutral-400",
- ])}
+ onKeyDown={handleSearchKeyDown}
+ placeholder="Search..."
+ className="flex-1 min-w-0 h-full bg-transparent text-xs placeholder:text-neutral-400 focus:outline-hidden"
/>
-
+
+
+
+
+
+
+
+
+ Replace
+ ⌘ H
+ >
+ }
+ >
+
+
+
+
{displayCount}
-
-
-
+
+ Close
+ Esc
+ >
+ }
>
-
-
+
+
+
+ {showReplace && (
+
+
setReplaceQuery(e.target.value)}
+ onKeyDown={handleReplaceKeyDown}
+ placeholder="Replace with..."
+ className="flex-1 min-w-0 h-full bg-transparent text-xs placeholder:text-neutral-400 focus:outline-hidden"
+ />
+
+
+ Replace
+ ↵
+ >
+ }
+ >
+
+
+
+ Replace all
+ ⌘ ↵
+ >
+ }
+ >
+
+
+
+
+ )}
);
}
diff --git a/apps/desktop/src/components/main/body/sessions/note-input/transcript/search-context.tsx b/apps/desktop/src/components/main/body/sessions/note-input/transcript/search-context.tsx
index ebb80d9fd4..359444333a 100644
--- a/apps/desktop/src/components/main/body/sessions/note-input/transcript/search-context.tsx
+++ b/apps/desktop/src/components/main/body/sessions/note-input/transcript/search-context.tsx
@@ -9,16 +9,31 @@ import {
} from "react";
import { useHotkeys } from "react-hotkeys-hook";
+export interface SearchOptions {
+ caseSensitive: boolean;
+ wholeWord: boolean;
+}
+
interface SearchContextValue {
query: string;
isVisible: boolean;
currentMatchIndex: number;
totalMatches: number;
activeMatchId: string | null;
+ caseSensitive: boolean;
+ wholeWord: boolean;
+ showReplace: boolean;
+ replaceQuery: string;
onNext: () => void;
onPrev: () => void;
close: () => void;
setQuery: (query: string) => void;
+ toggleCaseSensitive: () => void;
+ toggleWholeWord: () => void;
+ toggleReplace: () => void;
+ setReplaceQuery: (query: string) => void;
+ replaceCurrent: () => void;
+ replaceAll: () => void;
}
const SearchContext = createContext
(null);
@@ -27,23 +42,83 @@ export function useTranscriptSearch() {
return useContext(SearchContext);
}
+interface MatchResult {
+ element: HTMLElement;
+ id: string | null;
+}
+
+function prepareQuery(query: string, caseSensitive: boolean): string {
+ const trimmed = query.trim().normalize("NFC");
+ return caseSensitive ? trimmed : trimmed.toLowerCase();
+}
+
+function prepareText(text: string, caseSensitive: boolean): string {
+ const normalized = text.normalize("NFC");
+ return caseSensitive ? normalized : normalized.toLowerCase();
+}
+
+function isWordBoundary(text: string, index: number): boolean {
+ if (index < 0 || index >= text.length) return true;
+ return !/\w/.test(text[index]);
+}
+
+function findOccurrences(
+ text: string,
+ query: string,
+ wholeWord: boolean,
+): number[] {
+ const indices: number[] = [];
+ let from = 0;
+ while (from <= text.length - query.length) {
+ const idx = text.indexOf(query, from);
+ if (idx === -1) break;
+ if (wholeWord) {
+ const beforeOk = isWordBoundary(text, idx - 1);
+ const afterOk = isWordBoundary(text, idx + query.length);
+ if (beforeOk && afterOk) {
+ indices.push(idx);
+ }
+ } else {
+ indices.push(idx);
+ }
+ from = idx + 1;
+ }
+ return indices;
+}
+
function getMatchingElements(
container: HTMLElement | null,
query: string,
-): HTMLElement[] {
- if (!container || !query) {
- return [];
- }
+ opts: SearchOptions,
+): MatchResult[] {
+ if (!container || !query) return [];
- const normalizedQuery = query.trim().toLowerCase().normalize("NFC");
- if (!normalizedQuery) return [];
+ const prepared = prepareQuery(query, opts.caseSensitive);
+ if (!prepared) return [];
- const allSpans = Array.from(
+ const wordSpans = Array.from(
container.querySelectorAll("[data-word-id]"),
);
- if (allSpans.length === 0) return [];
- // Build concatenated text from all spans, tracking each span's position
+ if (wordSpans.length > 0) {
+ return getTranscriptMatches(wordSpans, prepared, opts);
+ }
+
+ const proseMirror =
+ container.querySelector(".ProseMirror") ??
+ (container.classList.contains("ProseMirror") ? container : null);
+ if (proseMirror) {
+ return getEditorMatches(proseMirror, prepared, opts);
+ }
+
+ return [];
+}
+
+function getTranscriptMatches(
+ allSpans: HTMLElement[],
+ prepared: string,
+ opts: SearchOptions,
+): MatchResult[] {
const spanPositions: { start: number; end: number }[] = [];
let fullText = "";
@@ -55,77 +130,135 @@ function getMatchingElements(
spanPositions.push({ start, end: fullText.length });
}
- const lowerFullText = fullText.toLowerCase();
- const result: HTMLElement[] = [];
- let searchFrom = 0;
+ const searchText = prepareText(fullText, opts.caseSensitive);
+ const indices = findOccurrences(searchText, prepared, opts.wholeWord);
+ const result: MatchResult[] = [];
- while (searchFrom <= lowerFullText.length - normalizedQuery.length) {
- const idx = lowerFullText.indexOf(normalizedQuery, searchFrom);
- if (idx === -1) break;
-
- // Find the span containing the start of this match
+ for (const idx of indices) {
for (let i = 0; i < spanPositions.length; i++) {
const { start, end } = spanPositions[i];
if (idx >= start && idx < end) {
- result.push(allSpans[i]);
+ result.push({
+ element: allSpans[i],
+ id: allSpans[i].dataset.wordId || null,
+ });
break;
}
- // Match starts in the space between spans
if (
i < spanPositions.length - 1 &&
idx >= end &&
idx < spanPositions[i + 1].start
) {
- result.push(allSpans[i + 1]);
+ result.push({
+ element: allSpans[i + 1],
+ id: allSpans[i + 1].dataset.wordId || null,
+ });
break;
}
}
+ }
+
+ return result;
+}
- searchFrom = idx + 1;
+function getEditorMatches(
+ proseMirror: HTMLElement,
+ prepared: string,
+ opts: SearchOptions,
+): MatchResult[] {
+ const allBlocks = Array.from(
+ proseMirror.querySelectorAll(
+ "p, h1, h2, h3, h4, h5, h6, li, blockquote, td, th",
+ ),
+ );
+
+ const blocks = allBlocks.filter(
+ (el) => !allBlocks.some((other) => other !== el && other.contains(el)),
+ );
+
+ const result: MatchResult[] = [];
+
+ for (const block of blocks) {
+ const text = prepareText(block.textContent || "", opts.caseSensitive);
+ const indices = findOccurrences(text, prepared, opts.wholeWord);
+ for (const _ of indices) {
+ result.push({ element: block, id: null });
+ }
}
return result;
}
-export function SearchProvider({ children }: { children: React.ReactNode }) {
+function findSearchContainer(): HTMLElement | null {
+ if (typeof document === "undefined") return null;
+
+ const transcript = document.querySelector(
+ "[data-transcript-container]",
+ );
+ if (transcript) return transcript;
+
+ const proseMirror = document.querySelector(".ProseMirror");
+ if (proseMirror) {
+ return proseMirror.parentElement ?? proseMirror;
+ }
+
+ return null;
+}
+
+export interface SearchReplaceDetail {
+ query: string;
+ replacement: string;
+ caseSensitive: boolean;
+ wholeWord: boolean;
+ all: boolean;
+ matchIndex: number;
+ sessionId: string;
+}
+
+export function SearchProvider({
+ children,
+ sessionId,
+}: {
+ children: React.ReactNode;
+ sessionId: string;
+}) {
const [isVisible, setIsVisible] = useState(false);
const [query, setQuery] = useState("");
const [currentMatchIndex, setCurrentMatchIndex] = useState(0);
const [totalMatches, setTotalMatches] = useState(0);
const [activeMatchId, setActiveMatchId] = useState(null);
- const containerRef = useRef(null);
+ const [caseSensitive, setCaseSensitive] = useState(false);
+ const [wholeWord, setWholeWord] = useState(false);
+ const [showReplace, setShowReplace] = useState(false);
+ const [replaceQuery, setReplaceQuery] = useState("");
+ const matchesRef = useRef([]);
+
+ const opts: SearchOptions = useMemo(
+ () => ({ caseSensitive, wholeWord }),
+ [caseSensitive, wholeWord],
+ );
- const ensureContainer = useCallback(() => {
- if (typeof document === "undefined") {
- containerRef.current = null;
- return null;
- }
+ const close = useCallback(() => {
+ setIsVisible(false);
+ setShowReplace(false);
+ }, []);
- const current = containerRef.current;
- if (current && document.body.contains(current)) {
- return current;
- }
+ const toggleCaseSensitive = useCallback(() => {
+ setCaseSensitive((prev) => !prev);
+ }, []);
- const next = document.querySelector(
- "[data-transcript-container]",
- );
- containerRef.current = next;
- return next;
+ const toggleWholeWord = useCallback(() => {
+ setWholeWord((prev) => !prev);
}, []);
- const close = useCallback(() => {
- setIsVisible(false);
+ const toggleReplace = useCallback(() => {
+ setShowReplace((prev) => !prev);
}, []);
useHotkeys(
"mod+f",
(event) => {
event.preventDefault();
- const container = ensureContainer();
- if (!container) {
- return;
- }
-
setIsVisible((prev) => !prev);
},
{
@@ -133,7 +266,22 @@ export function SearchProvider({ children }: { children: React.ReactNode }) {
enableOnFormTags: true,
enableOnContentEditable: true,
},
- [ensureContainer],
+ [],
+ );
+
+ useHotkeys(
+ "mod+h",
+ (event) => {
+ event.preventDefault();
+ setIsVisible(true);
+ setShowReplace((prev) => !prev);
+ },
+ {
+ preventDefault: true,
+ enableOnFormTags: true,
+ enableOnContentEditable: true,
+ },
+ [],
);
useHotkeys(
@@ -152,78 +300,111 @@ export function SearchProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
if (!isVisible) {
setQuery("");
+ setReplaceQuery("");
setCurrentMatchIndex(0);
setActiveMatchId(null);
+ setShowReplace(false);
+ matchesRef.current = [];
}
}, [isVisible]);
- useEffect(() => {
- const container = ensureContainer();
+ const runSearch = useCallback(() => {
+ const container = findSearchContainer();
if (!container || !query) {
setTotalMatches(0);
setCurrentMatchIndex(0);
setActiveMatchId(null);
+ matchesRef.current = [];
return;
}
- const matches = getMatchingElements(container, query);
+ const matches = getMatchingElements(container, query, opts);
+ matchesRef.current = matches;
setTotalMatches(matches.length);
setCurrentMatchIndex(0);
- setActiveMatchId(matches[0]?.dataset.wordId || null);
- }, [query, ensureContainer]);
+ setActiveMatchId(matches[0]?.id || null);
- const onNext = useCallback(() => {
- const container = ensureContainer();
- if (!container) {
- return;
+ if (matches.length > 0 && !matches[0].id) {
+ matches[0].element.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ });
}
+ }, [query, opts]);
- const matches = getMatchingElements(container, query);
- if (matches.length === 0) {
- return;
- }
+ useEffect(() => {
+ runSearch();
+ }, [runSearch]);
+
+ const onNext = useCallback(() => {
+ const matches = matchesRef.current;
+ if (matches.length === 0) return;
const nextIndex = (currentMatchIndex + 1) % matches.length;
setCurrentMatchIndex(nextIndex);
- setActiveMatchId(matches[nextIndex]?.dataset.wordId || null);
- }, [ensureContainer, query, currentMatchIndex]);
+ setActiveMatchId(matches[nextIndex]?.id || null);
+ matches[nextIndex]?.element.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ });
+ }, [currentMatchIndex]);
const onPrev = useCallback(() => {
- const container = ensureContainer();
- if (!container) {
- return;
- }
-
- const matches = getMatchingElements(container, query);
- if (matches.length === 0) {
- return;
- }
+ const matches = matchesRef.current;
+ if (matches.length === 0) return;
const prevIndex = (currentMatchIndex - 1 + matches.length) % matches.length;
setCurrentMatchIndex(prevIndex);
- setActiveMatchId(matches[prevIndex]?.dataset.wordId || null);
- }, [ensureContainer, query, currentMatchIndex]);
-
- useEffect(() => {
- if (!isVisible) {
- return;
- }
-
- const container = ensureContainer();
- if (!container) {
- setIsVisible(false);
- }
- }, [isVisible, ensureContainer]);
+ setActiveMatchId(matches[prevIndex]?.id || null);
+ matches[prevIndex]?.element.scrollIntoView({
+ behavior: "smooth",
+ block: "center",
+ });
+ }, [currentMatchIndex]);
+
+ const replaceCurrent = useCallback(() => {
+ if (!query || matchesRef.current.length === 0) return;
+ const detail: SearchReplaceDetail = {
+ query,
+ replacement: replaceQuery,
+ caseSensitive,
+ wholeWord,
+ all: false,
+ matchIndex: currentMatchIndex,
+ sessionId,
+ };
+ window.dispatchEvent(new CustomEvent("search-replace", { detail }));
+ setTimeout(runSearch, 50);
+ }, [
+ query,
+ replaceQuery,
+ caseSensitive,
+ wholeWord,
+ currentMatchIndex,
+ runSearch,
+ sessionId,
+ ]);
+
+ const replaceAllFn = useCallback(() => {
+ if (!query) return;
+ const detail: SearchReplaceDetail = {
+ query,
+ replacement: replaceQuery,
+ caseSensitive,
+ wholeWord,
+ all: true,
+ matchIndex: 0,
+ sessionId,
+ };
+ window.dispatchEvent(new CustomEvent("search-replace", { detail }));
+ setTimeout(runSearch, 50);
+ }, [query, replaceQuery, caseSensitive, wholeWord, runSearch, sessionId]);
useEffect(() => {
- if (!isVisible || !activeMatchId) {
- return;
- }
+ if (!isVisible || !activeMatchId) return;
- const container = ensureContainer();
- if (!container) {
- return;
- }
+ const container = findSearchContainer();
+ if (!container) return;
const element = container.querySelector(
`[data-word-id="${activeMatchId}"]`,
@@ -232,7 +413,7 @@ export function SearchProvider({ children }: { children: React.ReactNode }) {
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "center" });
}
- }, [isVisible, activeMatchId, ensureContainer]);
+ }, [isVisible, activeMatchId]);
const value = useMemo(
() => ({
@@ -241,10 +422,20 @@ export function SearchProvider({ children }: { children: React.ReactNode }) {
currentMatchIndex,
totalMatches,
activeMatchId,
+ caseSensitive,
+ wholeWord,
+ showReplace,
+ replaceQuery,
onNext,
onPrev,
close,
setQuery,
+ toggleCaseSensitive,
+ toggleWholeWord,
+ toggleReplace,
+ setReplaceQuery,
+ replaceCurrent,
+ replaceAll: replaceAllFn,
}),
[
query,
@@ -252,9 +443,18 @@ export function SearchProvider({ children }: { children: React.ReactNode }) {
currentMatchIndex,
totalMatches,
activeMatchId,
+ caseSensitive,
+ wholeWord,
+ showReplace,
+ replaceQuery,
onNext,
onPrev,
close,
+ toggleCaseSensitive,
+ toggleWholeWord,
+ toggleReplace,
+ replaceCurrent,
+ replaceAllFn,
],
);
diff --git a/apps/desktop/src/components/main/body/sessions/note-input/transcript/shared/word-span.tsx b/apps/desktop/src/components/main/body/sessions/note-input/transcript/shared/word-span.tsx
index 5dfb86e356..1bea930f51 100644
--- a/apps/desktop/src/components/main/body/sessions/note-input/transcript/shared/word-span.tsx
+++ b/apps/desktop/src/components/main/body/sessions/note-input/transcript/shared/word-span.tsx
@@ -61,6 +61,8 @@ function useTranscriptSearchHighlights(word: SegmentWord) {
const query = search?.query?.trim() ?? "";
const isVisible = Boolean(search?.isVisible);
const activeMatchId = search?.activeMatchId ?? null;
+ const caseSensitive = search?.caseSensitive ?? false;
+ const wholeWord = search?.wholeWord ?? false;
const segments = useMemo(() => {
const text = word.text ?? "";
@@ -73,8 +75,8 @@ function useTranscriptSearchHighlights(word: SegmentWord) {
return [{ text, isMatch: false }];
}
- return createSearchHighlightSegments(text, query);
- }, [isVisible, query, word.text]);
+ return createSearchHighlightSegments(text, query, caseSensitive, wholeWord);
+ }, [isVisible, query, word.text, caseSensitive, wholeWord]);
const isActive = word.id === activeMatchId;
diff --git a/apps/desktop/src/components/main/body/sessions/outer-header/listen.tsx b/apps/desktop/src/components/main/body/sessions/outer-header/listen.tsx
index 31f3793530..145daac129 100644
--- a/apps/desktop/src/components/main/body/sessions/outer-header/listen.tsx
+++ b/apps/desktop/src/components/main/body/sessions/outer-header/listen.tsx
@@ -56,8 +56,6 @@ function StartButton({ sessionId }: { sessionId: string }) {
"w-20 h-7",
"disabled:pointer-events-none disabled:opacity-50",
])}
- title={warningMessage || "Listen"}
- aria-label="Listen"
>
Listen
@@ -65,7 +63,16 @@ function StartButton({ sessionId }: { sessionId: string }) {
);
if (!warningMessage) {
- return button;
+ return (
+
+
+ {button}
+
+
+ Make Char listen to your meeting
+
+
+ );
}
return (
@@ -104,56 +111,65 @@ function InMeetingIndicator({ sessionId }: { sessionId: string }) {
}
return (
-
+
+
+
+
+
+ {finalizing ? "Finalizing..." : "Stop listening"}
+
+
);
}
diff --git a/apps/desktop/src/components/main/body/sessions/title-input.tsx b/apps/desktop/src/components/main/body/sessions/title-input.tsx
index 87c713fa9b..85c63ee683 100644
--- a/apps/desktop/src/components/main/body/sessions/title-input.tsx
+++ b/apps/desktop/src/components/main/body/sessions/title-input.tsx
@@ -10,6 +10,11 @@ import {
useState,
} from "react";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "@hypr/ui/components/ui/tooltip";
import { cn } from "@hypr/utils";
import { useTitleGenerating } from "../../../../hooks/useTitleGenerating";
@@ -326,20 +331,25 @@ const GenerateButton = memo(function GenerateButton({
onGenerateTitle: () => void;
}) {
return (
-
+
+
+
+
+ Regenerate title
+
);
});
diff --git a/apps/desktop/src/components/main/sidebar/index.tsx b/apps/desktop/src/components/main/sidebar/index.tsx
index 167b52e220..ea5ef8d296 100644
--- a/apps/desktop/src/components/main/sidebar/index.tsx
+++ b/apps/desktop/src/components/main/sidebar/index.tsx
@@ -71,10 +71,7 @@ export function LeftSidebar() {
-
+
Toggle sidebar
⌘ \
diff --git a/packages/transcript/src/ui/utils.ts b/packages/transcript/src/ui/utils.ts
index 53a21082a2..505c5ec074 100644
--- a/packages/transcript/src/ui/utils.ts
+++ b/packages/transcript/src/ui/utils.ts
@@ -105,28 +105,43 @@ export function useSegmentColor(key: SegmentKey): string {
return useMemo(() => getSegmentColor(key), [key]);
}
+function isWordBoundaryChar(text: string, index: number): boolean {
+ if (index < 0 || index >= text.length) return true;
+ return !/\w/.test(text[index]);
+}
+
export function createSearchHighlightSegments(
rawText: string,
query: string,
+ caseSensitive = false,
+ wholeWord = false,
): HighlightSegment[] {
const text = rawText.normalize("NFC");
- const lowerText = text.toLowerCase();
+ const searchText = caseSensitive ? text : text.toLowerCase();
const tokens = query
.normalize("NFC")
- .toLowerCase()
.split(/\s+/)
- .filter(Boolean);
+ .filter(Boolean)
+ .map((t) => (caseSensitive ? t : t.toLowerCase()));
if (tokens.length === 0) return [{ text, isMatch: false }];
const ranges: { start: number; end: number }[] = [];
for (const token of tokens) {
let cursor = 0;
- let index = lowerText.indexOf(token, cursor);
+ let index = searchText.indexOf(token, cursor);
while (index !== -1) {
- ranges.push({ start: index, end: index + token.length });
+ if (wholeWord) {
+ const beforeOk = isWordBoundaryChar(searchText, index - 1);
+ const afterOk = isWordBoundaryChar(searchText, index + token.length);
+ if (beforeOk && afterOk) {
+ ranges.push({ start: index, end: index + token.length });
+ }
+ } else {
+ ranges.push({ start: index, end: index + token.length });
+ }
cursor = index + 1;
- index = lowerText.indexOf(token, cursor);
+ index = searchText.indexOf(token, cursor);
}
}
diff --git a/packages/ui/src/components/ui/tooltip.tsx b/packages/ui/src/components/ui/tooltip.tsx
index 0f053468a5..c2506a0ae1 100644
--- a/packages/ui/src/components/ui/tooltip.tsx
+++ b/packages/ui/src/components/ui/tooltip.tsx
@@ -52,7 +52,8 @@ const TooltipContent = React.forwardRef<
ease: [0.16, 1, 0.3, 1],
}}
className={cn([
- "z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground",
+ "z-50 overflow-hidden rounded-md px-3 py-1.5 text-xs",
+ "bg-white/80 backdrop-blur-sm text-neutral-700 border border-neutral-200/50 shadow-lg",
"origin-(--radix-tooltip-content-transform-origin)",
className,
])}