feat: enhance UI components with dynamic theming and improved task handling
- Updated MemberBadge component to support an additional 'xs' size variant for better responsiveness. - Refactored TaskTooltip to improve task retrieval logic, ensuring accurate task matching based on team context. - Enhanced CreateTeamDialog and LaunchTeamDialog with dynamic background color adjustments based on theme. - Improved OptionalSettingsSection to utilize theme-aware styles for better visual consistency. - Updated MemberDraftRow to apply theme-based background and shadow effects for improved aesthetics. - Enhanced MentionSuggestionList to include dynamic theming for task owner display and team colors. - Refactored task reference utilities to improve task suggestion resolution and boundary handling.
This commit is contained in:
parent
b6ec408451
commit
057591060a
11 changed files with 198 additions and 48 deletions
|
|
@ -13,7 +13,7 @@ interface MemberBadgeProps {
|
|||
name: string;
|
||||
color?: string;
|
||||
/** Avatar + badge size variant */
|
||||
size?: 'sm' | 'md';
|
||||
size?: 'xs' | 'sm' | 'md';
|
||||
/** Hide the avatar icon, show only the name badge */
|
||||
hideAvatar?: boolean;
|
||||
onClick?: (name: string) => void;
|
||||
|
|
@ -37,9 +37,10 @@ export const MemberBadge = ({
|
|||
}: MemberBadgeProps): React.JSX.Element => {
|
||||
const colors = getTeamColorSet(color ?? '');
|
||||
const { isLight } = useTheme();
|
||||
const avatarSize = size === 'md' ? 32 : 24;
|
||||
const avatarClass = size === 'md' ? 'size-6' : 'size-5';
|
||||
const textClass = size === 'md' ? 'text-xs' : 'text-[10px]';
|
||||
const avatarSize = size === 'md' ? 32 : size === 'sm' ? 24 : 18;
|
||||
const avatarClass = size === 'md' ? 'size-6' : size === 'sm' ? 'size-5' : 'size-4';
|
||||
const textClass = size === 'md' ? 'text-xs' : size === 'sm' ? 'text-[10px]' : 'text-[9px]';
|
||||
const paddingClass = size === 'xs' ? 'px-1 py-0.5' : 'px-1.5 py-0.5';
|
||||
|
||||
const badgeStyle = {
|
||||
backgroundColor: getThemedBadge(colors, isLight),
|
||||
|
|
@ -58,7 +59,7 @@ export const MemberBadge = ({
|
|||
|
||||
const badge = (
|
||||
<span
|
||||
className={`rounded px-1.5 py-0.5 ${textClass} font-medium tracking-wide`}
|
||||
className={`rounded ${paddingClass} ${textClass} font-medium tracking-wide`}
|
||||
style={badgeStyle}
|
||||
>
|
||||
{name === 'team-lead' ? 'lead' : name}
|
||||
|
|
|
|||
|
|
@ -73,27 +73,39 @@ export const TaskTooltip = ({
|
|||
const globalTasks = useStore((s) => s.globalTasks);
|
||||
const teamByName = useStore((s) => s.teamByName);
|
||||
|
||||
const tasks = useMemo(() => {
|
||||
const task = useMemo(() => {
|
||||
if (teamName && selectedTeamName === teamName) {
|
||||
return selectedTeamData?.tasks ?? [];
|
||||
return (
|
||||
(selectedTeamData?.tasks ?? []).find((candidate) => taskMatchesRef(candidate, taskId)) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
if (teamName) {
|
||||
return globalTasks.filter((task) => task.teamName === teamName);
|
||||
return (
|
||||
globalTasks.find(
|
||||
(candidate) => candidate.teamName === teamName && taskMatchesRef(candidate, taskId)
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
const currentTasks = selectedTeamData?.tasks ?? [];
|
||||
const currentMatch = currentTasks.find((task) => taskMatchesRef(task, taskId));
|
||||
if (currentMatch) return currentTasks;
|
||||
return globalTasks;
|
||||
if (currentMatch) return currentMatch;
|
||||
|
||||
const globalMatches = globalTasks.filter((candidate) => taskMatchesRef(candidate, taskId));
|
||||
return globalMatches.length === 1 ? globalMatches[0] : null;
|
||||
}, [globalTasks, selectedTeamData, selectedTeamName, teamName, taskId]);
|
||||
|
||||
const members = useMemo(() => {
|
||||
if (teamName && selectedTeamName === teamName) {
|
||||
return selectedTeamData?.members ?? [];
|
||||
}
|
||||
if (!teamName && task && selectedTeamName === (task as { teamName?: string }).teamName) {
|
||||
return selectedTeamData?.members ?? [];
|
||||
}
|
||||
return [];
|
||||
}, [selectedTeamData, selectedTeamName, teamName]);
|
||||
|
||||
const task = useMemo(() => tasks?.find((t) => taskMatchesRef(t, taskId)), [tasks, taskId]);
|
||||
}, [selectedTeamData, selectedTeamName, teamName, task]);
|
||||
|
||||
const colorMap = useMemo(
|
||||
() => (members ? buildMemberColorMap(members) : new Map<string, string>()),
|
||||
|
|
|
|||
|
|
@ -839,18 +839,33 @@ export const CreateTeamDialog = ({
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-[var(--color-border-emphasis)] bg-[var(--color-surface-overlay)] p-4 shadow-sm md:col-span-2">
|
||||
<div
|
||||
className="rounded-lg border border-[var(--color-border-emphasis)] p-4 shadow-sm md:col-span-2"
|
||||
style={{
|
||||
backgroundColor: isLight
|
||||
? 'color-mix(in srgb, var(--color-surface-overlay) 24%, white 76%)'
|
||||
: 'var(--color-surface-overlay)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<Checkbox
|
||||
id="launch-team"
|
||||
className="mt-1 shrink-0"
|
||||
checked={launchTeam}
|
||||
onCheckedChange={(checked) => setLaunchTeam(checked === true)}
|
||||
/>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="launch-team" className="cursor-pointer text-sm font-medium">
|
||||
<Label htmlFor="launch-team" className="cursor-pointer text-sm font-semibold">
|
||||
Run command after create
|
||||
</Label>
|
||||
<p className="text-xs text-[var(--color-text-muted)]">
|
||||
<p
|
||||
className="text-xs"
|
||||
style={{
|
||||
color: isLight
|
||||
? 'color-mix(in srgb, var(--color-text-muted) 54%, var(--color-text) 46%)'
|
||||
: 'var(--color-text-muted)',
|
||||
}}
|
||||
>
|
||||
Start the team immediately via local Claude CLI.
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea
|
|||
import { useChipDraftPersistence } from '@renderer/hooks/useChipDraftPersistence';
|
||||
import { useDraftPersistence } from '@renderer/hooks/useDraftPersistence';
|
||||
import { useFileListCacheWarmer } from '@renderer/hooks/useFileListCacheWarmer';
|
||||
import { useTheme } from '@renderer/hooks/useTheme';
|
||||
import { useStore } from '@renderer/store';
|
||||
import { getTeamColorSet } from '@renderer/constants/teamColors';
|
||||
import { formatAgentRole } from '@renderer/utils/formatAgentRole';
|
||||
|
|
@ -108,6 +109,7 @@ function getLocalTimezone(): string {
|
|||
|
||||
export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Element => {
|
||||
const { open, onClose } = props;
|
||||
const { isLight } = useTheme();
|
||||
const isLaunch = props.mode === 'launch';
|
||||
const isSchedule = props.mode === 'schedule';
|
||||
const schedule = isSchedule ? ((props as LaunchDialogScheduleMode).schedule ?? null) : null;
|
||||
|
|
@ -821,7 +823,14 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
|||
Schedule-only: Schedule configuration section
|
||||
═══════════════════════════════════════════════════════════════════ */}
|
||||
{isSchedule ? (
|
||||
<div className="rounded-lg border border-[var(--color-border-emphasis)] bg-[var(--color-surface-overlay)] shadow-sm">
|
||||
<div
|
||||
className="rounded-lg border border-[var(--color-border-emphasis)] shadow-sm"
|
||||
style={{
|
||||
backgroundColor: isLight
|
||||
? 'color-mix(in srgb, var(--color-surface-overlay) 24%, white 76%)'
|
||||
: 'var(--color-surface-overlay)',
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full items-center gap-1.5 px-3 py-2 text-left"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React, { useMemo, useState } from 'react';
|
||||
|
||||
import { useTheme } from '@renderer/hooks/useTheme';
|
||||
import { cn } from '@renderer/lib/utils';
|
||||
import { ChevronRight, Settings2 } from 'lucide-react';
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ export const OptionalSettingsSection = ({
|
|||
children,
|
||||
}: OptionalSettingsSectionProps): React.JSX.Element => {
|
||||
const [isOpen, setIsOpen] = useState(defaultOpen);
|
||||
const { isLight } = useTheme();
|
||||
|
||||
const visibleSummary = useMemo(
|
||||
() =>
|
||||
|
|
@ -31,6 +33,26 @@ export const OptionalSettingsSection = ({
|
|||
[summary]
|
||||
);
|
||||
|
||||
const containerBackground = isLight
|
||||
? 'color-mix(in srgb, var(--color-surface-overlay) 30%, white 70%)'
|
||||
: 'var(--color-surface-overlay)';
|
||||
|
||||
const contentBackground = isLight
|
||||
? 'color-mix(in srgb, var(--color-surface-overlay) 52%, white 48%)'
|
||||
: 'color-mix(in srgb, var(--color-surface-raised) 88%, black 12%)';
|
||||
|
||||
const headerTitleColor = isLight
|
||||
? 'var(--color-text)'
|
||||
: 'color-mix(in srgb, var(--color-text) 82%, white 18%)';
|
||||
|
||||
const headerMutedColor = isLight
|
||||
? 'color-mix(in srgb, var(--color-text-muted) 58%, var(--color-text) 42%)'
|
||||
: 'color-mix(in srgb, var(--color-text-muted) 52%, white 48%)';
|
||||
|
||||
const headerIconColor = isLight
|
||||
? 'color-mix(in srgb, var(--color-text-muted) 64%, var(--color-text) 36%)'
|
||||
: 'color-mix(in srgb, var(--color-text-muted) 54%, white 46%)';
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
|
@ -38,7 +60,7 @@ export const OptionalSettingsSection = ({
|
|||
className
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: 'color-mix(in srgb, var(--color-surface-overlay) 94%, white 6%)',
|
||||
backgroundColor: containerBackground,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
|
|
@ -48,19 +70,29 @@ export const OptionalSettingsSection = ({
|
|||
aria-expanded={isOpen}
|
||||
>
|
||||
<div className="flex min-w-0 items-start gap-2.5">
|
||||
<div className="mt-0.5 rounded-md border border-[var(--color-border-emphasis)] bg-[var(--color-surface-raised)] p-1.5 text-[var(--color-text-muted)]">
|
||||
<div
|
||||
className="mt-0.5 rounded-md border border-[var(--color-border-emphasis)] bg-[var(--color-surface-raised)] p-1.5"
|
||||
style={{ color: headerIconColor }}
|
||||
>
|
||||
<Settings2 className="size-3.5" />
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="text-sm font-medium text-[var(--color-text)]">{title}</span>
|
||||
<span className="rounded-full border border-[var(--color-border-emphasis)] bg-[var(--color-surface-raised)] px-2 py-0.5 text-[10px] uppercase tracking-wide text-[var(--color-text-muted)]">
|
||||
<span className="text-sm font-medium" style={{ color: headerTitleColor }}>
|
||||
{title}
|
||||
</span>
|
||||
<span
|
||||
className="rounded-full border border-[var(--color-border-emphasis)] bg-[var(--color-surface-raised)] px-2 py-0.5 text-[10px] uppercase tracking-wide"
|
||||
style={{ color: headerMutedColor }}
|
||||
>
|
||||
Optional
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-[var(--color-text-muted)]">{description}</p>
|
||||
<p className="mt-1 text-xs" style={{ color: headerMutedColor }}>
|
||||
{description}
|
||||
</p>
|
||||
{!isOpen ? (
|
||||
<p className="mt-1.5 line-clamp-2 text-[11px] text-[var(--color-text-secondary)]">
|
||||
<p className="mt-1.5 line-clamp-2 text-[11px]" style={{ color: headerMutedColor }}>
|
||||
{visibleSummary.length > 0
|
||||
? visibleSummary.join(' • ')
|
||||
: 'Collapsed by default to keep the primary flow focused.'}
|
||||
|
|
@ -70,9 +102,10 @@ export const OptionalSettingsSection = ({
|
|||
</div>
|
||||
<ChevronRight
|
||||
className={cn(
|
||||
'mt-0.5 size-4 shrink-0 text-[var(--color-text-muted)] transition-transform duration-150',
|
||||
'mt-0.5 size-4 shrink-0 transition-transform duration-150',
|
||||
isOpen && 'rotate-90'
|
||||
)}
|
||||
style={{ color: headerIconColor }}
|
||||
/>
|
||||
</button>
|
||||
|
||||
|
|
@ -80,7 +113,7 @@ export const OptionalSettingsSection = ({
|
|||
<div
|
||||
className="border-t border-[var(--color-border-emphasis)] px-3 pb-3 pt-2.5"
|
||||
style={{
|
||||
backgroundColor: 'color-mix(in srgb, var(--color-surface-overlay) 86%, white 14%)',
|
||||
backgroundColor: contentBackground,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea
|
|||
import { getTeamColorSet } from '@renderer/constants/teamColors';
|
||||
import { useDraftPersistence } from '@renderer/hooks/useDraftPersistence';
|
||||
import { useFileListCacheWarmer } from '@renderer/hooks/useFileListCacheWarmer';
|
||||
import { useTheme } from '@renderer/hooks/useTheme';
|
||||
import { reconcileChips, removeChipTokenFromText } from '@renderer/utils/chipUtils';
|
||||
import { getMemberColorByName } from '@shared/constants/memberColors';
|
||||
import { ChevronDown, ChevronRight, Info } from 'lucide-react';
|
||||
|
|
@ -47,6 +48,7 @@ export const MemberDraftRow = ({
|
|||
projectPath,
|
||||
mentionSuggestions = [],
|
||||
}: MemberDraftRowProps): React.JSX.Element => {
|
||||
const { isLight } = useTheme();
|
||||
const memberColorSet = getTeamColorSet(
|
||||
getMemberColorByName(member.name.trim() || `member-${index}`)
|
||||
);
|
||||
|
|
@ -117,12 +119,19 @@ export const MemberDraftRow = ({
|
|||
|
||||
return (
|
||||
<div
|
||||
className="grid grid-cols-1 gap-2 rounded-md border border-[var(--color-border)] bg-[var(--color-surface-raised)] p-2 md:grid-cols-[1fr_220px_auto]"
|
||||
className="relative grid grid-cols-1 gap-2 overflow-hidden rounded-md p-2 shadow-sm md:grid-cols-[1fr_220px_auto]"
|
||||
style={{
|
||||
borderLeftWidth: '3px',
|
||||
borderLeftColor: memberColorSet.border,
|
||||
backgroundColor: isLight
|
||||
? 'color-mix(in srgb, var(--color-surface-raised) 22%, white 78%)'
|
||||
: 'var(--color-surface-raised)',
|
||||
boxShadow: isLight ? '0 1px 2px rgba(15, 23, 42, 0.06)' : '0 1px 2px rgba(0, 0, 0, 0.28)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 w-1"
|
||||
style={{ backgroundColor: memberColorSet.border }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="space-y-0.5">
|
||||
<Input
|
||||
className="h-8 text-xs"
|
||||
|
|
|
|||
|
|
@ -224,7 +224,7 @@ export const ChipInteractionLayer = ({
|
|||
>
|
||||
<button
|
||||
type="button"
|
||||
className="absolute -right-1 -top-1.5 z-30 flex size-3.5 items-center justify-center rounded-full border border-[var(--color-border-emphasis)] bg-[var(--color-surface-raised)] opacity-0 transition-opacity group-hover:opacity-100"
|
||||
className="pointer-events-none absolute -right-1 -top-1.5 z-30 flex size-3.5 items-center justify-center rounded-full border border-[var(--color-border-emphasis)] bg-[var(--color-surface-raised)] opacity-0 transition-opacity group-hover:pointer-events-auto group-hover:opacity-100"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { MemberBadge } from '@renderer/components/team/MemberBadge';
|
||||
import { FileIcon } from '@renderer/components/team/editor/FileIcon';
|
||||
import { getTeamColorSet } from '@renderer/constants/teamColors';
|
||||
import { getTeamColorSet, getThemedText } from '@renderer/constants/teamColors';
|
||||
import { useTheme } from '@renderer/hooks/useTheme';
|
||||
import { nameColorSet } from '@renderer/utils/projectColor';
|
||||
import { Folder, Hash, Loader2, UsersRound } from 'lucide-react';
|
||||
|
||||
|
|
@ -56,6 +58,7 @@ export const MentionSuggestionList = ({
|
|||
filesLoading,
|
||||
}: MentionSuggestionListProps): React.JSX.Element => {
|
||||
const listRef = useRef<HTMLUListElement>(null);
|
||||
const { isLight } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const list = listRef.current;
|
||||
|
|
@ -111,6 +114,12 @@ export const MentionSuggestionList = ({
|
|||
const isFileOrFolder = isFile || isFolder;
|
||||
const isTeam = section === 'team';
|
||||
const isTask = section === 'task';
|
||||
const taskTeamColorSet =
|
||||
isTask && s.color
|
||||
? getTeamColorSet(s.color)
|
||||
: isTask && s.teamDisplayName
|
||||
? nameColorSet(s.teamDisplayName, isLight)
|
||||
: null;
|
||||
|
||||
// Insert section header on transition
|
||||
if (showSections && section !== currentSection) {
|
||||
|
|
@ -177,8 +186,16 @@ export const MentionSuggestionList = ({
|
|||
>
|
||||
<HighlightedName name={isTask ? `#${s.name}` : s.name} query={query} />
|
||||
</span>
|
||||
{isTask && !s.isCurrentTeamTask && s.teamDisplayName ? (
|
||||
<span className="truncate text-[10px] text-[var(--color-text-muted)]">
|
||||
{isTask && s.ownerName ? (
|
||||
<MemberBadge name={s.ownerName} color={s.ownerColor} size="xs" disableHoverCard />
|
||||
) : null}
|
||||
{isTask && s.teamDisplayName ? (
|
||||
<span
|
||||
className="truncate text-[10px]"
|
||||
style={
|
||||
taskTeamColorSet ? { color: getThemedText(taskTeamColorSet, isLight) } : undefined
|
||||
}
|
||||
>
|
||||
{s.teamDisplayName}
|
||||
</span>
|
||||
) : null}
|
||||
|
|
|
|||
|
|
@ -868,6 +868,18 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
|
|||
/>
|
||||
) : null}
|
||||
|
||||
{/* Gradient fade overlay before corner action buttons */}
|
||||
{cornerAction || cornerActionLeft ? (
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-0 bottom-0 z-[15] rounded-b-md"
|
||||
style={{
|
||||
height: 48,
|
||||
background:
|
||||
'linear-gradient(to bottom, transparent 0%, var(--color-surface-raised) 75%)',
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{cornerAction ? (
|
||||
<div className="pointer-events-none absolute bottom-2 right-2 z-20 flex items-end justify-end">
|
||||
<div className="pointer-events-auto">{cornerAction}</div>
|
||||
|
|
|
|||
|
|
@ -71,7 +71,19 @@ export const TaskReferenceInteractionLayer = ({
|
|||
width: position.width,
|
||||
height: position.height,
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
if (e.metaKey || e.ctrlKey) return;
|
||||
e.preventDefault();
|
||||
const textarea = textareaRef.current;
|
||||
if (!textarea) return;
|
||||
|
||||
textarea.focus();
|
||||
const clickOffsetX = e.clientX - e.currentTarget.getBoundingClientRect().left;
|
||||
const snapTo = clickOffsetX < position.width / 2 ? position.start : position.end;
|
||||
textarea.setSelectionRange(snapTo, snapTo);
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (!e.metaKey && !e.ctrlKey) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (teamName) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,43 @@ import type { MentionSuggestion } from '@renderer/types/mention';
|
|||
|
||||
const TASK_REF_REGEX = /#([A-Za-z0-9-]+)\b/g;
|
||||
|
||||
function isAllowedTaskRefBoundary(char: string | undefined): boolean {
|
||||
if (!char) return true;
|
||||
return !/[A-Za-z0-9_]/.test(char);
|
||||
}
|
||||
|
||||
function buildSuggestionsByRef(
|
||||
taskSuggestions: MentionSuggestion[]
|
||||
): Map<string, MentionSuggestion[]> {
|
||||
const suggestionsByRef = new Map<string, MentionSuggestion[]>();
|
||||
|
||||
for (const suggestion of taskSuggestions) {
|
||||
if (suggestion.type !== 'task') continue;
|
||||
const ref = getSuggestionInsertionText(suggestion).trim().toLowerCase();
|
||||
if (!ref) continue;
|
||||
|
||||
const existing = suggestionsByRef.get(ref);
|
||||
if (existing) {
|
||||
existing.push(suggestion);
|
||||
} else {
|
||||
suggestionsByRef.set(ref, [suggestion]);
|
||||
}
|
||||
}
|
||||
|
||||
return suggestionsByRef;
|
||||
}
|
||||
|
||||
function resolveTaskSuggestion(candidates: MentionSuggestion[]): MentionSuggestion | null {
|
||||
if (candidates.length === 0) return null;
|
||||
|
||||
const currentTeamCandidate = candidates.find((candidate) => candidate.isCurrentTeamTask);
|
||||
if (currentTeamCandidate) return currentTeamCandidate;
|
||||
|
||||
if (candidates.length === 1) return candidates[0];
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export interface TaskReferenceMatch {
|
||||
start: number;
|
||||
end: number;
|
||||
|
|
@ -13,7 +50,10 @@ export interface TaskReferenceMatch {
|
|||
}
|
||||
|
||||
export function linkifyTaskIdsInMarkdown(text: string): string {
|
||||
return text.replace(TASK_REF_REGEX, '[#$1](task://$1)');
|
||||
return text.replace(TASK_REF_REGEX, (raw, ref: string, offset: number) => {
|
||||
const preceding = offset > 0 ? text[offset - 1] : undefined;
|
||||
return isAllowedTaskRefBoundary(preceding) ? `[${raw}](task://${ref})` : raw;
|
||||
});
|
||||
}
|
||||
|
||||
export function findTaskReferenceMatches(
|
||||
|
|
@ -22,15 +62,9 @@ export function findTaskReferenceMatches(
|
|||
): TaskReferenceMatch[] {
|
||||
if (!text || taskSuggestions.length === 0) return [];
|
||||
|
||||
const suggestionByRef = new Map<string, MentionSuggestion>();
|
||||
for (const suggestion of taskSuggestions) {
|
||||
if (suggestion.type !== 'task') continue;
|
||||
const ref = getSuggestionInsertionText(suggestion).trim().toLowerCase();
|
||||
if (!ref || suggestionByRef.has(ref)) continue;
|
||||
suggestionByRef.set(ref, suggestion);
|
||||
}
|
||||
const suggestionsByRef = buildSuggestionsByRef(taskSuggestions);
|
||||
|
||||
if (suggestionByRef.size === 0) return [];
|
||||
if (suggestionsByRef.size === 0) return [];
|
||||
|
||||
const matches: TaskReferenceMatch[] = [];
|
||||
for (const match of text.matchAll(TASK_REF_REGEX)) {
|
||||
|
|
@ -39,14 +73,10 @@ export function findTaskReferenceMatches(
|
|||
const start = match.index ?? -1;
|
||||
if (start < 0) continue;
|
||||
|
||||
if (start > 0) {
|
||||
const preceding = text[start - 1];
|
||||
if (preceding !== ' ' && preceding !== '\t' && preceding !== '\n' && preceding !== '\r') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
const preceding = start > 0 ? text[start - 1] : undefined;
|
||||
if (!isAllowedTaskRefBoundary(preceding)) continue;
|
||||
|
||||
const suggestion = suggestionByRef.get(ref.toLowerCase());
|
||||
const suggestion = resolveTaskSuggestion(suggestionsByRef.get(ref.toLowerCase()) ?? []);
|
||||
if (!suggestion) continue;
|
||||
|
||||
matches.push({
|
||||
|
|
|
|||
Loading…
Reference in a new issue