mirror of https://github.com/jetkvm/kvm.git
remove sortable list and simplify
This commit is contained in:
parent
7b8725892d
commit
4438dbbe0c
|
@ -1,311 +0,0 @@
|
|||
import { useState, useCallback, ReactNode } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { LuGripVertical } from 'react-icons/lu';
|
||||
import { cva } from "@/cva.config";
|
||||
|
||||
interface SortableListProps<T> {
|
||||
items: T[];
|
||||
keyFn: (item: T) => string;
|
||||
onSort: (newItems: T[]) => Promise<void>;
|
||||
children: (item: T, index: number) => ReactNode;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
itemClassName?: string;
|
||||
variant?: 'list' | 'grid';
|
||||
size?: keyof typeof sizes;
|
||||
renderHandle?: (isDragging: boolean) => ReactNode;
|
||||
hideHandle?: boolean;
|
||||
handlePosition?: 'left' | 'right';
|
||||
}
|
||||
|
||||
const sizes = {
|
||||
XS: "min-h-[24.5px] py-1 px-3 text-xs",
|
||||
SM: "min-h-[32px] py-1.5 px-3 text-[13px]",
|
||||
MD: "min-h-[40px] py-2 px-4 text-sm",
|
||||
LG: "min-h-[48px] py-2.5 px-4 text-base",
|
||||
};
|
||||
|
||||
const containerVariants = {
|
||||
list: {
|
||||
XS: 'space-y-1',
|
||||
SM: 'space-y-2',
|
||||
MD: 'space-y-3',
|
||||
LG: 'space-y-4'
|
||||
},
|
||||
grid: 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4',
|
||||
};
|
||||
|
||||
const sortableItemVariants = cva({
|
||||
base: 'transition-all duration-300 ease-out rounded',
|
||||
variants: {
|
||||
size: sizes,
|
||||
isDragging: {
|
||||
true: 'shadow-lg bg-blue-100/80 dark:bg-blue-900/40 border-blue-200 dark:border-blue-800 z-50',
|
||||
false: ''
|
||||
},
|
||||
isDropTarget: {
|
||||
true: 'border border-dashed border-blue-200 dark:border-blue-800 bg-blue-50/50 dark:bg-blue-900/10',
|
||||
false: ''
|
||||
},
|
||||
handlePosition: {
|
||||
left: 'flex-row',
|
||||
right: 'flex-row-reverse'
|
||||
},
|
||||
disabled: {
|
||||
true: 'pointer-events-none select-none bg-slate-50 text-slate-500/80 dark:bg-slate-800 dark:text-slate-400/80',
|
||||
false: 'hover:bg-blue-50/80 active:bg-blue-100/60 dark:hover:bg-slate-700 dark:active:bg-slate-800/60'
|
||||
}
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'MD',
|
||||
isDragging: false,
|
||||
isDropTarget: false,
|
||||
handlePosition: 'left',
|
||||
disabled: false
|
||||
}
|
||||
});
|
||||
|
||||
const DefaultHandle = ({ isDragging, disabled }: { isDragging: boolean; disabled?: boolean }) => (
|
||||
<div className={clsx(
|
||||
disabled ? 'cursor-not-allowed' : 'cursor-grab active:cursor-grabbing touch-none',
|
||||
'p-1',
|
||||
'text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-300',
|
||||
isDragging && 'text-slate-700 dark:text-slate-300',
|
||||
disabled && 'opacity-50'
|
||||
)}>
|
||||
<LuGripVertical className="h-4 w-4" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export function SortableList<T>({
|
||||
items,
|
||||
keyFn,
|
||||
onSort,
|
||||
children,
|
||||
disabled = false,
|
||||
className = '',
|
||||
itemClassName = '',
|
||||
variant = 'list',
|
||||
size = 'MD',
|
||||
renderHandle,
|
||||
hideHandle = false,
|
||||
handlePosition = 'left',
|
||||
}: SortableListProps<T>) {
|
||||
const [dragItem, setDragItem] = useState<number | null>(null);
|
||||
const [dragOverItem, setDragOverItem] = useState<number | null>(null);
|
||||
const [touchStartY, setTouchStartY] = useState<number | null>(null);
|
||||
|
||||
const containerClasses = clsx(
|
||||
'sortable-list',
|
||||
variant === 'grid' ? containerVariants.grid : containerVariants.list[size],
|
||||
className
|
||||
);
|
||||
|
||||
const getItemClasses = (index: number) => clsx(
|
||||
'relative flex items-center',
|
||||
sortableItemVariants({
|
||||
size,
|
||||
isDragging: dragItem === index,
|
||||
isDropTarget: dragOverItem === index && dragItem !== index,
|
||||
handlePosition,
|
||||
disabled
|
||||
}),
|
||||
itemClassName
|
||||
);
|
||||
|
||||
const handleDragStart = useCallback((index: number) => {
|
||||
if (disabled) return;
|
||||
setDragItem(index);
|
||||
|
||||
const allItems = document.querySelectorAll('[data-sortable-item]');
|
||||
const draggedElement = allItems[index];
|
||||
if (draggedElement) {
|
||||
draggedElement.classList.add('dragging');
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
const handleDragOver = useCallback((e: React.DragEvent, index: number) => {
|
||||
if (disabled) return;
|
||||
e.preventDefault();
|
||||
setDragOverItem(index);
|
||||
|
||||
const allItems = document.querySelectorAll('[data-sortable-item]');
|
||||
allItems.forEach(el => el.classList.remove('drop-target'));
|
||||
|
||||
const targetElement = allItems[index];
|
||||
if (targetElement) {
|
||||
targetElement.classList.add('drop-target');
|
||||
}
|
||||
}, [disabled]);
|
||||
|
||||
const handleDrop = useCallback(async (e: React.DragEvent) => {
|
||||
if (disabled) return;
|
||||
e.preventDefault();
|
||||
if (dragItem === null || dragOverItem === null) return;
|
||||
|
||||
const itemsCopy = [...items];
|
||||
const draggedItem = itemsCopy.splice(dragItem, 1)[0];
|
||||
itemsCopy.splice(dragOverItem, 0, draggedItem);
|
||||
|
||||
await onSort(itemsCopy);
|
||||
|
||||
const allItems = document.querySelectorAll('[data-sortable-item]');
|
||||
allItems.forEach(el => {
|
||||
el.classList.remove('drop-target');
|
||||
el.classList.remove('dragging');
|
||||
});
|
||||
|
||||
setDragItem(null);
|
||||
setDragOverItem(null);
|
||||
}, [disabled, dragItem, dragOverItem, items, onSort]);
|
||||
|
||||
const handleTouchStart = useCallback((e: React.TouchEvent, index: number) => {
|
||||
if (disabled) return;
|
||||
const touch = e.touches[0];
|
||||
setTouchStartY(touch.clientY);
|
||||
setDragItem(index);
|
||||
|
||||
const element = e.currentTarget as HTMLElement;
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
// Create ghost element
|
||||
const ghost = element.cloneNode(true) as HTMLElement;
|
||||
ghost.id = 'ghost-item';
|
||||
ghost.className = 'sortable-ghost';
|
||||
ghost.style.height = `${rect.height}px`;
|
||||
element.parentNode?.insertBefore(ghost, element);
|
||||
|
||||
// Set up dragged element
|
||||
element.style.position = 'fixed';
|
||||
element.style.left = `${rect.left}px`;
|
||||
element.style.top = `${rect.top}px`;
|
||||
element.style.width = `${rect.width}px`;
|
||||
element.style.zIndex = '50';
|
||||
}, [disabled]);
|
||||
|
||||
const handleTouchMove = useCallback((e: React.TouchEvent) => {
|
||||
if (disabled || touchStartY === null || dragItem === null) return;
|
||||
|
||||
const touch = e.touches[0];
|
||||
const deltaY = touch.clientY - touchStartY;
|
||||
const element = e.currentTarget as HTMLElement;
|
||||
|
||||
element.style.transform = `translateY(${deltaY}px)`;
|
||||
|
||||
const sortableElements = document.querySelectorAll('[data-sortable-item]');
|
||||
const draggedRect = element.getBoundingClientRect();
|
||||
const draggedMiddle = draggedRect.top + draggedRect.height / 2;
|
||||
|
||||
sortableElements.forEach((el, i) => {
|
||||
if (i === dragItem) return;
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
const elementMiddle = rect.top + rect.height / 2;
|
||||
const distance = Math.abs(draggedMiddle - elementMiddle);
|
||||
|
||||
if (distance < rect.height) {
|
||||
const direction = draggedMiddle > elementMiddle ? -1 : 1;
|
||||
(el as HTMLElement).style.transform = `translateY(${direction * rect.height}px)`;
|
||||
(el as HTMLElement).style.transition = 'transform 0.15s ease-out';
|
||||
} else {
|
||||
(el as HTMLElement).style.transform = '';
|
||||
(el as HTMLElement).style.transition = 'transform 0.15s ease-out';
|
||||
}
|
||||
});
|
||||
}, [disabled, touchStartY, dragItem]);
|
||||
|
||||
const handleTouchEnd = useCallback(async (e: React.TouchEvent) => {
|
||||
if (disabled || dragItem === null) return;
|
||||
|
||||
const element = e.currentTarget as HTMLElement;
|
||||
const touch = e.changedTouches[0];
|
||||
|
||||
// Remove ghost element
|
||||
const ghost = document.getElementById('ghost-item');
|
||||
ghost?.parentNode?.removeChild(ghost);
|
||||
|
||||
// Reset dragged element styles
|
||||
element.style.position = '';
|
||||
element.style.left = '';
|
||||
element.style.top = '';
|
||||
element.style.width = '';
|
||||
element.style.zIndex = '';
|
||||
element.style.transform = '';
|
||||
element.style.transition = '';
|
||||
|
||||
const sortableElements = document.querySelectorAll('[data-sortable-item]');
|
||||
let targetIndex = dragItem;
|
||||
|
||||
// Find the closest element to the final touch position
|
||||
const finalY = touch.clientY;
|
||||
let closestDistance = Infinity;
|
||||
|
||||
sortableElements.forEach((el, i) => {
|
||||
if (i === dragItem) return;
|
||||
|
||||
const rect = el.getBoundingClientRect();
|
||||
const distance = Math.abs(finalY - (rect.top + rect.height / 2));
|
||||
|
||||
if (distance < closestDistance) {
|
||||
closestDistance = distance;
|
||||
targetIndex = i;
|
||||
}
|
||||
|
||||
// Reset other elements
|
||||
(el as HTMLElement).style.transform = '';
|
||||
(el as HTMLElement).style.transition = '';
|
||||
});
|
||||
|
||||
if (targetIndex !== dragItem && closestDistance < 50) {
|
||||
const itemsCopy = [...items];
|
||||
const [draggedItem] = itemsCopy.splice(dragItem, 1);
|
||||
itemsCopy.splice(targetIndex, 0, draggedItem);
|
||||
await onSort(itemsCopy);
|
||||
}
|
||||
|
||||
setTouchStartY(null);
|
||||
setDragItem(null);
|
||||
}, [disabled, dragItem, items, onSort]);
|
||||
|
||||
return (
|
||||
<div className={containerClasses}>
|
||||
{items.map((item, index) => (
|
||||
<div
|
||||
key={keyFn(item)}
|
||||
data-sortable-item={index}
|
||||
draggable={!disabled}
|
||||
onDragStart={() => handleDragStart(index)}
|
||||
onDragOver={e => handleDragOver(e, index)}
|
||||
onDragEnd={() => {
|
||||
const allItems = document.querySelectorAll('[data-sortable-item]');
|
||||
allItems.forEach(el => {
|
||||
el.classList.remove('drop-target');
|
||||
el.classList.remove('dragging');
|
||||
});
|
||||
}}
|
||||
onDrop={handleDrop}
|
||||
onTouchStart={e => handleTouchStart(e, index)}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleTouchEnd}
|
||||
className={getItemClasses(index)}
|
||||
>
|
||||
<div className="flex-1 min-w-0 flex items-center gap-2">
|
||||
{handlePosition === 'left' && !hideHandle && (
|
||||
renderHandle ?
|
||||
renderHandle(dragItem === index) :
|
||||
<DefaultHandle isDragging={dragItem === index} disabled={disabled} />
|
||||
)}
|
||||
<div className="flex-1 min-w-0">
|
||||
{children(item, index)}
|
||||
</div>
|
||||
{handlePosition === 'right' && !hideHandle && (
|
||||
renderHandle ?
|
||||
renderHandle(dragItem === index) :
|
||||
<DefaultHandle isDragging={dragItem === index} disabled={disabled} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
import { useEffect, Fragment, useMemo } from "react";
|
||||
import { useEffect, Fragment, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { LuPenLine, LuLoader, LuCopy, LuMoveRight, LuCornerDownRight } from "react-icons/lu";
|
||||
import { LuPenLine, LuLoader, LuCopy, LuMoveRight, LuCornerDownRight, LuArrowUp, LuArrowDown } from "react-icons/lu";
|
||||
|
||||
import { KeySequence, useMacrosStore, generateMacroId } from "@/hooks/stores";
|
||||
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
||||
import { Button } from "@/components/Button";
|
||||
import EmptyCard from "@/components/EmptyCard";
|
||||
import { SortableList } from "@/components/SortableList";
|
||||
import Card from "@/components/Card";
|
||||
import { MAX_TOTAL_MACROS, COPY_SUFFIX } from "@/constants/macros";
|
||||
import { keyDisplayMap, modifierDisplayMap } from "@/keyboardMappings";
|
||||
import notifications from "@/notifications";
|
||||
|
@ -22,6 +22,7 @@ const normalizeSortOrders = (macros: KeySequence[]): KeySequence[] => {
|
|||
export default function SettingsMacrosRoute() {
|
||||
const { macros, loading, initialized, loadMacros, saveMacros } = useMacrosStore();
|
||||
const navigate = useNavigate();
|
||||
const [actionLoadingId, setActionLoadingId] = useState<string | null>(null);
|
||||
|
||||
const isMaxMacrosReached = useMemo(() =>
|
||||
macros.length >= MAX_TOTAL_MACROS,
|
||||
|
@ -35,11 +36,18 @@ export default function SettingsMacrosRoute() {
|
|||
}, [initialized, loadMacros]);
|
||||
|
||||
const handleDuplicateMacro = async (macro: KeySequence) => {
|
||||
if (!macro?.id || !macro?.name) {
|
||||
notifications.error("Invalid macro data");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isMaxMacrosReached) {
|
||||
notifications.error(`Maximum of ${MAX_TOTAL_MACROS} macros allowed`);
|
||||
return;
|
||||
}
|
||||
|
||||
setActionLoadingId(macro.id);
|
||||
|
||||
const newMacroCopy: KeySequence = {
|
||||
...JSON.parse(JSON.stringify(macro)),
|
||||
id: generateMacroId(),
|
||||
|
@ -56,9 +64,141 @@ export default function SettingsMacrosRoute() {
|
|||
} else {
|
||||
notifications.error("Failed to duplicate macro");
|
||||
}
|
||||
} finally {
|
||||
setActionLoadingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMoveMacro = async (index: number, direction: 'up' | 'down', macroId: string) => {
|
||||
if (!Array.isArray(macros) || macros.length === 0) {
|
||||
notifications.error("No macros available");
|
||||
return;
|
||||
}
|
||||
|
||||
const newIndex = direction === 'up' ? index - 1 : index + 1;
|
||||
if (newIndex < 0 || newIndex >= macros.length) return;
|
||||
|
||||
setActionLoadingId(macroId);
|
||||
|
||||
try {
|
||||
const newMacros = [...macros];
|
||||
[newMacros[index], newMacros[newIndex]] = [newMacros[newIndex], newMacros[index]];
|
||||
const updatedMacros = normalizeSortOrders(newMacros);
|
||||
|
||||
await saveMacros(updatedMacros);
|
||||
notifications.success("Macro order updated successfully");
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
notifications.error(`Failed to reorder macros: ${error.message}`);
|
||||
} else {
|
||||
notifications.error("Failed to reorder macros");
|
||||
}
|
||||
} finally {
|
||||
setActionLoadingId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const MacroList = useMemo(() => (
|
||||
<div className="space-y-2">
|
||||
{macros.map((macro, index) => (
|
||||
<Card key={macro.id} className="p-2 bg-white dark:bg-slate-800">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col gap-1 px-2">
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
onClick={() => handleMoveMacro(index, 'up', macro.id)}
|
||||
disabled={index === 0 || actionLoadingId === macro.id}
|
||||
LeadingIcon={LuArrowUp}
|
||||
aria-label={`Move ${macro.name} up`}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
onClick={() => handleMoveMacro(index, 'down', macro.id)}
|
||||
disabled={index === macros.length - 1 || actionLoadingId === macro.id}
|
||||
LeadingIcon={LuArrowDown}
|
||||
aria-label={`Move ${macro.name} down`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 flex flex-col justify-center ml-2">
|
||||
<h3 className="truncate text-sm font-semibold text-black dark:text-white">
|
||||
{macro.name}
|
||||
</h3>
|
||||
<p className="mt-1 ml-2 text-xs text-slate-500 dark:text-slate-400 overflow-hidden">
|
||||
<span className="flex flex-col items-start gap-1">
|
||||
{macro.steps.map((step, stepIndex) => {
|
||||
const StepIcon = stepIndex === 0 ? LuMoveRight : LuCornerDownRight;
|
||||
|
||||
return (
|
||||
<span key={stepIndex} className="inline-flex items-center">
|
||||
<StepIcon className="mr-1 text-slate-400 dark:text-slate-500 h-3 w-3 flex-shrink-0" />
|
||||
<span className="bg-slate-50 dark:bg-slate-800 px-2 py-0.5 rounded-md border border-slate-200/50 dark:border-slate-700/50">
|
||||
{(Array.isArray(step.modifiers) && step.modifiers.length > 0) || (Array.isArray(step.keys) && step.keys.length > 0) ? (
|
||||
<>
|
||||
{Array.isArray(step.modifiers) && step.modifiers.map((modifier, idx) => (
|
||||
<Fragment key={`mod-${idx}`}>
|
||||
<span className="font-medium text-slate-600 dark:text-slate-200">
|
||||
{modifierDisplayMap[modifier] || modifier}
|
||||
</span>
|
||||
{idx < step.modifiers.length - 1 && (
|
||||
<span className="text-slate-400 dark:text-slate-600"> + </span>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
{Array.isArray(step.modifiers) && step.modifiers.length > 0 && Array.isArray(step.keys) && step.keys.length > 0 && (
|
||||
<span className="text-slate-400 dark:text-slate-600"> + </span>
|
||||
)}
|
||||
|
||||
{Array.isArray(step.keys) && step.keys.map((key, idx) => (
|
||||
<Fragment key={`key-${idx}`}>
|
||||
<span className="font-medium text-blue-600 dark:text-blue-200">
|
||||
{keyDisplayMap[key] || key}
|
||||
</span>
|
||||
{idx < step.keys.length - 1 && (
|
||||
<span className="text-slate-400 dark:text-slate-600"> + </span>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<span className="font-medium text-slate-500 dark:text-slate-400">Delay only</span>
|
||||
)}
|
||||
<span className="ml-1 text-slate-400 dark:text-slate-500">({step.delay}ms)</span>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 ml-4">
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
LeadingIcon={LuCopy}
|
||||
onClick={() => handleDuplicateMacro(macro)}
|
||||
disabled={actionLoadingId === macro.id}
|
||||
aria-label={`Duplicate macro ${macro.name}`}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
LeadingIcon={LuPenLine}
|
||||
onClick={() => navigate(`${macro.id}/edit`)}
|
||||
disabled={actionLoadingId === macro.id}
|
||||
aria-label={`Edit macro ${macro.name}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
), [macros, actionLoadingId]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
|
@ -77,13 +217,14 @@ export default function SettingsMacrosRoute() {
|
|||
text={isMaxMacrosReached ? `Max Reached` : "Add New Macro"}
|
||||
onClick={() => navigate("add")}
|
||||
disabled={isMaxMacrosReached}
|
||||
aria-label="Add new macro"
|
||||
/>
|
||||
</div>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{loading ? (
|
||||
{loading && macros.length === 0 ? (
|
||||
<EmptyCard
|
||||
headline="Loading macros..."
|
||||
description="Please wait while we fetch your macros"
|
||||
|
@ -102,104 +243,11 @@ export default function SettingsMacrosRoute() {
|
|||
text="Add New Macro"
|
||||
onClick={() => navigate("add")}
|
||||
disabled={isMaxMacrosReached}
|
||||
aria-label="Add new macro"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<SortableList<KeySequence>
|
||||
keyFn={(macro) => macro.id}
|
||||
items={macros}
|
||||
itemClassName="rounded-md border border-slate-200 p-2 dark:border-slate-700 bg-white dark:bg-slate-800"
|
||||
onSort={async (newMacros) => {
|
||||
const updatedMacros = normalizeSortOrders(newMacros);
|
||||
try {
|
||||
await saveMacros(updatedMacros);
|
||||
notifications.success("Macro order updated successfully");
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
notifications.error(`Failed to reorder macros: ${error.message}`);
|
||||
} else {
|
||||
notifications.error("Failed to reorder macros");
|
||||
}
|
||||
}
|
||||
}}
|
||||
variant="list"
|
||||
size="XS"
|
||||
handlePosition="left"
|
||||
>
|
||||
{(macro) => (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1 min-w-0 flex flex-col justify-center">
|
||||
<h3 className="truncate text-sm font-semibold text-black dark:text-white">
|
||||
{macro.name}
|
||||
</h3>
|
||||
<p className="mt-1 ml-2 text-xs text-slate-500 dark:text-slate-400 overflow-hidden">
|
||||
<span className="flex flex-col items-start gap-1">
|
||||
{macro.steps.map((step, stepIndex) => {
|
||||
const StepIcon = stepIndex === 0 ? LuMoveRight : LuCornerDownRight;
|
||||
|
||||
return (
|
||||
<span key={stepIndex} className="inline-flex items-center">
|
||||
<StepIcon className="mr-1 text-slate-400 dark:text-slate-500 h-3 w-3 flex-shrink-0" />
|
||||
<span className="bg-slate-50 dark:bg-slate-800 px-2 py-0.5 rounded-md border border-slate-200/50 dark:border-slate-700/50">
|
||||
{(Array.isArray(step.modifiers) && step.modifiers.length > 0) || (Array.isArray(step.keys) && step.keys.length > 0) ? (
|
||||
<>
|
||||
{Array.isArray(step.modifiers) && step.modifiers.map((modifier, idx) => (
|
||||
<Fragment key={`mod-${idx}`}>
|
||||
<span className="font-medium text-slate-600 dark:text-slate-200">
|
||||
{modifierDisplayMap[modifier] || modifier}
|
||||
</span>
|
||||
{idx < step.modifiers.length - 1 && (
|
||||
<span className="text-slate-400 dark:text-slate-600"> + </span>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
{Array.isArray(step.modifiers) && step.modifiers.length > 0 && Array.isArray(step.keys) && step.keys.length > 0 && (
|
||||
<span className="text-slate-400 dark:text-slate-600"> + </span>
|
||||
)}
|
||||
|
||||
{Array.isArray(step.keys) && step.keys.map((key, idx) => (
|
||||
<Fragment key={`key-${idx}`}>
|
||||
<span className="font-medium text-blue-600 dark:text-blue-200">
|
||||
{keyDisplayMap[key] || key}
|
||||
</span>
|
||||
{idx < step.keys.length - 1 && (
|
||||
<span className="text-slate-400 dark:text-slate-600"> + </span>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<span className="font-medium text-slate-500 dark:text-slate-400">Delay only</span>
|
||||
)}
|
||||
<span className="ml-1 text-slate-400 dark:text-slate-500">({step.delay}ms)</span>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1 ml-4">
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
LeadingIcon={LuCopy}
|
||||
onClick={() => handleDuplicateMacro(macro)}
|
||||
/>
|
||||
<Button
|
||||
size="XS"
|
||||
theme="light"
|
||||
LeadingIcon={LuPenLine}
|
||||
onClick={() => navigate(`${macro.id}/edit`)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</SortableList>
|
||||
)}
|
||||
) : MacroList}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
Loading…
Reference in New Issue