use existing components and CTA

This commit is contained in:
Andrew Davis 2025-04-02 23:29:41 +10:00
parent c65d222ee0
commit 669e4244a6
No known key found for this signature in database
GPG Key ID: 30AB5B89A109D044
1 changed files with 450 additions and 484 deletions

View File

@ -1,14 +1,19 @@
import { useState, useEffect, useRef, useCallback } from "react";
import { LuPlus, LuTrash, LuX, LuPenLine, LuLoader, LuGripVertical, LuInfo } from "react-icons/lu";
import { LuPlus, LuTrash, LuX, LuPenLine, LuLoader, LuGripVertical, LuInfo, LuCopy, LuArrowUp, LuArrowDown } from "react-icons/lu";
import { Combobox, ComboboxInput, ComboboxOption, ComboboxOptions } from "@headlessui/react";
import { KeySequence, useMacrosStore } from "../hooks/stores";
import { SettingsPageHeader } from "../components/SettingsPageheader";
import { Button } from "../components/Button";
import { SettingsPageHeader } from "@/components/SettingsPageheader";
import { Button } from "@/components/Button";
import Checkbox from "@/components/Checkbox";
import { keys, modifiers } from "../keyboardMappings";
import { useJsonRpc } from "../hooks/useJsonRpc";
import notifications from "../notifications";
import { SettingsItem } from "../routes/devices.$id.settings";
import { InputFieldWithLabel, FieldError } from "@/components/InputField";
import Fieldset from "@/components/Fieldset";
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
import EmptyCard from "@/components/EmptyCard";
const DEFAULT_DELAY = 50;
@ -109,15 +114,15 @@ function KeyCombobox({
}
const PRESET_DELAYS = [
{ value: 50, label: "50ms" },
{ value: 100, label: "100ms" },
{ value: 200, label: "200ms" },
{ value: 300, label: "300ms" },
{ value: 500, label: "500ms" },
{ value: 750, label: "750ms" },
{ value: 1000, label: "1000ms" },
{ value: 1500, label: "1500ms" },
{ value: 2000, label: "2000ms" },
{ value: "50", label: "50ms" },
{ value: "100", label: "100ms" },
{ value: "200", label: "200ms" },
{ value: "300", label: "300ms" },
{ value: "500", label: "500ms" },
{ value: "750", label: "750ms" },
{ value: "1000", label: "1000ms" },
{ value: "1500", label: "1500ms" },
{ value: "2000", label: "2000ms" },
];
const MAX_STEPS_PER_MACRO = 10;
@ -171,26 +176,20 @@ function MacroStepCard({
<div className="mb-2 flex items-center justify-between">
<div className="flex items-center gap-1.5">
<div className="flex items-center gap-1">
<button
type="button"
className="p-1 text-slate-400 hover:text-slate-600 dark:text-slate-500 dark:hover:text-slate-400 disabled:opacity-50"
<Button
size="XS"
theme="light"
onClick={onMoveUp}
disabled={stepIndex === 0}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m18 15-6-6-6 6"/>
</svg>
</button>
<button
type="button"
className="p-1 text-slate-400 hover:text-slate-600 dark:text-slate-500 dark:hover:text-slate-400 disabled:opacity-50"
LeadingIcon={LuArrowUp}
/>
<Button
size="XS"
theme="light"
onClick={onMoveDown}
disabled={isLastStep}
>
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="m6 9 6 6 6-6"/>
</svg>
</button>
LeadingIcon={LuArrowDown}
/>
</div>
<span className="macro-step-number flex h-5 w-5 items-center justify-center rounded-full bg-blue-100 text-xs font-semibold text-blue-700 dark:bg-blue-900/40 dark:text-blue-200">
{stepIndex + 1}
@ -199,18 +198,13 @@ function MacroStepCard({
<div className="flex items-center space-x-2">
{onDelete && (
<button
type="button"
className="flex items-center text-xs text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
<Button
size="XS"
theme="danger"
text="Delete"
onClick={onDelete}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"></path>
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"></path>
</svg>
<span className="ml-1">Delete</span>
</button>
LeadingIcon={LuTrash}
/>
)}
</div>
</div>
@ -236,9 +230,9 @@ function MacroStepCard({
: 'bg-slate-100 border-slate-200 text-slate-600 hover:bg-slate-200 dark:bg-slate-800 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-700'
}`}
>
<input
type="checkbox"
<Checkbox
className="sr-only"
size="SM"
checked={ensureArray(step.modifiers).includes(option.value)}
onChange={e => {
const modifiersArray = ensureArray(step.modifiers);
@ -266,19 +260,20 @@ function MacroStepCard({
{ensureArray(step.keys).map((key, keyIndex) => (
<span
key={keyIndex}
className="macro-key-badge inline-flex items-center rounded-md bg-blue-100 px-2 py-1 text-xs font-medium text-blue-700 dark:bg-blue-900/40 dark:text-blue-200"
className="inline-flex items-center rounded-md bg-blue-100 px-2 py-1 text-xs font-medium text-blue-700 dark:bg-blue-900/40 dark:text-blue-200"
>
<span className="px-1">
{key}
<button
type="button"
className="ml-1 text-xs text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
</span>
<Button
size="XS"
theme="blank"
onClick={() => {
const newKeys = ensureArray(step.keys).filter((_, i) => i !== keyIndex);
onKeySelect({ value: null, keys: newKeys });
}}
>
×
</button>
LeadingIcon={LuX}
/>
</span>
))}
</div>
@ -313,17 +308,13 @@ function MacroStepCard({
</div>
</div>
<div className="flex items-center gap-3">
<select
className="w-full rounded-md border border-slate-300 bg-slate-50 p-2 text-sm shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 dark:border-slate-600 dark:bg-slate-800 dark:text-white"
value={step.delay}
<SelectMenuBasic
size="SM"
fullWidth
value={step.delay.toString()}
onChange={(e) => onDelayChange(parseInt(e.target.value, 10))}
>
{PRESET_DELAYS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
options={PRESET_DELAYS}
/>
</div>
</div>
</div>
@ -928,9 +919,7 @@ export default function SettingsMacrosRoute() {
const ErrorMessage = ({ error }: { error?: string }) => {
if (!error) return null;
return (
<p className="mt-1 text-xs text-red-500 dark:text-red-400">
{error}
</p>
<FieldError error={error} />
);
};
@ -940,22 +929,7 @@ export default function SettingsMacrosRoute() {
title="Keyboard Macros"
description="Create and manage keyboard macros for quick actions"
/>
{errorMessage && (
<div className="macro-error">
<div className="flex">
<div className="flex-shrink-0">
<svg className="macro-error-icon" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.28 7.22a.75.75 0 00-1.06 1.06L8.94 10l-1.72 1.72a.75.75 0 101.414 1.414L10 11.414l1.72 1.72a.75.75 0 101.414-1.414L11.414 10l1.72-1.72a.75.75 0 00-1.06-1.06L10 8.586 8.28 7.22z" clipRule="evenodd" />
</svg>
</div>
<div className="ml-3">
<h3 className="macro-error-text">{errorMessage}</h3>
</div>
</div>
</div>
)}
{macros.length > 0 && (
<div className="flex items-center justify-between mb-4">
<SettingsItem
title="Macros"
@ -974,6 +948,9 @@ export default function SettingsMacrosRoute() {
</div>
</SettingsItem>
</div>
)}
{errorMessage && (<FieldError error={errorMessage} />)}
{loading && (
<div className="flex items-center justify-center p-8">
@ -986,12 +963,14 @@ export default function SettingsMacrosRoute() {
<div className="flex items-center justify-between mb-2">
<h3 className="text-sm font-semibold text-black dark:text-white">Add New Macro</h3>
</div>
<Fieldset>
<div className="grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="flex flex-col">
<input
<InputFieldWithLabel
type="text"
className={`macro-input ${errors.name ? 'border-red-500 dark:border-red-500' : ''}`}
label="Macro Name"
placeholder="Macro Name"
value={newMacro.name}
error={errors.name}
onChange={e => {
setNewMacro(prev => ({ ...prev, name: e.target.value }));
if (errors.name) {
@ -1000,15 +979,13 @@ export default function SettingsMacrosRoute() {
setErrors(newErrors);
}
}}
placeholder="Macro Name"
/>
<ErrorMessage error={errors.name} />
</div>
<div className="flex flex-col">
<input
<InputFieldWithLabel
type="text"
className={`macro-input ${errors.description ? 'border-red-500 dark:border-red-500' : ''}`}
label="Description"
placeholder="Description (optional)"
value={newMacro.description}
error={errors.description}
onChange={e => {
setNewMacro(prev => ({ ...prev, description: e.target.value }));
if (errors.description) {
@ -1017,11 +994,9 @@ export default function SettingsMacrosRoute() {
setErrors(newErrors);
}
}}
placeholder="Description (optional)"
/>
<ErrorMessage error={errors.description} />
</div>
</div>
</Fieldset>
<div className="mt-4">
<div className="macro-section-header">
@ -1040,6 +1015,7 @@ export default function SettingsMacrosRoute() {
<div className="mt-2 text-xs text-slate-500 dark:text-slate-400">
You can add up to {MAX_STEPS_PER_MACRO} steps per macro
</div>
<Fieldset>
<div className="mt-2 space-y-4">
{(newMacro.steps || []).map((step, stepIndex) => (
<MacroStepCard
@ -1069,15 +1045,16 @@ export default function SettingsMacrosRoute() {
isLastStep={stepIndex === (newMacro.steps?.length || 0) - 1}
/>
))}
</div>
</Fieldset>
<div className="mt-4 border-t border-slate-200 pt-4 dark:border-slate-700">
<button
type="button"
className={`w-full flex items-center justify-center gap-1 rounded-md px-3 py-2 text-sm font-medium transition-colors ${
isMaxStepsReachedForNewMacro
? 'bg-slate-100 text-slate-400 cursor-not-allowed dark:bg-slate-800 dark:text-slate-500'
: 'bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700'
}`}
<Button
size="MD"
theme="light"
fullWidth
LeadingIcon={LuPlus}
text={`Add Step ${isMaxStepsReachedForNewMacro ? `(${MAX_STEPS_PER_MACRO} max)` : ''}`}
onClick={() => {
if (isMaxStepsReachedForNewMacro) {
showTemporaryError(`You can only add a maximum of ${MAX_STEPS_PER_MACRO} steps per macro.`);
@ -1094,10 +1071,7 @@ export default function SettingsMacrosRoute() {
clearErrors();
}}
disabled={isMaxStepsReachedForNewMacro}
>
<LuPlus className="h-4 w-4" />
<span>Add Step {isMaxStepsReachedForNewMacro && `(${MAX_STEPS_PER_MACRO} max)`}</span>
</button>
/>
</div>
<div className="mt-6 flex items-center justify-between border-t border-slate-200 pt-4 dark:border-slate-700">
@ -1136,7 +1110,6 @@ export default function SettingsMacrosRoute() {
size="SM"
theme="light"
text="Cancel"
LeadingIcon={LuX}
onClick={() => {
if (newMacro.name || newMacro.description || newMacro.steps?.some(s => s.keys?.length || s.modifiers?.length)) {
setShowClearConfirm(true);
@ -1151,26 +1124,34 @@ export default function SettingsMacrosRoute() {
</div>
</div>
</div>
</div>
)}
{macros.length === 0 && !showAddMacro && (
<EmptyCard
headline="No macros created yet"
BtnElm={
<Button
size="SM"
theme="primary"
text="Add New Macro"
onClick={() => setShowAddMacro(true)}
disabled={isMaxMacrosReached}
/>
}
/>
)}
{macros.length > 0 && (
<div className="space-y-2">
{macros.length === 0 ? (
<p className="text-center text-sm text-slate-500 dark:text-slate-400 py-4">
No macros created yet. Add your first macro above.
</p>
) : (
<div className="space-y-1">
{macros.map((macro, index) =>
editingMacro && editingMacro.id === macro.id ? (
<div key={macro.id} className="rounded-md border border-blue-300 bg-blue-50 p-3 dark:border-blue-700 dark:bg-blue-900/20">
<Fieldset>
<div className="mb-2 grid grid-cols-1 md:grid-cols-2 gap-2">
<div className="flex flex-col">
<input
<InputFieldWithLabel
type="text"
className={`macro-input ${errors.name ? 'border-red-500 dark:border-red-500' : ''}`}
label="Macro Name"
placeholder="Macro Name"
value={editingMacro.name}
error={errors.name}
onChange={e => {
setEditingMacro({ ...editingMacro, name: e.target.value });
if (errors.name) {
@ -1179,15 +1160,13 @@ export default function SettingsMacrosRoute() {
setErrors(newErrors);
}
}}
placeholder="Macro Name"
/>
<ErrorMessage error={errors.name} />
</div>
<div className="flex flex-col">
<input
<InputFieldWithLabel
type="text"
className={`macro-input ${errors.description ? 'border-red-500 dark:border-red-500' : ''}`}
label="Description"
placeholder="Description (optional)"
value={editingMacro.description}
error={errors.description}
onChange={e => {
setEditingMacro({ ...editingMacro, description: e.target.value });
if (errors.description) {
@ -1196,11 +1175,9 @@ export default function SettingsMacrosRoute() {
setErrors(newErrors);
}
}}
placeholder="Description (optional)"
/>
<ErrorMessage error={errors.description} />
</div>
</div>
</Fieldset>
<div className="mt-4">
<div className="flex items-center justify-between">
@ -1219,6 +1196,7 @@ export default function SettingsMacrosRoute() {
<div className="mt-2 text-xs text-slate-500 dark:text-slate-400">
You can add up to {MAX_STEPS_PER_MACRO} steps per macro
</div>
<Fieldset>
<div className="mt-2 space-y-4">
{editingMacro.steps.map((step, stepIndex) => (
<MacroStepCard
@ -1248,15 +1226,16 @@ export default function SettingsMacrosRoute() {
isLastStep={stepIndex === editingMacro.steps.length - 1}
/>
))}
</div>
</Fieldset>
<div className="mt-4 border-t border-slate-200 pt-4 dark:border-slate-700">
<button
type="button"
className={`w-full flex items-center justify-center gap-1 rounded-md px-3 py-2 text-sm font-medium transition-colors ${
editingMacro.steps.length >= MAX_STEPS_PER_MACRO
? 'bg-slate-100 text-slate-400 cursor-not-allowed dark:bg-slate-800 dark:text-slate-500'
: 'bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700'
}`}
<Button
size="MD"
theme="light"
fullWidth
LeadingIcon={LuPlus}
text={`Add Step ${editingMacro.steps.length >= MAX_STEPS_PER_MACRO ? `(${MAX_STEPS_PER_MACRO} max)` : ''}`}
onClick={() => {
if (editingMacro.steps.length >= MAX_STEPS_PER_MACRO) {
showTemporaryError(`You can only add a maximum of ${MAX_STEPS_PER_MACRO} steps per macro.`);
@ -1273,11 +1252,7 @@ export default function SettingsMacrosRoute() {
clearErrors();
}}
disabled={editingMacro.steps.length >= MAX_STEPS_PER_MACRO}
>
<LuPlus className="h-4 w-4" />
<span>Add Step {editingMacro.steps.length >= MAX_STEPS_PER_MACRO && `(${MAX_STEPS_PER_MACRO} max)`}</span>
</button>
</div>
/>
</div>
<div className="mt-4 flex items-center justify-between border-t border-slate-200 pt-4 dark:border-slate-700">
@ -1293,7 +1268,6 @@ export default function SettingsMacrosRoute() {
size="SM"
theme="light"
text="Cancel"
LeadingIcon={LuX}
onClick={() => {
setEditingMacro(null);
setErrors({});
@ -1378,14 +1352,15 @@ export default function SettingsMacrosRoute() {
<span className="text-sm text-slate-600 dark:text-slate-400">
Delete macro?
</span>
<div className="flex items-center gap-x-2">
<Button
size="XS"
theme="danger"
text={isDeleting ? "Deleting..." : "Yes"}
disabled={isDeleting}
text="Yes"
onClick={() => {
handleDeleteMacro(macro.id);
}}
disabled={isDeleting}
/>
<Button
size="XS"
@ -1394,35 +1369,28 @@ export default function SettingsMacrosRoute() {
onClick={() => setMacroToDelete(null)}
/>
</div>
</div>
) : (
<>
<button
type="button"
className="rounded-md p-1 text-slate-500 hover:bg-slate-100 hover:text-green-500 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-green-400"
<Button
size="XS"
theme="light"
LeadingIcon={LuPenLine}
onClick={() => handleEditMacro(macro)}
title="Edit"
>
<LuPenLine className="h-4 w-4" />
</button>
<button
type="button"
className="rounded-md p-1 text-slate-500 hover:bg-slate-100 hover:text-red-500 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-red-400"
/>
<Button
size="XS"
theme="light"
LeadingIcon={LuCopy}
onClick={() => handleDuplicateMacro(macro)}
title="Duplicate"
>
<svg className="h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="8" y="8" width="12" height="12" rx="2" />
<path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" />
</svg>
</button>
<button
type="button"
className="rounded-md p-1 text-slate-500 hover:bg-slate-100 hover:text-red-500 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-red-400"
/>
<Button
size="XS"
theme="light"
LeadingIcon={LuTrash}
onClick={() => setMacroToDelete(macro.id)}
title="Delete"
>
<LuTrash className="h-4 w-4" />
</button>
className="text-red-500 dark:text-red-400"
/>
</>
)}
</div>
@ -1432,8 +1400,6 @@ export default function SettingsMacrosRoute() {
</div>
)}
</div>
)}
</div>
</div>
);
}