From b2e8345ab6b21eabab1c67bad9b6e54ad83d78a3 Mon Sep 17 00:00:00 2001 From: Blur Date: Thu, 2 Jul 2026 21:42:47 +0200 Subject: [PATCH 01/45] Added Side bar to settings panel --- CHANGELOG.md | 4 + src/App.tsx | 6 +- src/components/TitleBar.css | 5 + src/components/TitleBar.tsx | 1 + src/components/panels/SettingsPanel.tsx | 1264 ----------------- .../panels/{ => settings}/SettingsPanel.css | 136 +- .../panels/settings/SettingsPanel.tsx | 155 ++ .../settings/sections/AppearanceSection.tsx | 219 +++ .../settings/sections/BehaviorSection.tsx | 190 +++ .../settings/sections/GeneralSection.tsx | 292 ++++ .../settings/sections/MaintenanceSection.tsx | 123 ++ .../settings/sections/PresetsSection.tsx | 338 +++++ .../settings/sections/StartupSection.tsx | 83 ++ .../panels/settings/sections/shared.tsx | 37 + 14 files changed, 1544 insertions(+), 1309 deletions(-) delete mode 100644 src/components/panels/SettingsPanel.tsx rename src/components/panels/{ => settings}/SettingsPanel.css (84%) create mode 100644 src/components/panels/settings/SettingsPanel.tsx create mode 100644 src/components/panels/settings/sections/AppearanceSection.tsx create mode 100644 src/components/panels/settings/sections/BehaviorSection.tsx create mode 100644 src/components/panels/settings/sections/GeneralSection.tsx create mode 100644 src/components/panels/settings/sections/MaintenanceSection.tsx create mode 100644 src/components/panels/settings/sections/PresetsSection.tsx create mode 100644 src/components/panels/settings/sections/StartupSection.tsx create mode 100644 src/components/panels/settings/sections/shared.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 10c96f6b..38a2e682 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# v3.9.0 - 02.07.2026 (d.m.y) +## Changed +- Settings panel now has a side menu for easier navigation and organization of settings. + # v3.8.3 - 28.06.2026 (d.m.y) ## Changed - Changed max Process list title length to 35 characters to hopefully prevent ui clipping. diff --git a/src/App.tsx b/src/App.tsx index d3d28646..71b8300c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -42,7 +42,9 @@ const AdvancedPanel = lazy( () => import("./components/panels/advanced/AdvancedPanel"), ); const ZonesPanel = lazy(() => import("./components/panels/zones/ZonesPanel")); -const SettingsPanel = lazy(() => import("./components/panels/SettingsPanel")); +const SettingsPanel = lazy( + () => import("./components/panels/settings/SettingsPanel"), +); const TitleBar = lazy(() => import("./components/TitleBar")); export type Tab = "simple" | "advanced" | "zones" | "settings"; @@ -66,7 +68,7 @@ function getPanelSize( if (tab === "simple") { return { width: 650, height: 175 + extra }; } - if (tab === "settings") return { width: 560, height: 720 + extra }; + if (tab === "settings") return { width: 700, height: 720 + extra }; if (tab === "zones") return { width: 750, height: 720 + extra }; if (advancedSequenceLayout === "tall") { return { width: 560, height: 720 + extra }; diff --git a/src/components/TitleBar.css b/src/components/TitleBar.css index da2023b3..0e46ce1d 100644 --- a/src/components/TitleBar.css +++ b/src/components/TitleBar.css @@ -14,6 +14,11 @@ backdrop-filter: blur(var(--bg-panel-blur, 0px)); } +.window-title-background[data-tab="settings"] { + border-bottom: none; + height: calc(2.5rem - 2px); +} + .title-wrapper { position: absolute; left: 50%; diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx index 5ef2ab3c..c9d484c9 100644 --- a/src/components/TitleBar.tsx +++ b/src/components/TitleBar.tsx @@ -215,6 +215,7 @@ const TitleBar = memo(function TitleBar({ } data-tauri-drag-region data-running={running} + data-tab={tab} >
- - - ) : isConfirmingDelete ? ( - <> - - - - ) : ( - <> - - - - - - )} -
- - - ); -} - -export default function SettingsPanel({ - settings, - update, - running, - appInfo, - onSavePreset, - onApplyPreset, - onUpdatePreset, - onRenamePreset, - onDeletePreset, - onToggleAlwaysOnTop, - onReset, - updateCheckStatus, - onCheckForUpdate, -}: Props) { - const [resetting, setResetting] = useState(false); - const [resettingStats, setResettingStats] = useState(false); - const [pendingAction, setPendingAction] = useState(null); - const [stats, setStats] = useState(null); - const [atBottom, setAtBottom] = useState(false); - const [presetsAtBottom, setPresetsAtBottom] = useState(true); - const [autostartEnabled, setAutostartEnabled] = useState( - null, - ); - const [newPresetName, setNewPresetName] = useState(""); - const [editingPresetId, setEditingPresetId] = useState(null); - const [renameDraft, setRenameDraft] = useState(""); - const [confirmingDeleteId, setConfirmingDeleteId] = useState( - null, - ); - const [showChangelog, setShowChangelog] = useState(false); - const [diagnosticsStatus, setDiagnosticsStatus] = useState( - null, - ); - const [exporting, setExporting] = useState(false); - - const panelRef = useRef(null); - const presetsListRef = useRef(null); - const language = "en"; - useEffect(() => { - invoke("get_stats") - .then(setStats) - .catch(() => {}); - invoke("get_autostart_enabled") - .then(setAutostartEnabled) - .catch(() => setAutostartEnabled(false)); - }, []); - - useEffect(() => { - if (!confirmingDeleteId) { - return; - } - - const handlePointerDown = (event: PointerEvent) => { - const target = event.target; - if (!(target instanceof Element)) { - return; - } - - const presetCard = target.closest("[data-preset-id]"); - if (presetCard?.getAttribute("data-preset-id") === confirmingDeleteId) { - return; - } - - setConfirmingDeleteId(null); - }; - - document.addEventListener("pointerdown", handlePointerDown); - return () => { - document.removeEventListener("pointerdown", handlePointerDown); - }; - }, [confirmingDeleteId]); - - const handleScroll = () => { - const el = panelRef.current; - if (!el) return; - setAtBottom(el.scrollTop + el.clientHeight >= el.scrollHeight - 2); - }; - - const handlePresetsScroll = () => { - const el = presetsListRef.current; - if (!el) return; - setPresetsAtBottom(el.scrollTop + el.clientHeight >= el.scrollHeight - 2); - }; - - const handleSavePreset = () => { - if (onSavePreset(newPresetName)) { - setNewPresetName(""); - setConfirmingDeleteId(null); - } - }; - - const handleStartRename = (preset: PresetDefinition) => { - setConfirmingDeleteId(null); - setEditingPresetId(preset.id); - setRenameDraft(preset.name); - }; - - const handleCommitRename = () => { - if (!editingPresetId) { - return; - } - - if (onRenamePreset(editingPresetId, renameDraft)) { - setEditingPresetId(null); - setRenameDraft(""); - } - }; - - const handleCancelRename = () => { - setEditingPresetId(null); - setRenameDraft(""); - }; - - const handleRequestDelete = (presetId: PresetId) => { - setEditingPresetId(null); - setRenameDraft(""); - setConfirmingDeleteId(presetId); - }; - - const handleConfirmDelete = (presetId: PresetId) => { - if (onDeletePreset(presetId)) { - setConfirmingDeleteId(null); - } - }; - - const handleBrowseBackgroundImage = async () => { - try { - const selected = await open({ - multiple: false, - filters: IMAGE_FILTERS, - }); - if (selected) { - update({ backgroundImage: selected }); - } - } catch (err) { - error( - JSON.stringify({ - source: "SettingsPanel.pickImage", - error: String(err), - }), - ); - } - }; - - const handleAlwaysOnTopChange = (nextValue: boolean) => { - if (settings.alwaysOnTop === nextValue) { - return; - } - - void onToggleAlwaysOnTop(); - }; - - const hasStats = stats !== null && stats.totalSessions > 0; - const presetLimitReached = settings.presets.length >= MAX_PRESETS; - const activeEditingPresetId = running ? null : editingPresetId; - const activeConfirmingDeleteId = running ? null : confirmingDeleteId; - const onOffOptions = [ - { value: false, label: "Off" }, - { value: true, label: "On" }, - ]; - const advancedLayoutOptions = [ - { value: "wide" as const, label: "Wide" }, - { value: "tall" as const, label: "Tall" }, - ]; - - const handleConfirmResetSettings = async () => { - setResetting(true); - try { - await onReset(); - setAutostartEnabled(false); - } finally { - setResetting(false); - setPendingAction(null); - } - }; - - const handleConfirmClearStats = async () => { - setResettingStats(true); - try { - const next = await invoke("reset_stats"); - setStats(next); - } catch { - // swallow ? failure leaves stats unchanged - } finally { - setResettingStats(false); - setPendingAction(null); - } - }; - - const handleExtendedClickSpeedLimitChange = (nextValue: boolean) => { - if (settings.extendedClickSpeedLimit === nextValue) { - return; - } - - if (nextValue) { - setPendingAction("extended-click-speed-limit"); - return; - } - - update({ - extendedClickSpeedLimit: false, - clickSpeed: Math.min(settings.clickSpeed, DEFAULT_MAX_CLICK_SPEED), - }); - }; - - const handleConfirmExtendedClickSpeedLimit = () => { - update({ extendedClickSpeedLimit: true }); - setPendingAction(null); - }; - - useEffect(() => { - handlePresetsScroll(); - }, [settings.presets.length]); - - const updateButtonLabel = { - idle: "Check for Update", - checking: "Checking...", - available: "Update found!", - unavailable: "No update found", - error: "Check failed", - }[updateCheckStatus]; - - return ( -
-
- - - -
-
- Version - v{appInfo.version} -
-
- - -
-
- {showChangelog && } -
- - -
-
- Usage Data - - Statistics are stored locally and never sent anywhere. - -
-
- {hasStats ? ( -
-
- Total Clicks - - {formatNumber(stats.totalClicks, language)} - -
-
- Total Time Clicking - - {formatTime(stats.totalTimeSecs, language)} - -
-
- Average CPU - - {formatCpu(stats.avgCpu, language, "N/A")} - -
-
- Sessions - - {formatNumber(stats.totalSessions, language)} - -
-
- ) : ( -
No session data yet.
- )} - {hasStats && ( -
-
- Clear Stats - Clear all usage data. -
- -
- )} -
- - -
-
- Presets - - Save and restore to quickly switch configurations. - -
-
- setNewPresetName(event.target.value)} - onKeyDown={(event) => { - if (event.key === "Enter") { - event.preventDefault(); - if ( - !running && - !presetLimitReached && - newPresetName.trim() - ) { - handleSavePreset(); - } - } - if (event.key === "Escape") { - event.preventDefault(); - setNewPresetName(""); - } - }} - disabled={running} - /> - -
- {presetLimitReached && ( - Max 6 presets allowed - )} - {running && ( - Disabled while clicking - )} - {settings.presets.length > 0 ? ( -
-
- {settings.presets.map((preset) => ( - handleStartRename(preset)} - onCancelRename={handleCancelRename} - onCommitRename={handleCommitRename} - onApply={() => { - setConfirmingDeleteId(null); - onApplyPreset(preset.id); - }} - onUpdatePreset={() => { - setConfirmingDeleteId(null); - onUpdatePreset(preset.id); - }} - onRequestDelete={() => handleRequestDelete(preset.id)} - onCancelDelete={() => setConfirmingDeleteId(null)} - onConfirmDelete={() => handleConfirmDelete(preset.id)} - /> - ))} -
-
-
- ) : ( -
No saved presets.
- )} -
- - - -
-
- Always on Top - - Keep the window above others. - -
-
- {onOffOptions.map((option) => ( - - ))} -
-
- -
-
- Stop Hitbox Overlay - - Show the stop zone boundaries. - -
-
- {onOffOptions.map((option) => ( - - ))} -
-
- -
-
- Stop Reason Alert - - Show a notification when the auto clicker stops. - -
-
- {onOffOptions.map((option) => ( - - ))} -
-
- -
-
- Strict Hotkey Modifiers - - Require exact modifier keys for hotkeys. - -
-
- {onOffOptions.map((option) => ( - - ))} -
-
- -
-
- Stop on Alt+Tab - - Stop clicking when switching to another window. - -
-
- {onOffOptions.map((option) => ( - - ))} -
-
- -
-
- Extended Click Speed Limit - - Allow click speeds up to 1000 CPS (may affect performance). - -
-
- {onOffOptions.map((option) => ( - - ))} -
-
-
- - -
-
- Minimize to Tray - - Minimize to the system tray instead of the taskbar. - -
-
- {onOffOptions.map((option) => ( - - ))} -
-
- -
-
- Run on Startup - - Start clicking when the app opens. - -
-
- {onOffOptions.map((option) => ( - - ))} -
-
-
- - -
-
- Theme - - Choose between dark and light mode. - -
-
- {(["dark", "light"] as const).map((theme) => ( - - ))} -
-
- -
-
- Advanced Layout - - Panel layout for sequence zones. - -
-
- {advancedLayoutOptions.map((option) => ( - - ))} -
-
- -
-
- Accent Color - - The primary accent color. - -
-
- - - {settings.accentColor.toUpperCase()} - - -
-
- -
-
- Background Image - - Path or URL to a background image. - -
-
- - update({ backgroundImage: event.target.value }) - } - placeholder="https://example.com/image.png" - /> -
- - -
-
-
- -
-
- Background Opacity - - Transparency of the background image. - -
-
- - update({ - backgroundOpacity: Number(event.target.value), - }) - } - /> - - {settings.backgroundOpacity}% - -
-
- -
-
- Panel Opacity - - Transparency of the settings panel. - -
-
- - update({ - panelOpacity: Number(event.target.value), - }) - } - /> - - {settings.panelOpacity}% - -
-
- -
-
- Panel Blur - - Blur effect behind the panel. - -
-
- - update({ - panelBlur: Number(event.target.value), - }) - } - /> - - {settings.panelBlur}px - -
-
-
- - -
-
- Reset All - - Reset all settings to their defaults. - -
- -
-
- - -
-
- Diagnostics - - View or export your diagnostics. - -
-
- - -
-
- {diagnosticsStatus && ( - {diagnosticsStatus} - )} -
-
-
- setPendingAction(null)} - /> - setPendingAction(null)} - /> - setPendingAction(null)} - /> -
- ); -} diff --git a/src/components/panels/SettingsPanel.css b/src/components/panels/settings/SettingsPanel.css similarity index 84% rename from src/components/panels/SettingsPanel.css rename to src/components/panels/settings/SettingsPanel.css index aa0caa10..791f250e 100644 --- a/src/components/panels/SettingsPanel.css +++ b/src/components/panels/settings/SettingsPanel.css @@ -1,25 +1,81 @@ -.settings-panel { +.settings-wrapper { + display: flex; + flex-direction: row; + position: relative; + flex: 1; + overflow: hidden; + margin: -1.25rem -1.25rem -0.75rem -1.25rem; +} + +.settings-sidebar { + width: 10rem; + flex-shrink: 0; display: flex; flex-direction: column; - gap: 0.75rem; - padding: 0.25rem 0.75rem 1rem; - height: 100%; + gap: 0.125rem; + padding: 0.5rem 0; + background: var(--bg-surface); + backdrop-filter: blur(var(--bg-panel-blur, 0px)); overflow-y: auto; } -.settings-panel::-webkit-scrollbar { - width: 0.375rem; +.sidebar-tab { + all: unset; + cursor: pointer; + font-family: var(--font-family-base); + font-size: 0.8125rem; + font-weight: var(--font-weight-md); + color: var(--text-muted); + padding: 0.5rem 0.75rem; + border-left: 3px solid transparent; + transition: + color 0.15s ease, + background 0.15s ease, + border-color 0.15s ease; + text-align: left; +} + +.sidebar-tab:hover { + color: var(--text-primary); background: var(--bg-elevated); - border-radius: 0.5rem; } -.settings-panel::-webkit-scrollbar-thumb { - background: var(--text-dim); - border-radius: 0.25rem; +.sidebar-tab.active { + color: var(--text-primary); + background: var(--bg-elevated); + border-left-color: var(--accent-green-strong); } -.settings-panel::-webkit-scrollbar-thumb:hover { - background: #777; +.settings-panel { + position: relative; + flex: 1; + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 0.75rem 0.75rem 1rem; + border-top: 2px solid rgb(49, 49, 49); + border-left: 2px solid rgb(49, 49, 49); + border-top-left-radius: var(--radius-lg); + overflow-y: auto; + min-width: 0; +} + +.settings-panel::before { + content: ""; + position: absolute; + top: -2px; + left: -2px; + width: var(--radius-lg); + height: var(--radius-lg); + background: var(--bg-surface); + pointer-events: none; + -webkit-mask-image: radial-gradient(circle at var(--radius-lg) var(--radius-lg), transparent var(--radius-lg), black var(--radius-lg)); + mask-image: radial-gradient(circle at var(--radius-lg) var(--radius-lg), transparent var(--radius-lg), black var(--radius-lg)); +} + +[data-theme="light"] .settings-panel { + border-top-color: rgb(166, 166, 166); + border-left-color: rgb(166, 166, 166); } .settings-section-heading { @@ -54,7 +110,7 @@ .settings-label-group { display: flex; flex-direction: column; - gap: 2px; + gap: 0.125rem; flex: 1; min-width: 0; } @@ -245,7 +301,7 @@ } .settings-btn-danger--compact { - padding-inline: 10px; + padding-inline: 0.625rem; } .settings-btn-primary:disabled, @@ -256,18 +312,12 @@ cursor: not-allowed; } -.settings-wrapper { - position: relative; - flex: 1; - overflow: hidden; -} - .settings-fade { position: absolute; bottom: 0; left: 0; right: 0; - height: 10vb; + height: 6rem; background: linear-gradient(to bottom, transparent, var(--bg-base)); pointer-events: none; transition: opacity 0.3s ease; @@ -278,21 +328,21 @@ } .settings-note { - font-size: 12px; + font-size: 0.75rem; color: var(--text-dim); } .settings-color-controls { display: flex; align-items: center; - gap: 10px; + gap: 0.625rem; flex-wrap: wrap; } .settings-color-picker { - width: 40px; - height: 40px; - padding: 4px; + width: 2.5rem; + height: 2.5rem; + padding: 0.25rem; border-radius: var(--radius-md); border: 1px solid var(--border-subtle); background: var(--bg-elevated); @@ -310,7 +360,7 @@ .preset-compose { display: flex; - gap: 8px; + gap: 0.5rem; flex-wrap: wrap; } @@ -318,7 +368,7 @@ .preset-rename-input { min-width: 0; flex: 1; - padding: 9px 12px; + padding: 0.5625rem 0.75rem; border-radius: var(--radius-md); border: 1px solid var(--border-subtle); background: var(--bg-elevated); @@ -339,7 +389,7 @@ .preset-list { display: flex; flex-direction: column; - gap: 10px; + gap: 0.625rem; max-height: clamp(20rem, 42vh, 26rem); overflow-y: auto; padding-right: 0.25rem; @@ -385,7 +435,7 @@ border: 1px solid var(--border-subtle); border-radius: var(--radius-lg); background: var(--bg-elevated); - padding: 12px; + padding: 0.75rem; } .preset-card--active { @@ -396,36 +446,36 @@ .preset-card-head { display: flex; justify-content: space-between; - gap: 12px; + gap: 0.75rem; flex-wrap: wrap; } .preset-card-meta { display: flex; flex-direction: column; - gap: 8px; + gap: 0.5rem; min-width: 0; flex: 1; } .preset-name { - font-size: 15px; + font-size: 0.9375rem; font-weight: var(--font-weight-md); color: var(--text-primary); } .preset-badges { display: flex; - gap: 8px; + gap: 0.5rem; flex-wrap: wrap; } .preset-badge { - padding: 3px 8px; - border-radius: 999px; + padding: 0.1875rem 0.5rem; + border-radius: 9999px; background: var(--bg-input); color: var(--text-muted); - font-size: 11px; + font-size: 0.6875rem; font-weight: var(--font-weight-md); letter-spacing: 0.04em; text-transform: uppercase; @@ -439,7 +489,7 @@ .preset-actions { display: flex; - gap: 8px; + gap: 0.5rem; flex-wrap: wrap; align-items: flex-start; } @@ -501,9 +551,9 @@ } .social-icon--kofi { - width: 114px; - height: 30px; - border-radius: 10px; + width: 7.125rem; + height: 1.875rem; + border-radius: 0.625rem; border: 1px solid var(--border-subtle); } @@ -536,7 +586,7 @@ .stats-cell { display: flex; flex-direction: column; - gap: 2px; + gap: 0.125rem; } .stats-cell-label { @@ -573,7 +623,7 @@ .settings-row .adv-dropdown-trigger { box-sizing: border-box; - height: 30px; + height: 1.875rem; padding: 0 0.5rem 0 0.5rem; background: var(--bg-elevated); border: 1px solid var(--border-subtle); diff --git a/src/components/panels/settings/SettingsPanel.tsx b/src/components/panels/settings/SettingsPanel.tsx new file mode 100644 index 00000000..9575fbd4 --- /dev/null +++ b/src/components/panels/settings/SettingsPanel.tsx @@ -0,0 +1,155 @@ +import "./SettingsPanel.css"; +import { useEffect, useRef, useState } from "react"; +import type { AppInfo, PresetId, Settings } from "../../../store"; + +import GeneralSection from "./sections/GeneralSection"; +import BehaviorSection from "./sections/BehaviorSection"; +import StartupSection from "./sections/StartupSection"; +import AppearanceSection from "./sections/AppearanceSection"; +import PresetsSection from "./sections/PresetsSection"; +import MaintenanceSection from "./sections/MaintenanceSection"; + +type SettingsTab = + | "general" + | "behavior" + | "startup" + | "appearance" + | "presets" + | "maintenance"; + +interface Props { + settings: Settings; + update: (patch: Partial) => void; + running: boolean; + appInfo: AppInfo; + onSavePreset: (name: string) => boolean; + onApplyPreset: (presetId: PresetId) => boolean; + onUpdatePreset: (presetId: PresetId) => boolean; + onRenamePreset: (presetId: PresetId, name: string) => boolean; + onDeletePreset: (presetId: PresetId) => boolean; + onToggleAlwaysOnTop: () => Promise; + onReset: () => Promise; + updateCheckStatus: + | "idle" + | "checking" + | "available" + | "unavailable" + | "error"; + onCheckForUpdate: () => void; +} + +export default function SettingsPanel({ + settings, + update, + running, + appInfo, + onSavePreset, + onApplyPreset, + onUpdatePreset, + onRenamePreset, + onDeletePreset, + onToggleAlwaysOnTop, + onReset, + updateCheckStatus, + onCheckForUpdate, +}: Props) { + const [activeTab, setActiveTab] = useState("general"); + const [atBottom, setAtBottom] = useState(true); + const panelRef = useRef(null); + + const handleScroll = () => { + const panel = panelRef.current; + if (!panel) return; + setAtBottom(panel.scrollTop + panel.clientHeight >= panel.scrollHeight - 2); + }; + + useEffect(() => { + const panel = panelRef.current; + if (!panel) return; + const observer = new ResizeObserver(handleScroll); + observer.observe(panel); + return () => observer.disconnect(); + }, [activeTab]); + + return ( +
+ +
+ {activeTab === "general" && ( + + )} + {activeTab === "behavior" && ( + + )} + {activeTab === "startup" && ( + + )} + {activeTab === "appearance" && ( + + )} + {activeTab === "presets" && ( + + )} + {activeTab === "maintenance" && ( + + )} +
+
+
+ ); +} diff --git a/src/components/panels/settings/sections/AppearanceSection.tsx b/src/components/panels/settings/sections/AppearanceSection.tsx new file mode 100644 index 00000000..56ee38e9 --- /dev/null +++ b/src/components/panels/settings/sections/AppearanceSection.tsx @@ -0,0 +1,219 @@ +import { error } from "@tauri-apps/plugin-log"; +import { open } from "@tauri-apps/plugin-dialog"; +import { DEFAULT_ACCENT_COLOR } from "../../../../settingsSchema"; +import type { Settings } from "../../../../store"; +import { SettingsCard } from "./shared"; + +const IMAGE_FILTERS = [ + { + name: "Images", + extensions: ["png", "jpg", "jpeg", "gif", "bmp", "webp"], + }, +]; + +const advancedLayoutOptions = [ + { value: "wide" as const, label: "Wide" }, + { value: "tall" as const, label: "Tall" }, +]; + +interface Props { + settings: Settings; + update: (patch: Partial) => void; +} + +export default function AppearanceSection({ settings, update }: Props) { + const handleBrowseBackgroundImage = async () => { + try { + const selected = await open({ + multiple: false, + filters: IMAGE_FILTERS, + }); + if (selected) { + update({ backgroundImage: selected }); + } + } catch (err) { + error( + JSON.stringify({ + source: "SettingsPanel.pickImage", + error: String(err), + }), + ); + } + }; + + return ( + +
+
+ Theme + + Choose between dark and light mode. + +
+
+ {(["dark", "light"] as const).map((theme) => ( + + ))} +
+
+ +
+
+ Advanced Layout + + Panel layout for sequence zones. + +
+
+ {advancedLayoutOptions.map((option) => ( + + ))} +
+
+ +
+
+ Accent Color + The primary accent color. +
+
+ + + {settings.accentColor.toUpperCase()} + + +
+
+ +
+
+ Background Image + + Path or URL to a background image. + +
+
+ + update({ backgroundImage: event.target.value }) + } + placeholder="https://example.com/image.png" + /> +
+ + +
+
+
+ +
+
+ Background Opacity + + Transparency of the background image. + +
+
+ + update({ backgroundOpacity: Number(event.target.value) }) + } + /> + + {settings.backgroundOpacity}% + +
+
+ +
+
+ Panel Opacity + + Transparency of the settings panel. + +
+
+ + update({ panelOpacity: Number(event.target.value) }) + } + /> + + {settings.panelOpacity}% + +
+
+ +
+
+ Panel Blur + + Blur effect behind the panel. + +
+
+ + update({ panelBlur: Number(event.target.value) }) + } + /> + {settings.panelBlur}px +
+
+
+ ); +} diff --git a/src/components/panels/settings/sections/BehaviorSection.tsx b/src/components/panels/settings/sections/BehaviorSection.tsx new file mode 100644 index 00000000..ddb9dea5 --- /dev/null +++ b/src/components/panels/settings/sections/BehaviorSection.tsx @@ -0,0 +1,190 @@ +import { useState } from "react"; +import { DEFAULT_MAX_CLICK_SPEED } from "../../../../settingsSchema"; +import type { Settings } from "../../../../store"; +import ConfirmDialog from "../../../ConfirmDialog"; +import { SettingsCard } from "./shared"; + +const onOffOptions = [ + { value: false, label: "Off" }, + { value: true, label: "On" }, +]; + +interface Props { + settings: Settings; + update: (patch: Partial) => void; + onToggleAlwaysOnTop: () => Promise; +} + +export default function BehaviorSection({ + settings, + update, + onToggleAlwaysOnTop, +}: Props) { + const [pendingAction, setPendingAction] = useState< + "extended-click-speed-limit" | null + >(null); + + const handleAlwaysOnTopChange = (nextValue: boolean) => { + if (settings.alwaysOnTop === nextValue) return; + void onToggleAlwaysOnTop(); + }; + + const handleExtendedClickSpeedLimitChange = (nextValue: boolean) => { + if (settings.extendedClickSpeedLimit === nextValue) return; + if (nextValue) { + setPendingAction("extended-click-speed-limit"); + return; + } + update({ + extendedClickSpeedLimit: false, + clickSpeed: Math.min(settings.clickSpeed, DEFAULT_MAX_CLICK_SPEED), + }); + }; + + const handleConfirmExtendedClickSpeedLimit = () => { + update({ extendedClickSpeedLimit: true }); + setPendingAction(null); + }; + + return ( + <> + +
+
+ Always on Top + + Keep the window above others. + +
+
+ {onOffOptions.map((option) => ( + + ))} +
+
+ +
+
+ Stop Hitbox Overlay + + Show the stop zone boundaries. + +
+
+ {onOffOptions.map((option) => ( + + ))} +
+
+ +
+
+ Stop Reason Alert + + Show a notification when the auto clicker stops. + +
+
+ {onOffOptions.map((option) => ( + + ))} +
+
+ +
+
+ Strict Hotkey Modifiers + + Require exact modifier keys for hotkeys. + +
+
+ {onOffOptions.map((option) => ( + + ))} +
+
+ +
+
+ Stop on Alt+Tab + + Stop clicking when switching to another window. + +
+
+ {onOffOptions.map((option) => ( + + ))} +
+
+ +
+
+ Extended Click Speed Limit + + Allow click speeds up to 1000 CPS (may affect performance). + +
+
+ {onOffOptions.map((option) => ( + + ))} +
+
+
+ + setPendingAction(null)} + /> + + ); +} diff --git a/src/components/panels/settings/sections/GeneralSection.tsx b/src/components/panels/settings/sections/GeneralSection.tsx new file mode 100644 index 00000000..d90ba415 --- /dev/null +++ b/src/components/panels/settings/sections/GeneralSection.tsx @@ -0,0 +1,292 @@ +import { useEffect, useState } from "react"; +import { invoke } from "@tauri-apps/api/core"; +import { openUrl } from "@tauri-apps/plugin-opener"; +import type { AppInfo } from "../../../../store"; +import { changelogEntries } from "../../../../changelog"; +import ChangelogContent from "../../../ChangelogContent"; +import ConfirmDialog from "../../../ConfirmDialog"; +import { SettingsCard } from "./shared"; + +interface CumulativeStats { + totalClicks: number; + totalTimeSecs: number; + totalSessions: number; + avgCpu: number; +} + +function formatTime(totalSeconds: number, language: string): string { + if (totalSeconds < 0.01) return "0s"; + if (totalSeconds < 60) { + return `${Math.floor(totalSeconds).toLocaleString(language)}s`; + } + if (totalSeconds < 3600) { + const m = Math.floor(totalSeconds / 60); + const s = Math.floor(totalSeconds % 60); + return s > 0 + ? `${m.toLocaleString(language)}m ${s.toLocaleString(language)}s` + : `${m.toLocaleString(language)}m`; + } + const h = Math.floor(totalSeconds / 3600); + const m = Math.floor((totalSeconds % 3600) / 60); + return m > 0 + ? `${h.toLocaleString(language)}h ${m.toLocaleString(language)}m` + : `${h.toLocaleString(language)}h`; +} + +function formatNumber(n: number, language: string): string { + return Math.floor(n).toLocaleString(language); +} + +function formatCpu( + cpu: number, + language: string, + notAvailable: string, +): string { + if (cpu < 0) return notAvailable; + return `${cpu.toLocaleString(language, { + minimumFractionDigits: 1, + maximumFractionDigits: 1, + })}%`; +} + +interface Props { + appInfo: AppInfo; + updateCheckStatus: + | "idle" + | "checking" + | "available" + | "unavailable" + | "error"; + onCheckForUpdate: () => void; +} + +export default function GeneralSection({ + appInfo, + updateCheckStatus, + onCheckForUpdate, +}: Props) { + const [stats, setStats] = useState(null); + const [showChangelog, setShowChangelog] = useState(false); + const [pendingAction, setPendingAction] = useState<"clear-stats" | null>( + null, + ); + const [resettingStats, setResettingStats] = useState(false); + const language = "en"; + + useEffect(() => { + invoke("get_stats") + .then(setStats) + .catch(() => {}); + }, []); + + const hasStats = stats !== null && stats.totalSessions > 0; + + const handleConfirmClearStats = async () => { + setResettingStats(true); + try { + const next = await invoke("reset_stats"); + setStats(next); + } catch { + // stats unchanged on failure + } finally { + setResettingStats(false); + setPendingAction(null); + } + }; + + const updateButtonLabel = { + idle: "Check for Update", + checking: "Checking...", + available: "Update found!", + unavailable: "No update found", + error: "Check failed", + }[updateCheckStatus]; + + return ( + <> + + +
+
+ Version + v{appInfo.version} +
+
+ + +
+
+ {showChangelog && } +
+ + +
+
+ Usage Data + + Statistics are stored locally and never sent anywhere. + +
+
+ {hasStats ? ( +
+
+ Total Clicks + + {formatNumber(stats.totalClicks, language)} + +
+
+ Total Time Clicking + + {formatTime(stats.totalTimeSecs, language)} + +
+
+ Average CPU + + {formatCpu(stats.avgCpu, language, "N/A")} + +
+
+ Sessions + + {formatNumber(stats.totalSessions, language)} + +
+
+ ) : ( +
No session data yet.
+ )} + {hasStats && ( +
+
+ Clear Stats + Clear all usage data. +
+ +
+ )} +
+ + setPendingAction(null)} + /> + + ); +} diff --git a/src/components/panels/settings/sections/MaintenanceSection.tsx b/src/components/panels/settings/sections/MaintenanceSection.tsx new file mode 100644 index 00000000..7ec22734 --- /dev/null +++ b/src/components/panels/settings/sections/MaintenanceSection.tsx @@ -0,0 +1,123 @@ +import { useState } from "react"; +import { invoke } from "@tauri-apps/api/core"; +import { error } from "@tauri-apps/plugin-log"; +import ConfirmDialog from "../../../ConfirmDialog"; +import { SettingsCard } from "./shared"; + +interface Props { + onReset: () => Promise; +} + +export default function MaintenanceSection({ onReset }: Props) { + const [pendingAction, setPendingAction] = useState<"reset-settings" | null>( + null, + ); + const [resetting, setResetting] = useState(false); + const [diagnosticsStatus, setDiagnosticsStatus] = useState( + null, + ); + const [exporting, setExporting] = useState(false); + + const handleConfirmResetSettings = async () => { + setResetting(true); + try { + await onReset(); + } finally { + setResetting(false); + setPendingAction(null); + } + }; + + return ( + <> + +
+
+ Reset All + + Reset all settings to their defaults. + +
+ +
+
+ + +
+
+ Diagnostics + + View or export your diagnostics. + +
+
+ + +
+
+ {diagnosticsStatus && ( + {diagnosticsStatus} + )} +
+ + setPendingAction(null)} + /> + + ); +} diff --git a/src/components/panels/settings/sections/PresetsSection.tsx b/src/components/panels/settings/sections/PresetsSection.tsx new file mode 100644 index 00000000..caecaad2 --- /dev/null +++ b/src/components/panels/settings/sections/PresetsSection.tsx @@ -0,0 +1,338 @@ +import { useEffect, useRef, useState } from "react"; +import { + MAX_PRESETS, + PRESET_NAME_MAX_LENGTH, +} from "../../../../settingsSchema"; +import type { PresetDefinition, PresetId, Settings } from "../../../../store"; +import { SettingsCard } from "./shared"; + +function PresetRow({ + preset, + isActive, + isEditing, + isConfirmingDelete, + running, + renameDraft, + onRenameDraftChange, + onStartRename, + onCancelRename, + onCommitRename, + onApply, + onUpdatePreset, + onRequestDelete, + onCancelDelete, + onConfirmDelete, +}: { + preset: PresetDefinition; + isActive: boolean; + isEditing: boolean; + isConfirmingDelete: boolean; + running: boolean; + renameDraft: string; + onRenameDraftChange: (value: string) => void; + onStartRename: () => void; + onCancelRename: () => void; + onCommitRename: () => void; + onApply: () => void; + onUpdatePreset: () => void; + onRequestDelete: () => void; + onCancelDelete: () => void; + onConfirmDelete: () => void; +}) { + return ( +
+
+
+ {isEditing ? ( + onRenameDraftChange(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + onCommitRename(); + } + if (event.key === "Escape") { + event.preventDefault(); + onCancelRename(); + } + }} + autoFocus + /> + ) : ( + {preset.name} + )} +
+ {isActive && ( + Active + )} + + {new Date(preset.updatedAt).toLocaleDateString()} + +
+
+
+ {isEditing ? ( + <> + + + + ) : isConfirmingDelete ? ( + <> + + + + ) : ( + <> + + + + + + )} +
+
+
+ ); +} + +interface Props { + settings: Settings; + running: boolean; + onSavePreset: (name: string) => boolean; + onApplyPreset: (presetId: PresetId) => boolean; + onUpdatePreset: (presetId: PresetId) => boolean; + onRenamePreset: (presetId: PresetId, name: string) => boolean; + onDeletePreset: (presetId: PresetId) => boolean; +} + +export default function PresetsSection({ + settings, + running, + onSavePreset, + onApplyPreset, + onUpdatePreset, + onRenamePreset, + onDeletePreset, +}: Props) { + const [newPresetName, setNewPresetName] = useState(""); + const [editingPresetId, setEditingPresetId] = useState(null); + const [renameDraft, setRenameDraft] = useState(""); + const [confirmingDeleteId, setConfirmingDeleteId] = useState( + null, + ); + const [presetsAtBottom, setPresetsAtBottom] = useState(true); + const presetsListRef = useRef(null); + + const presetLimitReached = settings.presets.length >= MAX_PRESETS; + const activeEditingPresetId = running ? null : editingPresetId; + const activeConfirmingDeleteId = running ? null : confirmingDeleteId; + + useEffect(() => { + if (!confirmingDeleteId) return; + + const handlePointerDown = (event: PointerEvent) => { + const target = event.target; + if (!(target instanceof Element)) return; + const presetCard = target.closest("[data-preset-id]"); + if (presetCard?.getAttribute("data-preset-id") === confirmingDeleteId) + return; + setConfirmingDeleteId(null); + }; + + document.addEventListener("pointerdown", handlePointerDown); + return () => { + document.removeEventListener("pointerdown", handlePointerDown); + }; + }, [confirmingDeleteId]); + + const handlePresetsScroll = () => { + const list = presetsListRef.current; + if (!list) return; + setPresetsAtBottom( + list.scrollTop + list.clientHeight >= list.scrollHeight - 2, + ); + }; + + useEffect(() => { + handlePresetsScroll(); + }, [settings.presets.length]); + + const handleSavePreset = () => { + if (onSavePreset(newPresetName)) { + setNewPresetName(""); + setConfirmingDeleteId(null); + } + }; + + const handleStartRename = (preset: PresetDefinition) => { + setConfirmingDeleteId(null); + setEditingPresetId(preset.id); + setRenameDraft(preset.name); + }; + + const handleCommitRename = () => { + if (!editingPresetId) return; + if (onRenamePreset(editingPresetId, renameDraft)) { + setEditingPresetId(null); + setRenameDraft(""); + } + }; + + const handleCancelRename = () => { + setEditingPresetId(null); + setRenameDraft(""); + }; + + const handleRequestDelete = (presetId: PresetId) => { + setEditingPresetId(null); + setRenameDraft(""); + setConfirmingDeleteId(presetId); + }; + + const handleConfirmDelete = (presetId: PresetId) => { + if (onDeletePreset(presetId)) { + setConfirmingDeleteId(null); + } + }; + + return ( + +
+
+ Presets + + Save and restore to quickly switch configurations. + +
+
+ setNewPresetName(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter") { + event.preventDefault(); + if (!running && !presetLimitReached && newPresetName.trim()) { + handleSavePreset(); + } + } + if (event.key === "Escape") { + event.preventDefault(); + setNewPresetName(""); + } + }} + disabled={running} + /> + +
+ {presetLimitReached && ( + Max 6 presets allowed + )} + {running && ( + Disabled while clicking + )} + {settings.presets.length > 0 ? ( +
+
+ {settings.presets.map((preset) => ( + handleStartRename(preset)} + onCancelRename={handleCancelRename} + onCommitRename={handleCommitRename} + onApply={() => { + setConfirmingDeleteId(null); + onApplyPreset(preset.id); + }} + onUpdatePreset={() => { + setConfirmingDeleteId(null); + onUpdatePreset(preset.id); + }} + onRequestDelete={() => handleRequestDelete(preset.id)} + onCancelDelete={() => setConfirmingDeleteId(null)} + onConfirmDelete={() => handleConfirmDelete(preset.id)} + /> + ))} +
+
+
+ ) : ( +
No saved presets.
+ )} +
+ + ); +} diff --git a/src/components/panels/settings/sections/StartupSection.tsx b/src/components/panels/settings/sections/StartupSection.tsx new file mode 100644 index 00000000..c564deb4 --- /dev/null +++ b/src/components/panels/settings/sections/StartupSection.tsx @@ -0,0 +1,83 @@ +import { useEffect, useState } from "react"; +import { invoke } from "@tauri-apps/api/core"; +import { error } from "@tauri-apps/plugin-log"; +import type { Settings } from "../../../../store"; +import { SettingsCard } from "./shared"; + +const onOffOptions = [ + { value: false, label: "Off" }, + { value: true, label: "On" }, +]; + +interface Props { + settings: Settings; + update: (patch: Partial) => void; +} + +export default function StartupSection({ settings, update }: Props) { + const [autostartEnabled, setAutostartEnabled] = useState( + null, + ); + + useEffect(() => { + invoke("get_autostart_enabled") + .then(setAutostartEnabled) + .catch(() => setAutostartEnabled(false)); + }, []); + + return ( + +
+
+ Minimize to Tray + + Minimize to the system tray instead of the taskbar. + +
+
+ {onOffOptions.map((option) => ( + + ))} +
+
+ +
+
+ Run on Startup + + Start clicking when the app opens. + +
+
+ {onOffOptions.map((option) => ( + + ))} +
+
+
+ ); +} diff --git a/src/components/panels/settings/sections/shared.tsx b/src/components/panels/settings/sections/shared.tsx new file mode 100644 index 00000000..dee95af6 --- /dev/null +++ b/src/components/panels/settings/sections/shared.tsx @@ -0,0 +1,37 @@ +import type { ReactNode } from "react"; + +function SettingsSectionHeading({ + title, + description, +}: { + title: string; + description?: string; +}) { + return ( +
+ {title} + {description ? ( + {description} + ) : null} +
+ ); +} + +function SettingsCard({ + title, + description, + children, +}: { + title: string; + description?: string; + children: ReactNode; +}) { + return ( +
+ +
{children}
+
+ ); +} + +export { SettingsSectionHeading, SettingsCard }; From 9cb0b32506959486ab9838260abc265425115c71 Mon Sep 17 00:00:00 2001 From: Blur Date: Thu, 2 Jul 2026 21:43:39 +0200 Subject: [PATCH 02/45] fix: settings panel formatting --- src/components/panels/settings/SettingsPanel.css | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/components/panels/settings/SettingsPanel.css b/src/components/panels/settings/SettingsPanel.css index 791f250e..04f21c6f 100644 --- a/src/components/panels/settings/SettingsPanel.css +++ b/src/components/panels/settings/SettingsPanel.css @@ -69,8 +69,16 @@ height: var(--radius-lg); background: var(--bg-surface); pointer-events: none; - -webkit-mask-image: radial-gradient(circle at var(--radius-lg) var(--radius-lg), transparent var(--radius-lg), black var(--radius-lg)); - mask-image: radial-gradient(circle at var(--radius-lg) var(--radius-lg), transparent var(--radius-lg), black var(--radius-lg)); + -webkit-mask-image: radial-gradient( + circle at var(--radius-lg) var(--radius-lg), + transparent var(--radius-lg), + black var(--radius-lg) + ); + mask-image: radial-gradient( + circle at var(--radius-lg) var(--radius-lg), + transparent var(--radius-lg), + black var(--radius-lg) + ); } [data-theme="light"] .settings-panel { From 5047315c539f3cfae801a42608fe48769704cb8c Mon Sep 17 00:00:00 2001 From: Blur Date: Thu, 2 Jul 2026 21:52:07 +0200 Subject: [PATCH 03/45] move advanced panel sections into dedicated sections folder --- src/components/CadenceInput.tsx | 2 +- src/components/panels/SimplePanel.tsx | 2 +- src/components/panels/advanced/AdvancedPanel.tsx | 12 ++++++------ .../advanced/{ => sections}/CadenceSection.tsx | 14 +++++++------- .../advanced/{ => sections}/DoubleClickSection.tsx | 4 ++-- .../advanced/{ => sections}/DutyCycleSection.tsx | 4 ++-- .../advanced/{ => sections}/LimitsSection.tsx | 4 ++-- .../advanced/{ => sections}/SequenceSection.tsx | 4 ++-- .../{ => sections}/SpeedVariationSection.tsx | 4 ++-- .../panels/advanced/{ => sections}/shared.tsx | 4 ++-- .../panels/zones/CustomStopZoneSection.tsx | 2 +- src/components/panels/zones/FailsafeSection.tsx | 2 +- src/components/panels/zones/ProcessListSection.tsx | 2 +- 13 files changed, 30 insertions(+), 30 deletions(-) rename src/components/panels/advanced/{ => sections}/CadenceSection.tsx (94%) rename src/components/panels/advanced/{ => sections}/DoubleClickSection.tsx (96%) rename src/components/panels/advanced/{ => sections}/DutyCycleSection.tsx (91%) rename src/components/panels/advanced/{ => sections}/LimitsSection.tsx (98%) rename src/components/panels/advanced/{ => sections}/SequenceSection.tsx (99%) rename src/components/panels/advanced/{ => sections}/SpeedVariationSection.tsx (93%) rename src/components/panels/advanced/{ => sections}/shared.tsx (99%) diff --git a/src/components/CadenceInput.tsx b/src/components/CadenceInput.tsx index 6b948c73..f55ffec1 100644 --- a/src/components/CadenceInput.tsx +++ b/src/components/CadenceInput.tsx @@ -6,7 +6,7 @@ import { convertDurationToRate, convertRateToDuration } from "../cadence"; import { normalizeIntegerRaw } from "../numberInput"; import type { RateInputMode, Settings } from "../store"; -import { AdvDropdown } from "./panels/advanced/shared"; +import { AdvDropdown } from "./panels/advanced/sections/shared"; import { getMaxClickSpeed, type ClickInterval } from "../settingsSchema"; // TODO: This should really be split up into what is in the advanced panel and what is in the simple panel. Having both in one feels kinda off i feel like. diff --git a/src/components/panels/SimplePanel.tsx b/src/components/panels/SimplePanel.tsx index e6e10e04..10234329 100644 --- a/src/components/panels/SimplePanel.tsx +++ b/src/components/panels/SimplePanel.tsx @@ -16,7 +16,7 @@ import { import { isAlphabeticKeyboardKey } from "../../keyboardKeyCase"; import { conflictsWithAutoPressKey } from "../../hotkeys"; import KeyCaptureInput from "../KeyCaptureInput"; -import { AdvDropdown } from "./advanced/shared"; +import { AdvDropdown } from "./advanced/sections/shared"; import "./SimplePanel.css"; interface SimplePanelProps { diff --git a/src/components/panels/advanced/AdvancedPanel.tsx b/src/components/panels/advanced/AdvancedPanel.tsx index 061bba8e..f11e5358 100644 --- a/src/components/panels/advanced/AdvancedPanel.tsx +++ b/src/components/panels/advanced/AdvancedPanel.tsx @@ -1,11 +1,11 @@ import "./AdvancedPanel.css"; import type { Settings } from "../../../store"; -import CadenceSection from "./CadenceSection"; -import DutyCycleSection from "./DutyCycleSection"; -import SpeedVariationSection from "./SpeedVariationSection"; -import DoubleClickSection from "./DoubleClickSection"; -import SequenceSection from "./SequenceSection"; -import LimitsSection from "./LimitsSection"; +import CadenceSection from "./sections/CadenceSection"; +import DutyCycleSection from "./sections/DutyCycleSection"; +import SpeedVariationSection from "./sections/SpeedVariationSection"; +import DoubleClickSection from "./sections/DoubleClickSection"; +import SequenceSection from "./sections/SequenceSection"; +import LimitsSection from "./sections/LimitsSection"; interface Props { settings: Settings; diff --git a/src/components/panels/advanced/CadenceSection.tsx b/src/components/panels/advanced/sections/CadenceSection.tsx similarity index 94% rename from src/components/panels/advanced/CadenceSection.tsx rename to src/components/panels/advanced/sections/CadenceSection.tsx index 22da5cd7..012ad61b 100644 --- a/src/components/panels/advanced/CadenceSection.tsx +++ b/src/components/panels/advanced/sections/CadenceSection.tsx @@ -1,11 +1,11 @@ -import type { MouseButton, Settings } from "../../../store"; +import type { MouseButton, Settings } from "../../../../store"; -import { MOUSE_BUTTON_OPTIONS } from "../../../settingsSchema"; -import { isAlphabeticKeyboardKey } from "../../../keyboardKeyCase"; -import { conflictsWithAutoPressKey } from "../../../hotkeys"; -import CadenceInput from "../../CadenceInput"; -import HotkeyCaptureInput from "../../HotkeyCaptureInput"; -import KeyCaptureInput from "../../KeyCaptureInput"; +import { MOUSE_BUTTON_OPTIONS } from "../../../../settingsSchema"; +import { isAlphabeticKeyboardKey } from "../../../../keyboardKeyCase"; +import { conflictsWithAutoPressKey } from "../../../../hotkeys"; +import CadenceInput from "../../../CadenceInput"; +import HotkeyCaptureInput from "../../../HotkeyCaptureInput"; +import KeyCaptureInput from "../../../KeyCaptureInput"; import { CardDivider, InfoIcon } from "./shared"; function MouseTargetIcon() { diff --git a/src/components/panels/advanced/DoubleClickSection.tsx b/src/components/panels/advanced/sections/DoubleClickSection.tsx similarity index 96% rename from src/components/panels/advanced/DoubleClickSection.tsx rename to src/components/panels/advanced/sections/DoubleClickSection.tsx index 7e5ce5b7..d941811e 100644 --- a/src/components/panels/advanced/DoubleClickSection.tsx +++ b/src/components/panels/advanced/sections/DoubleClickSection.tsx @@ -1,9 +1,9 @@ -import type { Settings } from "../../../store"; +import type { Settings } from "../../../../store"; import { getEffectiveClicksPerSecond, isDoubleClickSupported, -} from "../../../cadence"; +} from "../../../../cadence"; import { InfoIcon, ToggleBtn } from "./shared"; interface Props { diff --git a/src/components/panels/advanced/DutyCycleSection.tsx b/src/components/panels/advanced/sections/DutyCycleSection.tsx similarity index 91% rename from src/components/panels/advanced/DutyCycleSection.tsx rename to src/components/panels/advanced/sections/DutyCycleSection.tsx index 3874c7b5..f7f671c5 100644 --- a/src/components/panels/advanced/DutyCycleSection.tsx +++ b/src/components/panels/advanced/sections/DutyCycleSection.tsx @@ -1,6 +1,6 @@ -import type { Settings } from "../../../store"; +import type { Settings } from "../../../../store"; -import { SETTINGS_LIMITS } from "../../../settingsSchema"; +import { SETTINGS_LIMITS } from "../../../../settingsSchema"; import { InfoIcon, NumInput } from "./shared"; interface Props { diff --git a/src/components/panels/advanced/LimitsSection.tsx b/src/components/panels/advanced/sections/LimitsSection.tsx similarity index 98% rename from src/components/panels/advanced/LimitsSection.tsx rename to src/components/panels/advanced/sections/LimitsSection.tsx index 83704436..50814c9f 100644 --- a/src/components/panels/advanced/LimitsSection.tsx +++ b/src/components/panels/advanced/sections/LimitsSection.tsx @@ -1,10 +1,10 @@ import { useEffect, useLayoutEffect, useRef, useState } from "react"; -import type { Settings, TimeLimitUnit } from "../../../store"; +import type { Settings, TimeLimitUnit } from "../../../../store"; import { SETTINGS_LIMITS, TIME_LIMIT_UNIT_OPTIONS, -} from "../../../settingsSchema"; +} from "../../../../settingsSchema"; import { Disableable, NumInput, ToggleBtn, InfoIcon } from "./shared"; interface Props { diff --git a/src/components/panels/advanced/SequenceSection.tsx b/src/components/panels/advanced/sections/SequenceSection.tsx similarity index 99% rename from src/components/panels/advanced/SequenceSection.tsx rename to src/components/panels/advanced/sections/SequenceSection.tsx index 9c29f907..d57a3b39 100644 --- a/src/components/panels/advanced/SequenceSection.tsx +++ b/src/components/panels/advanced/sections/SequenceSection.tsx @@ -6,8 +6,8 @@ import { useState, } from "react"; import { error } from "@tauri-apps/plugin-log"; -import { getEffectiveIntervalMs } from "../../../cadence"; -import type { SequencePoint, Settings } from "../../../store"; +import { getEffectiveIntervalMs } from "../../../../cadence"; +import type { SequencePoint, Settings } from "../../../../store"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; diff --git a/src/components/panels/advanced/SpeedVariationSection.tsx b/src/components/panels/advanced/sections/SpeedVariationSection.tsx similarity index 93% rename from src/components/panels/advanced/SpeedVariationSection.tsx rename to src/components/panels/advanced/sections/SpeedVariationSection.tsx index 77b2e5a2..13fbc41e 100644 --- a/src/components/panels/advanced/SpeedVariationSection.tsx +++ b/src/components/panels/advanced/sections/SpeedVariationSection.tsx @@ -1,6 +1,6 @@ -import type { Settings } from "../../../store"; +import type { Settings } from "../../../../store"; -import { SETTINGS_LIMITS } from "../../../settingsSchema"; +import { SETTINGS_LIMITS } from "../../../../settingsSchema"; import { Disableable, InfoIcon, NumInput, ToggleBtn } from "./shared"; interface Props { diff --git a/src/components/panels/advanced/shared.tsx b/src/components/panels/advanced/sections/shared.tsx similarity index 99% rename from src/components/panels/advanced/shared.tsx rename to src/components/panels/advanced/sections/shared.tsx index 8029f551..81af6be1 100644 --- a/src/components/panels/advanced/shared.tsx +++ b/src/components/panels/advanced/sections/shared.tsx @@ -13,8 +13,8 @@ import { } from "react"; import { createPortal } from "react-dom"; -import { normalizeIntegerRaw } from "../../../numberInput"; -import UnavailableReason from "../../UnavailableReason"; +import { normalizeIntegerRaw } from "../../../../numberInput"; +import UnavailableReason from "../../../UnavailableReason"; // ToggleBtn ← These are here just for some visual space diff --git a/src/components/panels/zones/CustomStopZoneSection.tsx b/src/components/panels/zones/CustomStopZoneSection.tsx index 07ce04ba..6686ee0d 100644 --- a/src/components/panels/zones/CustomStopZoneSection.tsx +++ b/src/components/panels/zones/CustomStopZoneSection.tsx @@ -9,7 +9,7 @@ import { ToggleBtn, CardDivider, InfoIcon, -} from "../advanced/shared"; +} from "../advanced/sections/shared"; interface Props { settings: Settings; diff --git a/src/components/panels/zones/FailsafeSection.tsx b/src/components/panels/zones/FailsafeSection.tsx index 68ca1df3..4a6a5940 100644 --- a/src/components/panels/zones/FailsafeSection.tsx +++ b/src/components/panels/zones/FailsafeSection.tsx @@ -7,7 +7,7 @@ import { ToggleBtn, CardDivider, InfoIcon, -} from "../advanced/shared"; +} from "../advanced/sections/shared"; interface Props { settings: Settings; diff --git a/src/components/panels/zones/ProcessListSection.tsx b/src/components/panels/zones/ProcessListSection.tsx index c2dcf044..a7f35459 100644 --- a/src/components/panels/zones/ProcessListSection.tsx +++ b/src/components/panels/zones/ProcessListSection.tsx @@ -11,7 +11,7 @@ import { ToggleBtn, CardDivider, InfoIcon, -} from "../advanced/shared"; +} from "../advanced/sections/shared"; interface ProcessInfo { name: string; From e807071799a8404911a2ffa944f13add02e441dc Mon Sep 17 00:00:00 2001 From: Blur Date: Thu, 2 Jul 2026 21:59:30 +0200 Subject: [PATCH 04/45] move zones panel sections into dedicated sections folder --- .../panels/advanced/AdvancedPanel.css | 408 +---------------- src/components/panels/zones/ZonesPanel.css | 419 ++++++++++++++++++ src/components/panels/zones/ZonesPanel.tsx | 7 +- .../{ => sections}/CustomStopZoneSection.tsx | 4 +- .../zones/{ => sections}/FailsafeSection.tsx | 6 +- .../{ => sections}/ProcessListSection.tsx | 6 +- 6 files changed, 435 insertions(+), 415 deletions(-) create mode 100644 src/components/panels/zones/ZonesPanel.css rename src/components/panels/zones/{ => sections}/CustomStopZoneSection.tsx (98%) rename src/components/panels/zones/{ => sections}/FailsafeSection.tsx (96%) rename src/components/panels/zones/{ => sections}/ProcessListSection.tsx (98%) diff --git a/src/components/panels/advanced/AdvancedPanel.css b/src/components/panels/advanced/AdvancedPanel.css index bd077413..68f0d65e 100644 --- a/src/components/panels/advanced/AdvancedPanel.css +++ b/src/components/panels/advanced/AdvancedPanel.css @@ -256,42 +256,6 @@ input[type="number"]::-webkit-outer-spin-button { align-items: stretch; } -.adv-zones-row { - display: flex; - gap: 0.625rem; - flex: none; - align-items: stretch; - min-height: 0; -} - -.adv-zones-left { - flex: 1; - display: flex; - flex-direction: column; - gap: 0.625rem; - min-width: 0; -} - -.adv-zones-right { - flex: 1; - display: flex; - flex-direction: column; - min-width: 0; -} - -.adv-zones-left .adv-sectioncontainer { - flex: none; -} - -.adv-zones-right .adv-sectioncontainer { - flex: 1; - min-height: 0; -} - -.adv-panel--zones { - overflow: hidden; -} - .adv-target-row { min-width: 0; } @@ -495,114 +459,6 @@ input[type="number"]::-webkit-outer-spin-button { color: #a33c3c; } -/* Corner/Edge Trigger grid */ -.adv-corner-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 0.375rem; - flex-shrink: 0; -} - -.adv-corner-box { - display: flex; - align-items: center; - justify-content: center; - gap: 2px; - background: var(--bg-elevated); - border: 1px solid var(--border-subtle); - border-radius: 0.375rem; - padding: 0.25rem 0.5rem; - position: relative; - overflow: hidden; -} - -.adv-stop-boundary-box { - height: 30px; - box-sizing: border-box; - padding-top: 0; - padding-bottom: 0; -} - -.adv-stop-boundary-box .adv-number-sm { - height: 28px; - line-height: 28px; - box-sizing: border-box; -} - -/* Quarter circle arcs for corner stop */ -.adv-arc { - position: absolute; - width: 0.625rem; - height: 0.625rem; - border: 2px solid var(--accent-green); - pointer-events: none; -} - -.adv-arc-tl { - top: 0px; - left: 0px; - border-right: none; - border-bottom: none; - border-top-left-radius: 0.375rem; -} - -.adv-arc-tr { - top: 0px; - right: 0px; - border-left: none; - border-bottom: none; - border-top-right-radius: 0.375rem; -} - -.adv-arc-bl { - bottom: 0px; - left: 0px; - border-right: none; - border-top: none; - border-bottom-left-radius: 0.375rem; -} - -.adv-arc-br { - bottom: 0px; - right: 0px; - border-left: none; - border-top: none; - border-bottom-right-radius: 0.375rem; -} - -/* Edge bars for edge stop */ -.adv-edge-bar { - position: absolute; - background: var(--accent-green); - border-radius: 2px; - pointer-events: none; -} - -.adv-edge-bar-top { - top: 0; - left: 0px; - right: 0px; - height: 2px; -} -.adv-edge-bar-bottom { - bottom: 0; - left: 0px; - right: 0px; - height: 2px; -} -.adv-edge-bar-left { - left: 0; - top: 0px; - bottom: 0px; - width: 2px; -} -.adv-edge-bar-right { - right: 0; - top: 0px; - bottom: 0px; - width: 2px; -} - .adv-pick-btn { font-family: "DMSans", sans-serif; font-size: 0.6875rem; @@ -694,8 +550,7 @@ input[type="number"]::-webkit-outer-spin-button { min-height: 0; } -.adv-sequence-body, -.adv-stop-zone-body { +.adv-sequence-body { display: flex; flex-direction: column; gap: 10px; @@ -704,8 +559,7 @@ input[type="number"]::-webkit-outer-spin-button { height: 100%; } -.adv-sequence-controls, -.adv-stop-zone-controls { +.adv-sequence-controls { display: flex; flex-direction: column; gap: 8px; @@ -956,29 +810,15 @@ input[type="number"]::-webkit-outer-spin-button { width: calc(6ch + 0.75rem); } -.adv-stop-zone-input { - height: 30px; - box-sizing: border-box; -} - -.adv-stop-zone-input .adv-number-sm { - height: 28px; - line-height: 28px; - box-sizing: border-box; -} - .adv-sequence-clicks { min-width: 0; } -.adv-sequence-actions, -.adv-stop-zone-actions { +.adv-sequence-actions { display: flex; gap: 6px; flex-wrap: wrap; -} - -.adv-sequence-actions { + margin-left: auto; margin-left: auto; } @@ -1014,12 +854,6 @@ input[type="number"]::-webkit-outer-spin-button { background: linear-gradient(to bottom, transparent 0%, var(--bg-base) 100%); } -.adv-stop-zone-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 6px; -} - /* Container for the disabled section */ .adv-disabled-container { position: relative; @@ -1067,13 +901,6 @@ input[type="number"]::-webkit-outer-spin-button { box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.2); } -.adv-disabled-content .adv-edge-bar, -.adv-disabled-content .adv-arc { - border-color: var(--border-subtle) !important; - background: var(--border-subtle) !important; - opacity: 0.3; -} - .adv-toggle-btn.adv-disabled { opacity: 0.5; cursor: not-allowed; @@ -1288,230 +1115,3 @@ input[type="number"]::-webkit-outer-spin-button { color: var(--text-primary); background: var(--bg-input); } - -.adv-process-list-section { - display: flex; - flex-direction: column; - min-height: 0; -} - -.adv-process-list-section .unavailable-reason--block { - display: flex; - flex-direction: column; - flex: 1; - min-height: 0; -} - -.adv-process-list-section .adv-disabled-container { - display: flex; - flex-direction: column; - min-height: 0; - flex: 1; -} - -.adv-process-list-section .adv-disabled-container > :first-child { - display: flex; - flex-direction: column; - min-height: 0; - flex: 1; -} - -.adv-whitelist-warning { - display: flex; - align-items: center; - gap: 0.5rem; - font-family: "DMSans", sans-serif; - font-size: 0.75rem; - color: var(--text-warning, #b8860b); - background: color-mix(in srgb, var(--text-warning, #b8860b) 10%, transparent); - border: 1px solid - color-mix(in srgb, var(--text-warning, #b8860b) 30%, transparent); - border-radius: var(--radius-sm); - padding: 0.5rem 0.625rem; - margin-bottom: 0.5rem; -} - -.adv-process-list { - flex: 1; - min-height: 0; - overflow-y: auto; - scrollbar-gutter: stable; - padding-right: 4px; -} - -.adv-process-list::-webkit-scrollbar { - width: 0.375rem; - background: var(--bg-elevated); - border-radius: 0.5rem; -} - -.adv-process-list::-webkit-scrollbar-thumb { - background: var(--text-dim); - border-radius: 0.25rem; -} - -.adv-process-list::-webkit-scrollbar-thumb:hover { - background: #777; -} - -.adv-process-list .adv-sequence-item { - display: flex; - align-items: center; - gap: 10px; - padding: 8px 10px; - cursor: pointer; - grid-template-columns: none; - transition: - border-color 0.15s ease, - background 0.15s ease; -} - -.adv-process-list .adv-sequence-item:hover { - border-color: var(--text-dim); - background: var(--bg-input); -} - -.adv-process-list .adv-sequence-item + .adv-sequence-item { - margin-top: 6px; -} - -.adv-process-list .adv-sequence-item::before { - display: none; -} - -.adv-proc-checkbox { - appearance: none; - -webkit-appearance: none; - width: 16px; - height: 16px; - border: 1px solid var(--border-subtle); - border-radius: 3px; - background: var(--bg-elevated); - cursor: pointer; - flex-shrink: 0; - transition: - border-color 0.15s ease, - background 0.15s ease; - position: relative; -} - -.adv-proc-checkbox:hover { - border-color: var(--text-dim); -} - -.adv-proc-checkbox:checked { - background: #3a8a5c; - border-color: #3a8a5c; -} - -.adv-proc-checkbox:checked::after { - content: "✓"; - position: absolute; - inset: 0; - display: flex; - align-items: center; - justify-content: center; - color: #fff; - font-size: 11px; - font-weight: 700; - line-height: 1; -} - -.adv-proc-name { - flex: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-family: "DMSans", sans-serif; - font-size: 0.8125rem; - font-weight: 700; - color: var(--text-primary); -} - -.adv-proc-exe { - font-family: "DMSans", sans-serif; - font-size: 0.6875rem; - color: var(--text-dim); - flex-shrink: 0; -} - -.adv-proc-icon { - width: 16px; - height: 16px; - flex-shrink: 0; -} - -.adv-proc-behavior-toggle { - display: flex; - background: var(--bg-elevated); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - overflow: hidden; - flex-shrink: 0; - margin-left: auto; -} - -.adv-proc-behavior-toggle .adv-toggle-btn { - font-size: 0.6875rem; - font-weight: 700; - letter-spacing: 3%; - text-transform: uppercase; - height: 1.625rem; - padding: 0 0.5rem; - min-width: 2rem; - text-align: center; - background: transparent; - border: none; - color: var(--text-dim); - cursor: pointer; - transition: - color 50ms ease, - background 50ms ease; -} - -.adv-proc-behavior-toggle .adv-toggle-btn:hover { - color: var(--text-muted); -} - -.adv-proc-behavior-toggle .adv-toggle-on.active { - background: #3a8a5c; - color: var(--text-primary); -} - -.adv-proc-behavior-toggle .adv-toggle-off.active { - background: var(--bg-input); - color: #a33c3c; -} - -.adv-proc-behavior-toggle.disabled { - opacity: 0.25; - pointer-events: none; -} - -.adv-proc-search { - font-family: "DMSans", sans-serif; - font-size: 0.75rem; - height: var(--adv-control-height); - padding: 0 1.5rem 0 0.5rem; - margin-left: auto; - width: 12rem; - background-color: var(--bg-tertiary); - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Ccircle cx='5' cy='5' r='3.5' fill='none' stroke='%23999' stroke-width='1.2'/%3E%3Cline x1='7.75' y1='7.75' x2='10.5' y2='10.5' stroke='%23999' stroke-width='1.2' stroke-linecap='round'/%3E%3C/svg%3E"); - background-repeat: no-repeat; - background-position: right 0.375rem center; - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - color: var(--text-main); - outline: none; - box-sizing: border-box; -} - -.adv-proc-search::placeholder { - color: var(--text-dim); - opacity: 0.7; -} - -.adv-proc-search:focus { - border-color: var(--accent); -} diff --git a/src/components/panels/zones/ZonesPanel.css b/src/components/panels/zones/ZonesPanel.css new file mode 100644 index 00000000..a222f06b --- /dev/null +++ b/src/components/panels/zones/ZonesPanel.css @@ -0,0 +1,419 @@ +.adv-zones-row { + display: flex; + gap: 0.625rem; + flex: none; + align-items: stretch; + min-height: 0; +} + +.adv-zones-left { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.625rem; + min-width: 0; +} + +.adv-zones-right { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; +} + +.adv-zones-left .adv-sectioncontainer { + flex: none; +} + +.adv-zones-right .adv-sectioncontainer { + flex: 1; + min-height: 0; +} + +.adv-panel--zones { + overflow: hidden; +} + +.adv-corner-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.375rem; + flex-shrink: 0; +} + +.adv-corner-box { + display: flex; + align-items: center; + justify-content: center; + gap: 2px; + background: var(--bg-elevated); + border: 1px solid var(--border-subtle); + border-radius: 0.375rem; + padding: 0.25rem 0.5rem; + position: relative; + overflow: hidden; +} + +.adv-stop-boundary-box { + height: 30px; + box-sizing: border-box; + padding-top: 0; + padding-bottom: 0; +} + +.adv-stop-boundary-box .adv-number-sm { + height: 28px; + line-height: 28px; + box-sizing: border-box; +} + +.adv-arc { + position: absolute; + width: 0.625rem; + height: 0.625rem; + border: 2px solid var(--accent-green); + pointer-events: none; +} + +.adv-arc-tl { + top: 0px; + left: 0px; + border-right: none; + border-bottom: none; + border-top-left-radius: 0.375rem; +} + +.adv-arc-tr { + top: 0px; + right: 0px; + border-left: none; + border-bottom: none; + border-top-right-radius: 0.375rem; +} + +.adv-arc-bl { + bottom: 0px; + left: 0px; + border-right: none; + border-top: none; + border-bottom-left-radius: 0.375rem; +} + +.adv-arc-br { + bottom: 0px; + right: 0px; + border-left: none; + border-top: none; + border-bottom-right-radius: 0.375rem; +} + +.adv-edge-bar { + position: absolute; + background: var(--accent-green); + border-radius: 2px; + pointer-events: none; +} + +.adv-edge-bar-top { + top: 0; + left: 0px; + right: 0px; + height: 2px; +} + +.adv-edge-bar-bottom { + bottom: 0; + left: 0px; + right: 0px; + height: 2px; +} + +.adv-edge-bar-left { + left: 0; + top: 0px; + bottom: 0px; + width: 2px; +} + +.adv-edge-bar-right { + right: 0; + top: 0px; + bottom: 0px; + width: 2px; +} + +.adv-stop-zone-body { + display: flex; + flex-direction: column; + gap: 10px; + align-items: stretch; + min-height: 0; + height: 100%; +} + +.adv-stop-zone-controls { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; + flex: 1; + width: 100%; + min-height: 0; +} + +.adv-stop-zone-input { + height: 30px; + box-sizing: border-box; +} + +.adv-stop-zone-input .adv-number-sm { + height: 28px; + line-height: 28px; + box-sizing: border-box; +} + +.adv-stop-zone-actions { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.adv-stop-zone-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 6px; +} + +.adv-disabled-content .adv-edge-bar, +.adv-disabled-content .adv-arc { + border-color: var(--border-subtle) !important; + background: var(--border-subtle) !important; + opacity: 0.3; +} + +.adv-process-list-section { + display: flex; + flex-direction: column; + min-height: 0; +} + +.adv-process-list-section .unavailable-reason--block { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; +} + +.adv-process-list-section .adv-disabled-container { + display: flex; + flex-direction: column; + min-height: 0; + flex: 1; +} + +.adv-process-list-section .adv-disabled-container > :first-child { + display: flex; + flex-direction: column; + min-height: 0; + flex: 1; +} + +.adv-whitelist-warning { + display: flex; + align-items: center; + gap: 0.5rem; + font-family: "DMSans", sans-serif; + font-size: 0.75rem; + color: var(--text-warning, #b8860b); + background: color-mix(in srgb, var(--text-warning, #b8860b) 10%, transparent); + border: 1px solid + color-mix(in srgb, var(--text-warning, #b8860b) 30%, transparent); + border-radius: var(--radius-sm); + padding: 0.5rem 0.625rem; + margin-bottom: 0.5rem; +} + +.adv-process-list { + flex: 1; + min-height: 0; + overflow-y: auto; + scrollbar-gutter: stable; + padding-right: 4px; +} + +.adv-process-list::-webkit-scrollbar { + width: 0.375rem; + background: var(--bg-elevated); + border-radius: 0.5rem; +} + +.adv-process-list::-webkit-scrollbar-thumb { + background: var(--text-dim); + border-radius: 0.25rem; +} + +.adv-process-list::-webkit-scrollbar-thumb:hover { + background: #777; +} + +.adv-process-list .adv-sequence-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 10px; + cursor: pointer; + grid-template-columns: none; + transition: + border-color 0.15s ease, + background 0.15s ease; +} + +.adv-process-list .adv-sequence-item:hover { + border-color: var(--text-dim); + background: var(--bg-input); +} + +.adv-process-list .adv-sequence-item + .adv-sequence-item { + margin-top: 6px; +} + +.adv-process-list .adv-sequence-item::before { + display: none; +} + +.adv-proc-checkbox { + appearance: none; + -webkit-appearance: none; + width: 16px; + height: 16px; + border: 1px solid var(--border-subtle); + border-radius: 3px; + background: var(--bg-elevated); + cursor: pointer; + flex-shrink: 0; + transition: + border-color 0.15s ease, + background 0.15s ease; + position: relative; +} + +.adv-proc-checkbox:hover { + border-color: var(--text-dim); +} + +.adv-proc-checkbox:checked { + background: #3a8a5c; + border-color: #3a8a5c; +} + +.adv-proc-checkbox:checked::after { + content: "✓"; + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + font-size: 11px; + font-weight: 700; + line-height: 1; +} + +.adv-proc-name { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-family: "DMSans", sans-serif; + font-size: 0.8125rem; + font-weight: 700; + color: var(--text-primary); +} + +.adv-proc-exe { + font-family: "DMSans", sans-serif; + font-size: 0.6875rem; + color: var(--text-dim); + flex-shrink: 0; +} + +.adv-proc-icon { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +.adv-proc-behavior-toggle { + display: flex; + background: var(--bg-elevated); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + overflow: hidden; + flex-shrink: 0; + margin-left: auto; +} + +.adv-proc-behavior-toggle .adv-toggle-btn { + font-size: 0.6875rem; + font-weight: 700; + letter-spacing: 3%; + text-transform: uppercase; + height: 1.625rem; + padding: 0 0.5rem; + min-width: 2rem; + text-align: center; + background: transparent; + border: none; + color: var(--text-dim); + cursor: pointer; + transition: + color 50ms ease, + background 50ms ease; +} + +.adv-proc-behavior-toggle .adv-toggle-btn:hover { + color: var(--text-muted); +} + +.adv-proc-behavior-toggle .adv-toggle-on.active { + background: #3a8a5c; + color: var(--text-primary); +} + +.adv-proc-behavior-toggle .adv-toggle-off.active { + background: var(--bg-input); + color: #a33c3c; +} + +.adv-proc-behavior-toggle.disabled { + opacity: 0.25; + pointer-events: none; +} + +.adv-proc-search { + font-family: "DMSans", sans-serif; + font-size: 0.75rem; + height: var(--adv-control-height); + padding: 0 1.5rem 0 0.5rem; + margin-left: auto; + width: 12rem; + background-color: var(--bg-tertiary); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Ccircle cx='5' cy='5' r='3.5' fill='none' stroke='%23999' stroke-width='1.2'/%3E%3Cline x1='7.75' y1='7.75' x2='10.5' y2='10.5' stroke='%23999' stroke-width='1.2' stroke-linecap='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 0.375rem center; + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + color: var(--text-main); + outline: none; + box-sizing: border-box; +} + +.adv-proc-search::placeholder { + color: var(--text-dim); + opacity: 0.7; +} + +.adv-proc-search:focus { + border-color: var(--accent); +} diff --git a/src/components/panels/zones/ZonesPanel.tsx b/src/components/panels/zones/ZonesPanel.tsx index f749359a..f464ba74 100644 --- a/src/components/panels/zones/ZonesPanel.tsx +++ b/src/components/panels/zones/ZonesPanel.tsx @@ -1,8 +1,9 @@ import "../advanced/AdvancedPanel.css"; +import "./ZonesPanel.css"; import type { Settings } from "../../../store"; -import FailsafeSection from "./FailsafeSection"; -import CustomStopZoneSection from "./CustomStopZoneSection"; -import ProcessListSection from "./ProcessListSection"; +import FailsafeSection from "./sections/FailsafeSection"; +import CustomStopZoneSection from "./sections/CustomStopZoneSection"; +import ProcessListSection from "./sections/ProcessListSection"; // TODO: This still entirely needs to get done. I've just moved everything over here. If you are a back end dev, please be careful changing UI things xD // TODO: Custom Stop zones should be like Sequence clicking where you can add as many as you want in a list. diff --git a/src/components/panels/zones/CustomStopZoneSection.tsx b/src/components/panels/zones/sections/CustomStopZoneSection.tsx similarity index 98% rename from src/components/panels/zones/CustomStopZoneSection.tsx rename to src/components/panels/zones/sections/CustomStopZoneSection.tsx index 6686ee0d..53a483b4 100644 --- a/src/components/panels/zones/CustomStopZoneSection.tsx +++ b/src/components/panels/zones/sections/CustomStopZoneSection.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import type { Settings } from "../../../store"; +import type { Settings } from "../../../../store"; import { error } from "@tauri-apps/plugin-log"; import { invoke } from "@tauri-apps/api/core"; import { listen } from "@tauri-apps/api/event"; @@ -9,7 +9,7 @@ import { ToggleBtn, CardDivider, InfoIcon, -} from "../advanced/sections/shared"; +} from "../../advanced/sections/shared"; interface Props { settings: Settings; diff --git a/src/components/panels/zones/FailsafeSection.tsx b/src/components/panels/zones/sections/FailsafeSection.tsx similarity index 96% rename from src/components/panels/zones/FailsafeSection.tsx rename to src/components/panels/zones/sections/FailsafeSection.tsx index 4a6a5940..32c20f53 100644 --- a/src/components/panels/zones/FailsafeSection.tsx +++ b/src/components/panels/zones/sections/FailsafeSection.tsx @@ -1,13 +1,13 @@ -import type { Settings } from "../../../store"; +import type { Settings } from "../../../../store"; -import { SETTINGS_LIMITS } from "../../../settingsSchema"; +import { SETTINGS_LIMITS } from "../../../../settingsSchema"; import { Disableable, NumInput, ToggleBtn, CardDivider, InfoIcon, -} from "../advanced/sections/shared"; +} from "../../advanced/sections/shared"; interface Props { settings: Settings; diff --git a/src/components/panels/zones/ProcessListSection.tsx b/src/components/panels/zones/sections/ProcessListSection.tsx similarity index 98% rename from src/components/panels/zones/ProcessListSection.tsx rename to src/components/panels/zones/sections/ProcessListSection.tsx index a7f35459..0ecb5337 100644 --- a/src/components/panels/zones/ProcessListSection.tsx +++ b/src/components/panels/zones/sections/ProcessListSection.tsx @@ -1,17 +1,17 @@ import { useCallback, useEffect, useRef, useState } from "react"; import { invoke } from "@tauri-apps/api/core"; -import type { Settings } from "../../../store"; +import type { Settings } from "../../../../store"; import type { ProcessListBehavior, ProcessListEntry, -} from "../../../settingsSchema"; +} from "../../../../settingsSchema"; import { Disableable, ToggleBtn, CardDivider, InfoIcon, -} from "../advanced/sections/shared"; +} from "../../advanced/sections/shared"; interface ProcessInfo { name: string; From 64e0c58b92f052034558613a8082aeae64f79c7d Mon Sep 17 00:00:00 2001 From: Blur Date: Thu, 2 Jul 2026 22:21:55 +0200 Subject: [PATCH 05/45] refactor: split CadenceInput into simple/advanced sub-components --- src/components/CadenceInput.tsx | 510 +----------------- src/components/panels/SimpleCadenceInput.tsx | 218 ++++++++ .../sections/AdvancedCadenceInput.tsx | 233 ++++++++ .../advanced/sections/CadenceSection.tsx | 7 +- src/components/sharedCadence.ts | 94 ++++ 5 files changed, 552 insertions(+), 510 deletions(-) create mode 100644 src/components/panels/SimpleCadenceInput.tsx create mode 100644 src/components/panels/advanced/sections/AdvancedCadenceInput.tsx create mode 100644 src/components/sharedCadence.ts diff --git a/src/components/CadenceInput.tsx b/src/components/CadenceInput.tsx index f55ffec1..da9a0696 100644 --- a/src/components/CadenceInput.tsx +++ b/src/components/CadenceInput.tsx @@ -1,514 +1,16 @@ -import type { ChangeEvent, CSSProperties, FocusEvent, WheelEvent } from "react"; -import { useState } from "react"; -import "./panels/advanced/AdvancedPanel.css"; -import { RATE_INPUT_MODE_OPTIONS } from "../cadence"; -import { convertDurationToRate, convertRateToDuration } from "../cadence"; -import { normalizeIntegerRaw } from "../numberInput"; -import type { RateInputMode, Settings } from "../store"; - -import { AdvDropdown } from "./panels/advanced/sections/shared"; -import { getMaxClickSpeed, type ClickInterval } from "../settingsSchema"; - -// TODO: This should really be split up into what is in the advanced panel and what is in the simple panel. Having both in one feels kinda off i feel like. +import type { Settings } from "../store"; +import SimpleCadenceInput from "./panels/SimpleCadenceInput"; +import AdvancedCadenceInput from "./panels/advanced/sections/AdvancedCadenceInput"; interface Props { settings: Settings; update: (patch: Partial) => void; variant: "simple" | "advanced"; - showInfo?: boolean; -} - -const INTERVAL_OPTIONS = [ - { value: "s", label: "Second" }, - { value: "m", label: "Minute" }, - { value: "h", label: "Hour" }, - { value: "d", label: "Day" }, -] as const; - -const SIMPLE_RATE_INPUT_MODE_OPTIONS = [ - { value: "rate", label: "Rate" }, - { value: "duration", label: "Delay" }, -] as const; - -function parseIntegerRaw(raw: string) { - const normalized = normalizeIntegerRaw(raw); - return normalized === "" || normalized === "-" ? 0 : Number(normalized); -} - -function clamp(value: number, min: number, max?: number) { - const minClamped = Math.max(min, value); - return max === undefined ? minClamped : Math.min(max, minClamped); } -function dynamicChWidth(value: number, min = 1, max = 3) { - return `${clamp(String(Math.abs(value)).length, min, max)}ch`; -} - -function handleWheelStep( - event: WheelEvent, - current: number, - min: number, - max: number | undefined, - apply: (next: number) => void, -) { - event.preventDefault(); - event.stopPropagation(); - event.currentTarget.blur(); - const delta = event.deltaY < 0 ? 1 : -1; - let step = 1; - if (event.shiftKey && event.ctrlKey) step = 10; - else if (event.shiftKey) step = 5; - apply(clamp(current + delta * step, min, max)); -} - -function handleNumberChange( - event: ChangeEvent, - apply: (next: number) => void, -) { - const normalized = normalizeIntegerRaw(event.target.value); - if (normalized !== event.target.value) { - event.target.value = normalized; - } - apply(parseIntegerRaw(normalized)); -} - -function handleNumberBlur( - event: FocusEvent, - min: number, - max: number | undefined, - apply: (next: number) => void, -) { - const normalized = normalizeIntegerRaw(event.target.value); - if (normalized !== event.target.value) { - event.target.value = normalized; - } - apply(clamp(parseIntegerRaw(normalized), min, max)); -} - -function DurationField({ - value, - min, - max, - onChange, - style, - unit, - className, -}: { - value: number; - min: number; - max?: number; - onChange: (next: number) => void; - style?: CSSProperties; - unit: string; - className?: string; -}) { - return ( -
- handleNumberChange(event, onChange)} - onBlur={(event) => handleNumberBlur(event, min, max, onChange)} - onWheel={(event) => handleWheelStep(event, value, min, max, onChange)} - style={style} - /> - {unit} -
- ); -} - -export default function CadenceInput({ settings, update, variant }: Props) { - const maxClickSpeed = getMaxClickSpeed(settings.extendedClickSpeedLimit); - - const [draftCps, setDraftCps] = useState(null); - - const switchMode = (mode: RateInputMode) => { - if (mode === settings.rateInputMode) return; - - if (mode === "rate") { - const converted = convertDurationToRate(settings); - update({ - rateInputMode: mode, - ...(converted ?? {}), - }); - return; - } - - const converted = convertRateToDuration(settings); - update({ - rateInputMode: mode, - ...(converted ?? {}), - }); - }; - - const updateSimpleCadence = (patch: Partial) => { - if (variant !== "simple") { - update(patch); - return; - } - - const nextSettings = { ...settings, ...patch }; - - if (nextSettings.rateInputMode === "rate") { - const converted = convertRateToDuration(nextSettings); - update({ - ...patch, - ...(converted ?? {}), - }); - return; - } - - const converted = convertDurationToRate(nextSettings); - update({ - ...patch, - ...(converted ?? {}), - }); - }; - +export default function CadenceInput({ variant, ...rest }: Props) { if (variant === "simple") { - return ( -
- {settings.rateInputMode === "rate" ? ( -
- { - const raw = event.target.value; - if (raw === "") { - setDraftCps(""); - } else { - const normalized = normalizeIntegerRaw(raw); - if (normalized !== raw) event.target.value = normalized; - if (normalized === "" || normalized === "-") { - setDraftCps(normalized); - return; - } - setDraftCps(null); - updateSimpleCadence({ clickSpeed: Number(normalized) }); - } - }} - onBlur={(event) => { - setDraftCps(null); - handleNumberBlur(event, 1, maxClickSpeed, (next) => - updateSimpleCadence({ clickSpeed: next }), - ); - }} - onWheel={(event) => - handleWheelStep( - event, - settings.clickSpeed, - 1, - maxClickSpeed, - (next) => updateSimpleCadence({ clickSpeed: next }), - ) - } - /> -
- Clicks Per -
- - updateSimpleCadence({ clickInterval: value as ClickInterval }) - } - /> -
- switchMode(value as RateInputMode)} - /> -
- ) : ( -
-
- - updateSimpleCadence({ durationHours: next }) - } - style={{ - width: dynamicChWidth(settings.durationHours, 1, 3), - minWidth: "1ch", - }} - unit="h" - /> - - updateSimpleCadence({ durationMinutes: next }) - } - style={{ - width: dynamicChWidth(settings.durationMinutes, 1, 2), - minWidth: "1ch", - }} - unit="m" - /> - - updateSimpleCadence({ durationSeconds: next }) - } - style={{ - width: dynamicChWidth(settings.durationSeconds, 1, 2), - minWidth: "1ch", - }} - unit="s" - /> - - updateSimpleCadence({ durationMilliseconds: next }) - } - style={{ - width: dynamicChWidth(settings.durationMilliseconds, 1, 3), - minWidth: "1ch", - }} - unit="ms" - /> -
-
- Per Click -
- switchMode(value as RateInputMode)} - /> -
- )} -
- ); + return ; } - - const modeToggle = ( -
- {RATE_INPUT_MODE_OPTIONS.map((mode) => ( - - ))} -
- ); - - return ( -
-
-
- {settings.rateInputMode === "rate" ? ( -
-
- { - const raw = event.target.value; - if (raw === "") { - setDraftCps(""); - } else { - const normalized = normalizeIntegerRaw(raw); - if (normalized !== raw) event.target.value = normalized; - if (normalized === "" || normalized === "-") { - setDraftCps(normalized); - return; - } - setDraftCps(null); - update({ clickSpeed: Number(normalized) }); - } - }} - onBlur={(event) => { - setDraftCps(null); - handleNumberBlur(event, 1, maxClickSpeed, (next) => - update({ clickSpeed: next }), - ); - }} - onWheel={(event) => - handleWheelStep( - event, - settings.clickSpeed, - 1, - maxClickSpeed, - (next) => update({ clickSpeed: next }), - ) - } - /> -
-
- Clicks Per -
-
- - update({ clickInterval: v as ClickInterval }) - } - /> -
-
- ) : ( -
-
- - handleNumberChange(event, (next) => - update({ durationHours: next }), - ) - } - onBlur={(event) => - handleNumberBlur(event, 0, 999, (next) => - update({ durationHours: next }), - ) - } - onWheel={(event) => - handleWheelStep( - event, - settings.durationHours, - 0, - 999, - (next) => update({ durationHours: next }), - ) - } - /> - h -
-
-
- - handleNumberChange(event, (next) => - update({ durationMinutes: next }), - ) - } - onBlur={(event) => - handleNumberBlur(event, 0, 59, (next) => - update({ durationMinutes: next }), - ) - } - onWheel={(event) => - handleWheelStep( - event, - settings.durationMinutes, - 0, - 59, - (next) => update({ durationMinutes: next }), - ) - } - /> - m -
-
-
- - handleNumberChange(event, (next) => - update({ durationSeconds: next }), - ) - } - onBlur={(event) => - handleNumberBlur(event, 0, 59, (next) => - update({ durationSeconds: next }), - ) - } - onWheel={(event) => - handleWheelStep( - event, - settings.durationSeconds, - 0, - 59, - (next) => update({ durationSeconds: next }), - ) - } - /> - s -
-
-
- - handleNumberChange(event, (next) => - update({ durationMilliseconds: next }), - ) - } - onBlur={(event) => - handleNumberBlur(event, 0, 999, (next) => - update({ durationMilliseconds: next }), - ) - } - onWheel={(event) => - handleWheelStep( - event, - settings.durationMilliseconds, - 0, - 999, - (next) => update({ durationMilliseconds: next }), - ) - } - /> - ms -
-
- )} -
- {modeToggle} -
-
- ); + return ; } diff --git a/src/components/panels/SimpleCadenceInput.tsx b/src/components/panels/SimpleCadenceInput.tsx new file mode 100644 index 00000000..3a4cf33e --- /dev/null +++ b/src/components/panels/SimpleCadenceInput.tsx @@ -0,0 +1,218 @@ +import { useState } from "react"; +import type { CSSProperties } from "react"; +import type { RateInputMode, Settings } from "../../store"; +import { convertDurationToRate, convertRateToDuration } from "../../cadence"; +import { normalizeIntegerRaw } from "../../numberInput"; +import { getMaxClickSpeed, type ClickInterval } from "../../settingsSchema"; +import { AdvDropdown } from "./advanced/sections/shared"; +import "./advanced/AdvancedPanel.css"; +import { + INTERVAL_OPTIONS, + SIMPLE_RATE_INPUT_MODE_OPTIONS, + dynamicChWidth, + handleNumberBlur, + handleNumberChange, + handleWheelStep, + switchCadenceMode, +} from "../sharedCadence"; + +function DurationField({ + value, + min, + max, + onChange, + style, + unit, + className, +}: { + value: number; + min: number; + max?: number; + onChange: (next: number) => void; + style?: CSSProperties; + unit: string; + className?: string; +}) { + return ( +
+ handleNumberChange(event, onChange)} + onBlur={(event) => handleNumberBlur(event, min, max, onChange)} + onWheel={(event) => handleWheelStep(event, value, min, max, onChange)} + style={style} + /> + {unit} +
+ ); +} + +interface Props { + settings: Settings; + update: (patch: Partial) => void; +} + +export default function SimpleCadenceInput({ settings, update }: Props) { + const maxClickSpeed = getMaxClickSpeed(settings.extendedClickSpeedLimit); + const [draftCps, setDraftCps] = useState(null); + + const updateSimpleCadence = (patch: Partial) => { + const nextSettings = { ...settings, ...patch }; + + if (nextSettings.rateInputMode === "rate") { + const converted = convertRateToDuration(nextSettings); + update({ + ...patch, + ...(converted ?? {}), + }); + return; + } + + const converted = convertDurationToRate(nextSettings); + update({ + ...patch, + ...(converted ?? {}), + }); + }; + + return ( +
+ {settings.rateInputMode === "rate" ? ( +
+ { + const raw = event.target.value; + if (raw === "") { + setDraftCps(""); + } else { + const normalized = normalizeIntegerRaw(raw); + if (normalized !== raw) event.target.value = normalized; + if (normalized === "" || normalized === "-") { + setDraftCps(normalized); + return; + } + setDraftCps(null); + updateSimpleCadence({ clickSpeed: Number(normalized) }); + } + }} + onBlur={(event) => { + setDraftCps(null); + handleNumberBlur(event, 1, maxClickSpeed, (next) => + updateSimpleCadence({ clickSpeed: next }), + ); + }} + onWheel={(event) => + handleWheelStep( + event, + settings.clickSpeed, + 1, + maxClickSpeed, + (next) => updateSimpleCadence({ clickSpeed: next }), + ) + } + /> +
+ Clicks Per +
+ + updateSimpleCadence({ clickInterval: value as ClickInterval }) + } + /> +
+ + switchCadenceMode(value as RateInputMode, settings, update) + } + /> +
+ ) : ( +
+
+ updateSimpleCadence({ durationHours: next })} + style={{ + width: dynamicChWidth(settings.durationHours, 1, 3), + minWidth: "1ch", + }} + unit="h" + /> + + updateSimpleCadence({ durationMinutes: next }) + } + style={{ + width: dynamicChWidth(settings.durationMinutes, 1, 2), + minWidth: "1ch", + }} + unit="m" + /> + + updateSimpleCadence({ durationSeconds: next }) + } + style={{ + width: dynamicChWidth(settings.durationSeconds, 1, 2), + minWidth: "1ch", + }} + unit="s" + /> + + updateSimpleCadence({ durationMilliseconds: next }) + } + style={{ + width: dynamicChWidth(settings.durationMilliseconds, 1, 3), + minWidth: "1ch", + }} + unit="ms" + /> +
+
+ Per Click +
+ + switchCadenceMode(value as RateInputMode, settings, update) + } + /> +
+ )} +
+ ); +} diff --git a/src/components/panels/advanced/sections/AdvancedCadenceInput.tsx b/src/components/panels/advanced/sections/AdvancedCadenceInput.tsx new file mode 100644 index 00000000..e7588774 --- /dev/null +++ b/src/components/panels/advanced/sections/AdvancedCadenceInput.tsx @@ -0,0 +1,233 @@ +import { useState } from "react"; +import type { Settings } from "../../../../store"; +import { RATE_INPUT_MODE_OPTIONS } from "../../../../cadence"; +import { normalizeIntegerRaw } from "../../../../numberInput"; +import { + getMaxClickSpeed, + type ClickInterval, +} from "../../../../settingsSchema"; +import { AdvDropdown } from "./shared"; +import { + INTERVAL_OPTIONS, + handleNumberBlur, + handleNumberChange, + handleWheelStep, + switchCadenceMode, +} from "../../../sharedCadence"; + +interface Props { + settings: Settings; + update: (patch: Partial) => void; +} + +export default function AdvancedCadenceInput({ settings, update }: Props) { + const maxClickSpeed = getMaxClickSpeed(settings.extendedClickSpeedLimit); + const [draftCps, setDraftCps] = useState(null); + + const modeToggle = ( +
+ {RATE_INPUT_MODE_OPTIONS.map((mode) => ( + + ))} +
+ ); + + return ( +
+
+
+ {settings.rateInputMode === "rate" ? ( +
+
+ { + const raw = event.target.value; + if (raw === "") { + setDraftCps(""); + } else { + const normalized = normalizeIntegerRaw(raw); + if (normalized !== raw) event.target.value = normalized; + if (normalized === "" || normalized === "-") { + setDraftCps(normalized); + return; + } + setDraftCps(null); + update({ clickSpeed: Number(normalized) }); + } + }} + onBlur={(event) => { + setDraftCps(null); + handleNumberBlur(event, 1, maxClickSpeed, (next) => + update({ clickSpeed: next }), + ); + }} + onWheel={(event) => + handleWheelStep( + event, + settings.clickSpeed, + 1, + maxClickSpeed, + (next) => update({ clickSpeed: next }), + ) + } + /> +
+
+ Clicks Per +
+
+ + update({ clickInterval: v as ClickInterval }) + } + /> +
+
+ ) : ( +
+
+ + handleNumberChange(event, (next) => + update({ durationHours: next }), + ) + } + onBlur={(event) => + handleNumberBlur(event, 0, 999, (next) => + update({ durationHours: next }), + ) + } + onWheel={(event) => + handleWheelStep( + event, + settings.durationHours, + 0, + 999, + (next) => update({ durationHours: next }), + ) + } + /> + h +
+
+
+ + handleNumberChange(event, (next) => + update({ durationMinutes: next }), + ) + } + onBlur={(event) => + handleNumberBlur(event, 0, 59, (next) => + update({ durationMinutes: next }), + ) + } + onWheel={(event) => + handleWheelStep( + event, + settings.durationMinutes, + 0, + 59, + (next) => update({ durationMinutes: next }), + ) + } + /> + m +
+
+
+ + handleNumberChange(event, (next) => + update({ durationSeconds: next }), + ) + } + onBlur={(event) => + handleNumberBlur(event, 0, 59, (next) => + update({ durationSeconds: next }), + ) + } + onWheel={(event) => + handleWheelStep( + event, + settings.durationSeconds, + 0, + 59, + (next) => update({ durationSeconds: next }), + ) + } + /> + s +
+
+
+ + handleNumberChange(event, (next) => + update({ durationMilliseconds: next }), + ) + } + onBlur={(event) => + handleNumberBlur(event, 0, 999, (next) => + update({ durationMilliseconds: next }), + ) + } + onWheel={(event) => + handleWheelStep( + event, + settings.durationMilliseconds, + 0, + 999, + (next) => update({ durationMilliseconds: next }), + ) + } + /> + ms +
+
+ )} +
+ {modeToggle} +
+
+ ); +} diff --git a/src/components/panels/advanced/sections/CadenceSection.tsx b/src/components/panels/advanced/sections/CadenceSection.tsx index 012ad61b..661a661f 100644 --- a/src/components/panels/advanced/sections/CadenceSection.tsx +++ b/src/components/panels/advanced/sections/CadenceSection.tsx @@ -125,12 +125,7 @@ export default function CadenceSection({ settings, update, showInfo }: Props) {
- +
, + current: number, + min: number, + max: number | undefined, + apply: (next: number) => void, +) { + event.preventDefault(); + event.stopPropagation(); + event.currentTarget.blur(); + const delta = event.deltaY < 0 ? 1 : -1; + let step = 1; + if (event.shiftKey && event.ctrlKey) step = 10; + else if (event.shiftKey) step = 5; + apply(clamp(current + delta * step, min, max)); +} + +export function handleNumberChange( + event: ChangeEvent, + apply: (next: number) => void, +) { + const normalized = normalizeIntegerRaw(event.target.value); + if (normalized !== event.target.value) { + event.target.value = normalized; + } + apply(parseIntegerRaw(normalized)); +} + +export function switchCadenceMode( + mode: RateInputMode, + settings: Settings, + update: (patch: Partial) => void, +) { + if (mode === settings.rateInputMode) return; + + if (mode === "rate") { + const converted = convertDurationToRate(settings); + update({ + rateInputMode: mode, + ...(converted ?? {}), + }); + return; + } + + const converted = convertRateToDuration(settings); + update({ + rateInputMode: mode, + ...(converted ?? {}), + }); +} + +export function handleNumberBlur( + event: FocusEvent, + min: number, + max: number | undefined, + apply: (next: number) => void, +) { + const normalized = normalizeIntegerRaw(event.target.value); + if (normalized !== event.target.value) { + event.target.value = normalized; + } + apply(clamp(parseIntegerRaw(normalized), min, max)); +} From f21cdc6a1c43a1f0af739ffc26fde37a9446a6ad Mon Sep 17 00:00:00 2001 From: Blur Date: Thu, 2 Jul 2026 22:26:35 +0200 Subject: [PATCH 06/45] fix: update active background and focus ring colors for simple tab in title bar --- CHANGELOG.md | 2 ++ src/components/TitleBar.tsx | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38a2e682..a2b56786 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # v3.9.0 - 02.07.2026 (d.m.y) ## Changed - Settings panel now has a side menu for easier navigation and organization of settings. +# Fixed +- Fixed simple tab button in the title bar showing the wrong background color when a custom accent color is selected. # v3.8.3 - 28.06.2026 (d.m.y) ## Changed diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx index c9d484c9..75e5f6ce 100644 --- a/src/components/TitleBar.tsx +++ b/src/components/TitleBar.tsx @@ -157,8 +157,8 @@ const TAB_ITEMS: readonly TabItem[] = [ value: "simple", label: "Simple", color: "var(--accent-green)", - activeBg: "rgba(25, 194, 51, 0.1)", - activeFocusRing: "rgba(25, 194, 51, 0.25)", + activeBg: "var(--accent-green-soft)", + activeFocusRing: "var(--accent-green-ring)", icon: ({ active }) => , }, { From 8a21eee29bc433b899c4ca580257449af92736be Mon Sep 17 00:00:00 2001 From: Blur Date: Thu, 2 Jul 2026 22:43:02 +0200 Subject: [PATCH 07/45] fix scroll bar appearing on tab switching --- .../panels/advanced/AdvancedPanel.css | 18 +++++- .../panels/settings/SettingsPanel.css | 59 ++++++++++++++++++- src/components/panels/zones/ZonesPanel.css | 9 ++- src/index.css | 42 ++++++++++++- 4 files changed, 121 insertions(+), 7 deletions(-) diff --git a/src/components/panels/advanced/AdvancedPanel.css b/src/components/panels/advanced/AdvancedPanel.css index 68f0d65e..9766dc48 100644 --- a/src/components/panels/advanced/AdvancedPanel.css +++ b/src/components/panels/advanced/AdvancedPanel.css @@ -90,9 +90,10 @@ input[type="number"]::-webkit-outer-spin-button { .adv-columns--tall { display: block; - overflow-y: auto; + overflow-y: hidden; padding-right: 0.25rem; scrollbar-gutter: stable; + animation: adv-columns-enable 0.01s 350ms forwards; } .adv-columns--tall::-webkit-scrollbar { @@ -110,6 +111,12 @@ input[type="number"]::-webkit-outer-spin-button { background: #777; } +@keyframes adv-columns-enable { + to { + overflow-y: auto; + } +} + .adv-col { display: flex; flex-direction: column; @@ -589,12 +596,13 @@ input[type="number"]::-webkit-outer-spin-button { position: relative; z-index: 0; width: 100%; - overflow-y: auto; + overflow-y: hidden; min-height: 0; height: 100%; padding: 0 2px 0 0; box-sizing: border-box; scrollbar-gutter: stable; + animation: adv-seqlist-enable 0.01s 350ms forwards; } .adv-sequence-list::-webkit-scrollbar { @@ -612,6 +620,12 @@ input[type="number"]::-webkit-outer-spin-button { background: #777; } +@keyframes adv-seqlist-enable { + to { + overflow-y: auto; + } +} + .adv-sequence-list > * + * { margin-top: 6px; } diff --git a/src/components/panels/settings/SettingsPanel.css b/src/components/panels/settings/SettingsPanel.css index 04f21c6f..ef76213a 100644 --- a/src/components/panels/settings/SettingsPanel.css +++ b/src/components/panels/settings/SettingsPanel.css @@ -16,7 +16,30 @@ padding: 0.5rem 0; background: var(--bg-surface); backdrop-filter: blur(var(--bg-panel-blur, 0px)); - overflow-y: auto; + overflow-y: hidden; + scrollbar-gutter: stable; + animation: settings-sidebar-enable 0.01s 350ms forwards; +} + +.settings-sidebar::-webkit-scrollbar { + width: 0.375rem; + background: var(--bg-elevated); + border-radius: 0.5rem; +} + +.settings-sidebar::-webkit-scrollbar-thumb { + background: var(--text-dim); + border-radius: 0.25rem; +} + +.settings-sidebar::-webkit-scrollbar-thumb:hover { + background: #777; +} + +@keyframes settings-sidebar-enable { + to { + overflow-y: auto; + } } .sidebar-tab { @@ -56,8 +79,31 @@ border-top: 2px solid rgb(49, 49, 49); border-left: 2px solid rgb(49, 49, 49); border-top-left-radius: var(--radius-lg); - overflow-y: auto; + overflow-y: hidden; + scrollbar-gutter: stable; min-width: 0; + animation: settings-panel-enable 0.01s 350ms forwards; +} + +.settings-panel::-webkit-scrollbar { + width: 0.375rem; + background: var(--bg-elevated); + border-radius: 0.5rem; +} + +.settings-panel::-webkit-scrollbar-thumb { + background: var(--text-dim); + border-radius: 0.25rem; +} + +.settings-panel::-webkit-scrollbar-thumb:hover { + background: #777; +} + +@keyframes settings-panel-enable { + to { + overflow-y: auto; + } } .settings-panel::before { @@ -399,10 +445,11 @@ flex-direction: column; gap: 0.625rem; max-height: clamp(20rem, 42vh, 26rem); - overflow-y: auto; + overflow-y: hidden; padding-right: 0.25rem; scrollbar-gutter: stable; overscroll-behavior: contain; + animation: preset-list-enable 0.01s 350ms forwards; } .preset-list::-webkit-scrollbar { @@ -420,6 +467,12 @@ background: #777; } +@keyframes preset-list-enable { + to { + overflow-y: auto; + } +} + .preset-list-shell { position: relative; } diff --git a/src/components/panels/zones/ZonesPanel.css b/src/components/panels/zones/ZonesPanel.css index a222f06b..2ddafd8c 100644 --- a/src/components/panels/zones/ZonesPanel.css +++ b/src/components/panels/zones/ZonesPanel.css @@ -236,9 +236,10 @@ .adv-process-list { flex: 1; min-height: 0; - overflow-y: auto; + overflow-y: hidden; scrollbar-gutter: stable; padding-right: 4px; + animation: adv-proc-enable 0.01s 350ms forwards; } .adv-process-list::-webkit-scrollbar { @@ -256,6 +257,12 @@ background: #777; } +@keyframes adv-proc-enable { + to { + overflow-y: auto; + } +} + .adv-process-list .adv-sequence-item { display: flex; align-items: center; diff --git a/src/index.css b/src/index.css index 33242910..4751c792 100644 --- a/src/index.css +++ b/src/index.css @@ -146,13 +146,53 @@ body { padding: 1.25rem 1.25rem 0.75rem 1.25rem; display: flex; flex-direction: column; - overflow: auto; + overflow: hidden; min-height: 0; min-width: 0; contain: layout style paint; backdrop-filter: blur(var(--bg-panel-blur, 0px)); } +.app-root[data-tab="simple"] .panel-area { + animation: panel-enable-scroll-simple 0.01s 350ms forwards; +} + +.app-root[data-tab="advanced"] .panel-area { + animation: panel-enable-scroll-advanced 0.01s 350ms forwards; +} + +.app-root[data-tab="zones"] .panel-area { + animation: panel-enable-scroll-zones 0.01s 350ms forwards; +} + +.app-root[data-tab="settings"] .panel-area { + animation: panel-enable-scroll-settings 0.01s 350ms forwards; +} + +@keyframes panel-enable-scroll-simple { + to { + overflow: auto; + } +} + +@keyframes panel-enable-scroll-advanced { + to { + overflow: auto; + } +} + +@keyframes panel-enable-scroll-zones { + to { + overflow: auto; + } +} + +@keyframes panel-enable-scroll-settings { + to { + overflow: auto; + } +} + .settings-card { backdrop-filter: blur(var(--bg-panel-blur, 0px)); } From 12443ae285253dd2358d8ecb1ae6b4887687e9c9 Mon Sep 17 00:00:00 2001 From: Blur Date: Thu, 2 Jul 2026 23:05:54 +0200 Subject: [PATCH 08/45] fix: top left corner of settings panel having the wrong color --- .../panels/settings/SettingsPanel.css | 43 +++++++++---------- .../panels/settings/SettingsPanel.tsx | 1 + 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/components/panels/settings/SettingsPanel.css b/src/components/panels/settings/SettingsPanel.css index ef76213a..900edec3 100644 --- a/src/components/panels/settings/SettingsPanel.css +++ b/src/components/panels/settings/SettingsPanel.css @@ -85,6 +85,27 @@ animation: settings-panel-enable 0.01s 350ms forwards; } +.settings-corner { + position: absolute; + top: 0; + left: 10rem; + width: var(--radius-lg); + height: var(--radius-lg); + background: var(--bg-surface); + pointer-events: none; + z-index: 1; + -webkit-mask-image: radial-gradient( + circle at var(--radius-lg) var(--radius-lg), + transparent var(--radius-lg), + black var(--radius-lg) + ); + mask-image: radial-gradient( + circle at var(--radius-lg) var(--radius-lg), + transparent var(--radius-lg), + black var(--radius-lg) + ); +} + .settings-panel::-webkit-scrollbar { width: 0.375rem; background: var(--bg-elevated); @@ -105,28 +126,6 @@ overflow-y: auto; } } - -.settings-panel::before { - content: ""; - position: absolute; - top: -2px; - left: -2px; - width: var(--radius-lg); - height: var(--radius-lg); - background: var(--bg-surface); - pointer-events: none; - -webkit-mask-image: radial-gradient( - circle at var(--radius-lg) var(--radius-lg), - transparent var(--radius-lg), - black var(--radius-lg) - ); - mask-image: radial-gradient( - circle at var(--radius-lg) var(--radius-lg), - transparent var(--radius-lg), - black var(--radius-lg) - ); -} - [data-theme="light"] .settings-panel { border-top-color: rgb(166, 166, 166); border-left-color: rgb(166, 166, 166); diff --git a/src/components/panels/settings/SettingsPanel.tsx b/src/components/panels/settings/SettingsPanel.tsx index 9575fbd4..04788c79 100644 --- a/src/components/panels/settings/SettingsPanel.tsx +++ b/src/components/panels/settings/SettingsPanel.tsx @@ -111,6 +111,7 @@ export default function SettingsPanel({ Maintenance +
{activeTab === "general" && ( Date: Thu, 2 Jul 2026 23:14:27 +0200 Subject: [PATCH 09/45] changed some wording for settings tab and moved reset buttons together --- .../settings/sections/GeneralSection.tsx | 59 ++----------------- .../settings/sections/MaintenanceSection.tsx | 58 +++++++++++++++--- 2 files changed, 54 insertions(+), 63 deletions(-) diff --git a/src/components/panels/settings/sections/GeneralSection.tsx b/src/components/panels/settings/sections/GeneralSection.tsx index d90ba415..ae50019e 100644 --- a/src/components/panels/settings/sections/GeneralSection.tsx +++ b/src/components/panels/settings/sections/GeneralSection.tsx @@ -4,7 +4,6 @@ import { openUrl } from "@tauri-apps/plugin-opener"; import type { AppInfo } from "../../../../store"; import { changelogEntries } from "../../../../changelog"; import ChangelogContent from "../../../ChangelogContent"; -import ConfirmDialog from "../../../ConfirmDialog"; import { SettingsCard } from "./shared"; interface CumulativeStats { @@ -67,10 +66,6 @@ export default function GeneralSection({ }: Props) { const [stats, setStats] = useState(null); const [showChangelog, setShowChangelog] = useState(false); - const [pendingAction, setPendingAction] = useState<"clear-stats" | null>( - null, - ); - const [resettingStats, setResettingStats] = useState(false); const language = "en"; useEffect(() => { @@ -81,19 +76,6 @@ export default function GeneralSection({ const hasStats = stats !== null && stats.totalSessions > 0; - const handleConfirmClearStats = async () => { - setResettingStats(true); - try { - const next = await invoke("reset_stats"); - setStats(next); - } catch { - // stats unchanged on failure - } finally { - setResettingStats(false); - setPendingAction(null); - } - }; - const updateButtonLabel = { idle: "Check for Update", checking: "Checking...", @@ -221,16 +203,8 @@ export default function GeneralSection({ -
-
- Usage Data - - Statistics are stored locally and never sent anywhere. - -
-
{hasStats ? (
@@ -240,19 +214,19 @@ export default function GeneralSection({
- Total Time Clicking + Total Clicking Time {formatTime(stats.totalTimeSecs, language)}
- Average CPU + Average CPU Usage {formatCpu(stats.avgCpu, language, "N/A")}
- Sessions + Clicking Sessions {formatNumber(stats.totalSessions, language)} @@ -261,32 +235,7 @@ export default function GeneralSection({ ) : (
No session data yet.
)} - {hasStats && ( -
-
- Clear Stats - Clear all usage data. -
- -
- )} - - setPendingAction(null)} - /> ); } diff --git a/src/components/panels/settings/sections/MaintenanceSection.tsx b/src/components/panels/settings/sections/MaintenanceSection.tsx index 7ec22734..37b829dd 100644 --- a/src/components/panels/settings/sections/MaintenanceSection.tsx +++ b/src/components/panels/settings/sections/MaintenanceSection.tsx @@ -4,26 +4,45 @@ import { error } from "@tauri-apps/plugin-log"; import ConfirmDialog from "../../../ConfirmDialog"; import { SettingsCard } from "./shared"; +interface CumulativeStats { + totalClicks: number; + totalTimeSecs: number; + totalSessions: number; + avgCpu: number; +} + interface Props { onReset: () => Promise; } export default function MaintenanceSection({ onReset }: Props) { - const [pendingAction, setPendingAction] = useState<"reset-settings" | null>( - null, - ); - const [resetting, setResetting] = useState(false); + const [pendingAction, setPendingAction] = useState< + "reset-settings" | "reset-usage" | null + >(null); + const [busy, setBusy] = useState(false); const [diagnosticsStatus, setDiagnosticsStatus] = useState( null, ); const [exporting, setExporting] = useState(false); const handleConfirmResetSettings = async () => { - setResetting(true); + setBusy(true); try { await onReset(); } finally { - setResetting(false); + setBusy(false); + setPendingAction(null); + } + }; + + const handleConfirmResetUsage = async () => { + setBusy(true); + try { + await invoke("reset_stats"); + } catch { + // ignore + } finally { + setBusy(false); setPendingAction(null); } }; @@ -36,7 +55,7 @@ export default function MaintenanceSection({ onReset }: Props) { >
- Reset All + Reset All Settings Reset all settings to their defaults. @@ -48,6 +67,20 @@ export default function MaintenanceSection({ onReset }: Props) { Reset
+
+
+ Reset Usage Data + + Clear all session statistics and usage history. + +
+ +
@@ -114,10 +147,19 @@ export default function MaintenanceSection({ onReset }: Props) { title="Reset all settings" message="This will reset all settings to their default values. This action cannot be undone." confirmLabel="Reset" - busy={resetting} + busy={busy} onConfirm={handleConfirmResetSettings} onCancel={() => setPendingAction(null)} /> + setPendingAction(null)} + /> ); } From 7abf7b2030654e47742c1051c48390c54bd893e3 Mon Sep 17 00:00:00 2001 From: Blur Date: Thu, 2 Jul 2026 23:24:49 +0200 Subject: [PATCH 10/45] add ko-fi badge to settings panel --- .../panels/settings/SettingsPanel.css | 22 +++++++++++++++++++ .../panels/settings/SettingsPanel.tsx | 15 +++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/components/panels/settings/SettingsPanel.css b/src/components/panels/settings/SettingsPanel.css index 900edec3..22501162 100644 --- a/src/components/panels/settings/SettingsPanel.css +++ b/src/components/panels/settings/SettingsPanel.css @@ -42,6 +42,28 @@ } } +.sidebar-kofi { + margin-top: auto; + display: flex; + align-items: center; + justify-content: center; + padding: 0.5rem 0.75rem; + cursor: pointer; + text-decoration: none; + opacity: 0.6; + transition: opacity 0.15s ease, transform 0.2s ease; +} + +.sidebar-kofi:hover { + opacity: 1; + transform: scale(1.05); +} + +.sidebar-kofi img { + height: 4rem; + border: 0; +} + .sidebar-tab { all: unset; cursor: pointer; diff --git a/src/components/panels/settings/SettingsPanel.tsx b/src/components/panels/settings/SettingsPanel.tsx index 04788c79..7ca15df6 100644 --- a/src/components/panels/settings/SettingsPanel.tsx +++ b/src/components/panels/settings/SettingsPanel.tsx @@ -1,5 +1,6 @@ import "./SettingsPanel.css"; import { useEffect, useRef, useState } from "react"; +import { openUrl } from "@tauri-apps/plugin-opener"; import type { AppInfo, PresetId, Settings } from "../../../store"; import GeneralSection from "./sections/GeneralSection"; @@ -110,6 +111,20 @@ export default function SettingsPanel({ > Maintenance + { + e.preventDefault(); + void openUrl("https://ko-fi.com/Z8Z71T8QD4"); + }} + > + Buy Me a Coffee at ko-fi.com +
From 2c8818a736460c40f626903b6d896079efac1941 Mon Sep 17 00:00:00 2001 From: Blur Date: Thu, 2 Jul 2026 23:34:16 +0200 Subject: [PATCH 11/45] added placeholder icons for settings page --- .../panels/settings/SettingsPanel.css | 11 +++- .../panels/settings/SettingsPanel.tsx | 56 ++++++++++++++++++- 2 files changed, 65 insertions(+), 2 deletions(-) diff --git a/src/components/panels/settings/SettingsPanel.css b/src/components/panels/settings/SettingsPanel.css index 22501162..f4c1b699 100644 --- a/src/components/panels/settings/SettingsPanel.css +++ b/src/components/panels/settings/SettingsPanel.css @@ -51,7 +51,9 @@ cursor: pointer; text-decoration: none; opacity: 0.6; - transition: opacity 0.15s ease, transform 0.2s ease; + transition: + opacity 0.15s ease, + transform 0.2s ease; } .sidebar-kofi:hover { @@ -66,6 +68,9 @@ .sidebar-tab { all: unset; + display: flex; + align-items: center; + gap: 0.5rem; cursor: pointer; font-family: var(--font-family-base); font-size: 0.8125rem; @@ -80,6 +85,10 @@ text-align: left; } +.sidebar-tab svg { + flex-shrink: 0; +} + .sidebar-tab:hover { color: var(--text-primary); background: var(--bg-elevated); diff --git a/src/components/panels/settings/SettingsPanel.tsx b/src/components/panels/settings/SettingsPanel.tsx index 7ca15df6..0b21c955 100644 --- a/src/components/panels/settings/SettingsPanel.tsx +++ b/src/components/panels/settings/SettingsPanel.tsx @@ -78,37 +78,91 @@ export default function SettingsPanel({ Date: Thu, 2 Jul 2026 23:52:34 +0200 Subject: [PATCH 12/45] refactor: remove unused CSS class and clean up imports in ZonesPanel --- .../panels/advanced/AdvancedPanel.css | 6 - src/components/panels/zones/ZonesPanel.css | 446 ++++++++++++++++++ src/components/panels/zones/ZonesPanel.tsx | 1 - 3 files changed, 446 insertions(+), 7 deletions(-) diff --git a/src/components/panels/advanced/AdvancedPanel.css b/src/components/panels/advanced/AdvancedPanel.css index 9766dc48..2bd37af1 100644 --- a/src/components/panels/advanced/AdvancedPanel.css +++ b/src/components/panels/advanced/AdvancedPanel.css @@ -257,12 +257,6 @@ input[type="number"]::-webkit-outer-spin-button { gap: 0.5rem; } -.adv-row--grow { - flex: 1; - overflow: hidden; - align-items: stretch; -} - .adv-target-row { min-width: 0; } diff --git a/src/components/panels/zones/ZonesPanel.css b/src/components/panels/zones/ZonesPanel.css index 2ddafd8c..f9742ea2 100644 --- a/src/components/panels/zones/ZonesPanel.css +++ b/src/components/panels/zones/ZonesPanel.css @@ -424,3 +424,449 @@ .adv-proc-search:focus { border-color: var(--accent); } + +/* ============================= */ +/* Shared panel structure */ +/* ============================= */ + +.adv-panel { + --adv-control-height: 32px; +} + +.adv-panel { + display: flex; + flex-direction: column; + height: 100%; +} + +.adv-row { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.adv-row--grow { + flex: 1; + overflow: hidden; + align-items: stretch; +} + +/* ============================= */ +/* Card / section container */ +/* ============================= */ + +.adv-sectioncontainer { + background: var(--bg-surface); + border: 0px solid var(--border); + border-radius: 0.4375rem; + padding: 0.625rem 0.75rem; + margin-bottom: 0.625rem; + flex: 1; + box-shadow: var(--container-shadow); +} + +.adv-card-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; +} + +.adv-card-title { + font-family: "DMSans", sans-serif; + font-size: 0.875rem; + font-weight: var(--font-weight-hv); + color: var(--text-primary); + letter-spacing: -0.02em; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.adv-card-divider { + width: 100%; + border-radius: 1px; + height: 1px; + background: var(--border-subtle); + margin: 0.5rem 0; +} + +/* ============================= */ +/* Input controls */ +/* ============================= */ + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.adv-numbox-sm { + display: flex; + align-items: center; + gap: 2px; + background: var(--bg-elevated); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + min-height: var(--adv-control-height); + padding: 0 0.5rem; +} + +.adv-number-sm { + font-family: "DMSans", sans-serif; + font-size: 0.875rem; + font-weight: var(--font-weight-hv); + color: var(--text-primary); +} + +.adv-unit { + font-family: "DMSans", sans-serif; + font-size: 0.625rem; + font-weight: var(--font-weight-hv); + letter-spacing: 0.05em; + color: var(--text-dim); + margin-left: 3px; +} + +.adv-secondary-btn { + display: inline-flex; + align-items: center; + justify-content: center; + height: 30px; + box-sizing: border-box; + font-family: "DMSans", sans-serif; + font-size: 12px; + font-weight: var(--font-weight-hv); + padding: 0 8px; + line-height: 1; + background: transparent; + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + color: var(--text-muted); + cursor: pointer; + transition: + border-color 0.15s ease, + color 0.15s ease, + opacity 0.15s ease; +} + +.adv-secondary-btn:hover { + border-color: var(--text-dim); + color: var(--text-primary); +} + +.adv-secondary-btn:disabled, +.adv-pick-btn:disabled { + opacity: 0.6; + cursor: progress; +} + +/* ============================= */ +/* Segmented button group */ +/* ============================= */ + +.adv-seg-group { + display: flex; + background: var(--bg-elevated); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + min-height: var(--adv-control-height); + overflow: hidden; + transition-duration: 0.2s; + flex-shrink: 0; +} + +.adv-seg-group:hover { + border-color: var(--border-focus); +} + +.adv-seg-btn { + flex: 1; + min-width: 4.5rem; + height: calc(var(--adv-control-height) - 2px); + font-family: "DMSans", sans-serif; + font-size: 1rem; + font-weight: var(--font-weight-md); + line-height: 1.125rem; + padding: 0.25rem 0.75rem; + background: transparent; + border: none; + color: var(--text-dim); + cursor: pointer; + transition: + color 0.2s ease, + background 0.2s ease; +} + +.adv-seg-btn:hover { + color: var(--text-muted); +} + +.adv-seg-btn.active { + background: var(--bg-input); + color: var(--text-primary); +} + +.adv-seg-btn:focus, +.adv-seg-btn:focus-visible { + outline: none; + box-shadow: none; +} + +/* ============================= */ +/* Toggle buttons (On/Off) */ +/* ============================= */ + +.adv-toggle-group { + display: flex; + background: var(--bg-elevated); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + min-height: var(--adv-control-height); + overflow: hidden; + flex-shrink: 0; +} + +.adv-toggle-group:hover { + border-color: var(--); +} + +.adv-toggle-btn { + font-family: "DMSans", sans-serif; + font-size: 0.75rem; + font-weight: 800; + letter-spacing: 4%; + text-transform: uppercase; + height: calc(var(--adv-control-height) - 2px); + padding: 0 0.5rem; + min-width: 2.1875rem; + text-align: center; + background: transparent; + border: none; + color: var(--text-dim); + cursor: pointer; + transition: + color 50ms ease, + background 50ms ease; +} + +.adv-toggle-btn:hover { + color: var(--text-muted); +} + +.adv-toggle-on.active { + background: #3a8a5c; + color: var(--text-primary); +} + +.adv-toggle-off.active { + background: var(--bg-input); + color: #a33c3c; +} + +.adv-toggle-btn.adv-disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} + +/* ============================= */ +/* Sequence item (used by proc */ +/* list) */ +/* ============================= */ + +.adv-sequence-item { + display: grid; + grid-template-columns: + 28px minmax(0, auto) minmax(0, auto) minmax(0, auto) + auto; + gap: 6px; + align-items: center; + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + padding: 6px; + background: var(--bg-elevated); + width: 100%; + box-sizing: border-box; + flex: 0 0 auto; + min-height: fit-content; + position: relative; + overflow: hidden; + --sequence-step-duration: 1ms; + --sequence-step-clicks: 1; + transition: + border-color 220ms ease, + box-shadow 280ms ease, + background-color 220ms ease; +} + +.adv-sequence-item::before { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 2px; + background: color-mix(in srgb, var(--border-subtle) 80%, transparent); + pointer-events: none; +} + +.adv-sequence-empty { + font-family: "DMSans", sans-serif; + font-size: 12px; + color: var(--text-dim); + border: 1px dashed var(--border-subtle); + border-radius: var(--radius-sm); + padding: 10px 12px; + width: 100%; + box-sizing: border-box; + overflow-wrap: break-word; + word-break: break-word; +} + +.adv-sequence-coord { + min-width: 0; + min-height: 26px; + padding: 0 0.35rem; +} + +.adv-sequence-coord .adv-number-sm { + height: 26px; + line-height: 26px; + box-sizing: border-box; + padding: 0; +} + +.adv-sequence-actions { + display: flex; + gap: 6px; + flex-wrap: wrap; + margin-left: auto; +} + +/* ============================= */ +/* Disabled overlay */ +/* ============================= */ + +.adv-disabled-container { + position: relative; + transition: opacity 0.3s ease; +} + +.adv-disabled-content { + opacity: 0.35; + pointer-events: none; +} + +.adv-disabled-overlay { + position: absolute; + inset: -0.25rem; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.01); + backdrop-filter: blur(0px); + border-radius: var(--radius-md); + opacity: 0; + pointer-events: none; + transition: all 0.2s ease; + z-index: 10; +} + +.adv-disabled-container:hover .adv-disabled-overlay { + opacity: 1; + backdrop-filter: blur(2px); + background: rgba(0, 0, 0, 0.1); + pointer-events: auto; +} + +.adv-disabled-label { + font-family: "DMSans", sans-serif; + font-size: 0.6875rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); + background: var(--bg-surface); + padding: 0.25rem 0.625rem; + border-radius: 1.25rem; + border: 1px solid var(--border-subtle); + box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.2); +} + +/* ============================= */ +/* Number input (NumInput) */ +/* ============================= */ + +.adv-number-sm { + font-family: "DMSans", sans-serif; + font-size: 0.75rem; + font-weight: var(--font-weight-hv); + color: var(--text-dim); + background: transparent; + border: none; + outline: none; +} + +/* ============================= */ +/* Info icon / tooltip */ +/* ============================= */ + +.adv-info-icon { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + cursor: help; + color: var(--text-dim); + transition: color 0.15s ease; + user-select: none; + z-index: 2000; + isolation: isolate; +} + +.adv-info-icon:hover { + color: var(--text-muted); +} + +.adv-info-tooltip { + visibility: hidden; + opacity: 0; + position: absolute; + bottom: calc(100% + 6px); + left: 0; + transform: none; + width: 220px; + padding: 6px 8px; + background: var(--bg-elevated); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + font-family: "DMSans", sans-serif; + font-size: 0.6875rem; + font-weight: var(--font-weight-li); + color: var(--text-muted); + line-height: 1.5; + letter-spacing: 0.02em; + text-align: left; + z-index: 3000; + pointer-events: none; + transition: + opacity 0.15s ease, + visibility 0.15s ease; + box-shadow: var(--container-shadow); +} + +.adv-info-icon:hover .adv-info-tooltip { + visibility: visible; + opacity: 1; +} + +.adv-info-tooltip--portal { + position: fixed; + visibility: visible; + opacity: 1; + bottom: auto; + z-index: 9999; +} diff --git a/src/components/panels/zones/ZonesPanel.tsx b/src/components/panels/zones/ZonesPanel.tsx index f464ba74..e368bad0 100644 --- a/src/components/panels/zones/ZonesPanel.tsx +++ b/src/components/panels/zones/ZonesPanel.tsx @@ -1,4 +1,3 @@ -import "../advanced/AdvancedPanel.css"; import "./ZonesPanel.css"; import type { Settings } from "../../../store"; import FailsafeSection from "./sections/FailsafeSection"; From b2c5034005db92a424163389c64df4443846bd9f Mon Sep 17 00:00:00 2001 From: Blur Date: Thu, 2 Jul 2026 23:55:50 +0200 Subject: [PATCH 13/45] consolidate ts tests into one folder --- src/components/panels/settings/SettingsPanel.tsx | 3 ++- src/{components/__tests__ => test}/ErrorBoundary.test.tsx | 2 +- src/{__tests__ => test}/main.test.tsx | 0 3 files changed, 3 insertions(+), 2 deletions(-) rename src/{components/__tests__ => test}/ErrorBoundary.test.tsx (97%) rename src/{__tests__ => test}/main.test.tsx (100%) diff --git a/src/components/panels/settings/SettingsPanel.tsx b/src/components/panels/settings/SettingsPanel.tsx index 0b21c955..22959525 100644 --- a/src/components/panels/settings/SettingsPanel.tsx +++ b/src/components/panels/settings/SettingsPanel.tsx @@ -78,7 +78,8 @@ export default function SettingsPanel({
+
); } From 07cee0d639ca38e05fe1119ce14dbfe7f1b359ca Mon Sep 17 00:00:00 2001 From: Blur Date: Fri, 3 Jul 2026 00:35:09 +0200 Subject: [PATCH 15/45] re-fix remove unused css class and clean up imports in Zones panel --- .../panels/advanced/AdvancedPanel.css | 447 +--------------- .../panels/advanced/AdvancedPanel.tsx | 1 + .../panels/advanced/sections/shared.css | 475 ++++++++++++++++++ .../panels/advanced/sections/shared.tsx | 2 + src/components/panels/zones/ZonesPanel.css | 450 +---------------- src/components/panels/zones/ZonesPanel.tsx | 1 + 6 files changed, 494 insertions(+), 882 deletions(-) create mode 100644 src/components/panels/advanced/sections/shared.css diff --git a/src/components/panels/advanced/AdvancedPanel.css b/src/components/panels/advanced/AdvancedPanel.css index 2bd37af1..38b9b061 100644 --- a/src/components/panels/advanced/AdvancedPanel.css +++ b/src/components/panels/advanced/AdvancedPanel.css @@ -1,38 +1,3 @@ -.adv-panel { - --adv-control-height: 32px; -} - -.adv-seg-group { - display: flex; - background: var(--bg-elevated); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - min-height: var(--adv-control-height); - overflow: hidden; - transition-duration: 0.2s; - flex-shrink: 0; -} -.adv-seg-group:hover { - border-color: var(--border-focus); -} -.adv-seg-btn { - flex: 1; - min-width: 4.5rem; - height: calc(var(--adv-control-height) - 2px); - font-family: "DMSans", sans-serif; - font-size: 1rem; - font-weight: var(--font-weight-md); - line-height: 1.125rem; - padding: 0.25rem 0.75rem; - background: transparent; - border: none; - color: var(--text-dim); - cursor: pointer; - transition: - color 0.2s ease, - background 0.2s ease; -} - .adv-seg-btn-dynamic { flex: 1; height: calc(var(--adv-control-height) - 2px); @@ -69,18 +34,6 @@ box-shadow: none; } -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; -} - -.adv-panel { - display: flex; - flex-direction: column; - height: 100%; -} - .adv-columns { display: flex; gap: 0.625rem; @@ -136,17 +89,6 @@ input[type="number"]::-webkit-outer-spin-button { flex: none; } -/* Card */ -.adv-sectioncontainer { - background: var(--bg-surface); - border: 0px solid var(--border); - border-radius: 0.4375rem; - padding: 0.625rem 0.75rem; - margin-bottom: 0.625rem; - flex: 1; - box-shadow: var(--container-shadow); -} - .adv-col > .adv-sectioncontainer:last-child { margin-bottom: 0; } @@ -159,13 +101,6 @@ input[type="number"]::-webkit-outer-spin-button { flex: none; } -.adv-card-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.5rem; -} - .adv-cadence-block { display: flex; flex-direction: column; @@ -221,27 +156,6 @@ input[type="number"]::-webkit-outer-spin-button { flex-wrap: wrap; } -.adv-card-title { - font-family: "DMSans", sans-serif; - font-size: 0.875rem; - font-weight: var(--font-weight-hv); - color: var(--text-primary); - letter-spacing: -0.02em; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -/* Divider */ -.adv-card-divider { - width: 100%; - border-radius: 1px; - height: 1px; - background: var(--border-subtle); - margin: 0.5rem 0; -} - .adv-desc { font-family: "DMSans", sans-serif; font-size: 0.6875rem; @@ -251,12 +165,6 @@ input[type="number"]::-webkit-outer-spin-button { letter-spacing: 0.02em; } -.adv-row { - display: flex; - align-items: center; - gap: 0.5rem; -} - .adv-target-row { min-width: 0; } @@ -278,33 +186,6 @@ input[type="number"]::-webkit-outer-spin-button { white-space: nowrap; } -.adv-numbox-sm { - display: flex; - align-items: center; - gap: 2px; - background: var(--bg-elevated); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - min-height: var(--adv-control-height); - padding: 0 0.5rem; -} - -.adv-number-sm { - font-family: "DMSans", sans-serif; - font-size: 0.875rem; - font-weight: var(--font-weight-hv); - color: var(--text-primary); -} - -.adv-unit { - font-family: "DMSans", sans-serif; - font-size: 0.625rem; - font-weight: var(--font-weight-hv); - letter-spacing: 0.05em; - color: var(--text-dim); - margin-left: 3px; -} - .adv-textbox { display: flex; align-items: center; @@ -412,54 +293,6 @@ input[type="number"]::-webkit-outer-spin-button { flex-wrap: wrap; } -/* ON/OFF toggle */ -.adv-toggle-group { - display: flex; - background: var(--bg-elevated); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - min-height: var(--adv-control-height); - overflow: hidden; - flex-shrink: 0; -} - -.adv-toggle-group:hover { - border-color: var(--); -} - -.adv-toggle-btn { - font-family: "DMSans", sans-serif; - font-size: 0.75rem; - font-weight: 800; - letter-spacing: 4%; - text-transform: uppercase; - height: calc(var(--adv-control-height) - 2px); - padding: 0 0.5rem; - min-width: 2.1875rem; - text-align: center; - background: transparent; - border: none; - color: var(--text-dim); - cursor: pointer; - transition: - color 50ms ease, - background 50ms ease; -} - -.adv-toggle-btn:hover { - color: var(--text-muted); -} - -.adv-toggle-on.active { - background: #3a8a5c; - color: var(--text-primary); -} - -.adv-toggle-off.active { - background: var(--bg-input); - color: #a33c3c; -} - .adv-pick-btn { font-family: "DMSans", sans-serif; font-size: 0.6875rem; @@ -481,34 +314,6 @@ input[type="number"]::-webkit-outer-spin-button { color: var(--text-primary); } -.adv-secondary-btn { - display: inline-flex; - align-items: center; - justify-content: center; - height: 30px; - box-sizing: border-box; - font-family: "DMSans", sans-serif; - font-size: 12px; - font-weight: var(--font-weight-hv); - padding: 0 8px; - line-height: 1; - background: transparent; - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - color: var(--text-muted); - cursor: pointer; - transition: - border-color 0.15s ease, - color 0.15s ease, - opacity 0.15s ease; -} - -.adv-secondary-btn:hover { - border-color: var(--text-dim); - color: var(--text-primary); -} - -.adv-secondary-btn:disabled, .adv-pick-btn:disabled { opacity: 0.6; cursor: progress; @@ -638,19 +443,6 @@ input[type="number"]::-webkit-outer-spin-button { user-select: none; } -.adv-sequence-empty { - font-family: "DMSans", sans-serif; - font-size: 12px; - color: var(--text-dim); - border: 1px dashed var(--border-subtle); - border-radius: var(--radius-sm); - padding: 10px 12px; - width: 100%; - box-sizing: border-box; - overflow-wrap: break-word; - word-break: break-word; -} - .adv-sequence-item { display: grid; grid-template-columns: @@ -676,6 +468,17 @@ input[type="number"]::-webkit-outer-spin-button { background-color 220ms ease; } +.adv-sequence-item::before { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 2px; + background: color-mix(in srgb, var(--border-subtle) 80%, transparent); + pointer-events: none; +} + .adv-sequence-leading { width: 28px; height: 24px; @@ -749,17 +552,6 @@ input[type="number"]::-webkit-outer-spin-button { box-shadow: 0 8px 22px rgba(0, 0, 0, 0.2); } -.adv-sequence-item::before { - content: ""; - position: absolute; - left: 0; - right: 0; - bottom: 0; - height: 2px; - background: color-mix(in srgb, var(--border-subtle) 80%, transparent); - pointer-events: none; -} - .adv-sequence-progress { content: ""; position: absolute; @@ -801,19 +593,6 @@ input[type="number"]::-webkit-outer-spin-button { z-index: 1; } -.adv-sequence-coord { - min-width: 0; - min-height: 26px; - padding: 0 0.35rem; -} - -.adv-sequence-coord .adv-number-sm { - height: 26px; - line-height: 26px; - box-sizing: border-box; - padding: 0; -} - .adv-sequence-position { width: calc(6ch + 0.75rem); } @@ -822,14 +601,6 @@ input[type="number"]::-webkit-outer-spin-button { min-width: 0; } -.adv-sequence-actions { - display: flex; - gap: 6px; - flex-wrap: wrap; - margin-left: auto; - margin-left: auto; -} - .adv-sequence-delete { width: 24px; height: 24px; @@ -862,131 +633,6 @@ input[type="number"]::-webkit-outer-spin-button { background: linear-gradient(to bottom, transparent 0%, var(--bg-base) 100%); } -/* Container for the disabled section */ -.adv-disabled-container { - position: relative; - transition: opacity 0.3s ease; -} - -.adv-disabled-content { - opacity: 0.35; - pointer-events: none; -} - -.adv-disabled-overlay { - position: absolute; - inset: -0.25rem; - display: flex; - align-items: center; - justify-content: center; - background: rgba(255, 255, 255, 0.01); - backdrop-filter: blur(0px); - border-radius: var(--radius-md); - opacity: 0; - pointer-events: none; - transition: all 0.2s ease; - z-index: 10; -} - -.adv-disabled-container:hover .adv-disabled-overlay { - opacity: 1; - backdrop-filter: blur(2px); - background: rgba(0, 0, 0, 0.1); - pointer-events: auto; -} - -.adv-disabled-label { - font-family: "DMSans", sans-serif; - font-size: 0.6875rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--text-muted); - background: var(--bg-surface); - padding: 0.25rem 0.625rem; - border-radius: 1.25rem; - border: 1px solid var(--border-subtle); - box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.2); -} - -.adv-toggle-btn.adv-disabled { - opacity: 0.5; - cursor: not-allowed; - pointer-events: none; -} - -.adv-number-sm { - font-family: "DMSans", sans-serif; - font-size: 0.75rem; - font-weight: var(--font-weight-hv); - color: var(--text-dim); - background: transparent; - border: none; - outline: none; -} - -.adv-info-icon { - position: relative; - display: inline-flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - cursor: help; - color: var(--text-dim); - transition: color 0.15s ease; - user-select: none; - z-index: 2000; - isolation: isolate; -} - -.adv-info-icon:hover { - color: var(--text-muted); -} - -.adv-info-tooltip { - visibility: hidden; - opacity: 0; - position: absolute; - bottom: calc(100% + 6px); - left: 0; - transform: none; - width: 220px; - padding: 6px 8px; - background: var(--bg-elevated); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - font-family: "DMSans", sans-serif; - font-size: 0.6875rem; - font-weight: var(--font-weight-li); - color: var(--text-muted); - line-height: 1.5; - letter-spacing: 0.02em; - text-align: left; - z-index: 3000; - pointer-events: none; - transition: - opacity 0.15s ease, - visibility 0.15s ease; - box-shadow: var(--container-shadow); -} - -.adv-info-icon:hover .adv-info-tooltip { - visibility: visible; - opacity: 1; -} - -.adv-info-tooltip--portal { - position: fixed; - visibility: visible; - opacity: 1; - bottom: auto; - z-index: 9999; -} - -/* - Container and Insides for selectors -*/ - .adv-value-outline { margin-left: auto; display: inline-flex; @@ -1049,77 +695,6 @@ input[type="number"]::-webkit-outer-spin-button { flex-shrink: 0; } -/* - Advanced Drop-Down -*/ -.adv-dropdown-menu { - position: absolute; - top: calc(100% + 4px); - left: 0; - z-index: 100; - background: var(--bg-elevated); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - box-shadow: var(--container-shadow); - overflow: hidden; - min-width: fit-content; -} - -.adv-dropdown { - position: relative; - display: inline-flex; - flex-shrink: 0; -} - .adv-foc-grow .adv-dropdown { width: 100%; } - -.adv-dropdown-trigger { - display: inline-flex; - align-items: center; - justify-content: space-between; - width: 100%; - min-height: calc(var(--adv-control-height) - 2px); - padding: 0.125rem 0.5rem 0.125rem 0.375rem; - background: transparent; - border: none; - color: var(--text-primary); - font-family: "DMSans", sans-serif; - font-size: 1rem; - font-weight: var(--font-weight-md); - cursor: pointer; - transition: color 0.15s ease; - gap: 2px; -} - -.adv-dropdown-trigger:hover { - color: var(--text-primary); -} - -.adv-dropdown-item { - display: block; - width: 100%; - padding: 0.375rem 0.75rem; - background: transparent; - border: none; - color: var(--text-dim); - font-family: "DMSans", sans-serif; - font-size: 1rem; - font-weight: var(--font-weight-md); - cursor: pointer; - text-align: left; - transition: - color 0.1s ease, - background 0.1s ease; -} - -.adv-dropdown-item:hover { - color: var(--text-primary); - background: var(--bg-input); -} - -.adv-dropdown-item.active { - color: var(--text-primary); - background: var(--bg-input); -} diff --git a/src/components/panels/advanced/AdvancedPanel.tsx b/src/components/panels/advanced/AdvancedPanel.tsx index f11e5358..05ad1852 100644 --- a/src/components/panels/advanced/AdvancedPanel.tsx +++ b/src/components/panels/advanced/AdvancedPanel.tsx @@ -1,3 +1,4 @@ +import "./sections/shared.css"; import "./AdvancedPanel.css"; import type { Settings } from "../../../store"; import CadenceSection from "./sections/CadenceSection"; diff --git a/src/components/panels/advanced/sections/shared.css b/src/components/panels/advanced/sections/shared.css new file mode 100644 index 00000000..49f553b5 --- /dev/null +++ b/src/components/panels/advanced/sections/shared.css @@ -0,0 +1,475 @@ +.adv-panel { + --adv-control-height: 32px; +} + +.adv-panel { + display: flex; + flex-direction: column; + height: 100%; +} + +.adv-row { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.adv-row--grow { + flex: 1; + overflow: hidden; + align-items: stretch; +} + +.adv-sectioncontainer { + background: var(--bg-surface); + border: 0px solid var(--border); + border-radius: 0.4375rem; + padding: 0.625rem 0.75rem; + margin-bottom: 0.625rem; + flex: 1; + box-shadow: var(--container-shadow); +} + +.adv-card-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; +} + +.adv-card-title { + font-family: "DMSans", sans-serif; + font-size: 0.875rem; + font-weight: var(--font-weight-hv); + color: var(--text-primary); + letter-spacing: -0.02em; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.adv-card-divider { + width: 100%; + border-radius: 1px; + height: 1px; + background: var(--border-subtle); + margin: 0.5rem 0; +} + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +.adv-numbox-sm { + display: flex; + align-items: center; + gap: 2px; + background: var(--bg-elevated); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + min-height: var(--adv-control-height); + padding: 0 0.5rem; +} + +.adv-number-sm { + font-family: "DMSans", sans-serif; + font-size: 0.875rem; + font-weight: var(--font-weight-hv); + color: var(--text-primary); +} + +.adv-unit { + font-family: "DMSans", sans-serif; + font-size: 0.625rem; + font-weight: var(--font-weight-hv); + letter-spacing: 0.05em; + color: var(--text-dim); + margin-left: 3px; +} + +.adv-secondary-btn { + display: inline-flex; + align-items: center; + justify-content: center; + height: 30px; + box-sizing: border-box; + font-family: "DMSans", sans-serif; + font-size: 12px; + font-weight: var(--font-weight-hv); + padding: 0 8px; + line-height: 1; + background: transparent; + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + color: var(--text-muted); + cursor: pointer; + transition: + border-color 0.15s ease, + color 0.15s ease, + opacity 0.15s ease; +} + +.adv-secondary-btn:hover { + border-color: var(--text-dim); + color: var(--text-primary); +} + +.adv-secondary-btn:disabled { + opacity: 0.6; + cursor: progress; +} + +.adv-seg-group { + display: flex; + background: var(--bg-elevated); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + min-height: var(--adv-control-height); + overflow: hidden; + transition-duration: 0.2s; + flex-shrink: 0; +} + +.adv-seg-group:hover { + border-color: var(--border-focus); +} + +.adv-seg-btn { + flex: 1; + min-width: 4.5rem; + height: calc(var(--adv-control-height) - 2px); + font-family: "DMSans", sans-serif; + font-size: 1rem; + font-weight: var(--font-weight-md); + line-height: 1.125rem; + padding: 0.25rem 0.75rem; + background: transparent; + border: none; + color: var(--text-dim); + cursor: pointer; + transition: + color 0.2s ease, + background 0.2s ease; +} + +.adv-seg-btn:hover { + color: var(--text-muted); +} + +.adv-seg-btn.active { + background: var(--bg-input); + color: var(--text-primary); +} + +.adv-seg-btn:focus, +.adv-seg-btn:focus-visible { + outline: none; + box-shadow: none; +} + +.adv-toggle-group { + display: flex; + background: var(--bg-elevated); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + min-height: var(--adv-control-height); + overflow: hidden; + flex-shrink: 0; +} + +.adv-toggle-group:hover { + border-color: var(--); +} + +.adv-toggle-btn { + font-family: "DMSans", sans-serif; + font-size: 0.75rem; + font-weight: 800; + letter-spacing: 4%; + text-transform: uppercase; + height: calc(var(--adv-control-height) - 2px); + padding: 0 0.5rem; + min-width: 2.1875rem; + text-align: center; + background: transparent; + border: none; + color: var(--text-dim); + cursor: pointer; + transition: + color 50ms ease, + background 50ms ease; +} + +.adv-toggle-btn:hover { + color: var(--text-muted); +} + +.adv-toggle-on.active { + background: #3a8a5c; + color: var(--text-primary); +} + +.adv-toggle-off.active { + background: var(--bg-input); + color: #a33c3c; +} + +.adv-toggle-btn.adv-disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} + +.adv-sequence-item { + display: grid; + grid-template-columns: + 28px minmax(0, auto) minmax(0, auto) minmax(0, auto) + auto; + gap: 6px; + align-items: center; + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + padding: 6px; + background: var(--bg-elevated); + width: 100%; + box-sizing: border-box; + flex: 0 0 auto; + min-height: fit-content; + position: relative; + overflow: hidden; + --sequence-step-duration: 1ms; + --sequence-step-clicks: 1; + transition: + border-color 220ms ease, + box-shadow 280ms ease, + background-color 220ms ease; +} + +.adv-sequence-item::before { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 2px; + background: color-mix(in srgb, var(--border-subtle) 80%, transparent); + pointer-events: none; +} + +.adv-sequence-empty { + font-family: "DMSans", sans-serif; + font-size: 12px; + color: var(--text-dim); + border: 1px dashed var(--border-subtle); + border-radius: var(--radius-sm); + padding: 10px 12px; + width: 100%; + box-sizing: border-box; + overflow-wrap: break-word; + word-break: break-word; +} + +.adv-sequence-coord { + min-width: 0; + min-height: 26px; + padding: 0 0.35rem; +} + +.adv-sequence-coord .adv-number-sm { + height: 26px; + line-height: 26px; + box-sizing: border-box; + padding: 0; +} + +.adv-sequence-actions { + display: flex; + gap: 6px; + flex-wrap: wrap; + margin-left: auto; +} + +.adv-disabled-container { + position: relative; + transition: opacity 0.3s ease; +} + +.adv-disabled-content { + opacity: 0.35; + pointer-events: none; +} + +.adv-disabled-overlay { + position: absolute; + inset: -0.25rem; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.01); + backdrop-filter: blur(0px); + border-radius: var(--radius-md); + opacity: 0; + pointer-events: none; + transition: all 0.2s ease; + z-index: 10; +} + +.adv-disabled-container:hover .adv-disabled-overlay { + opacity: 1; + backdrop-filter: blur(2px); + background: rgba(0, 0, 0, 0.1); + pointer-events: auto; +} + +.adv-disabled-label { + font-family: "DMSans", sans-serif; + font-size: 0.6875rem; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); + background: var(--bg-surface); + padding: 0.25rem 0.625rem; + border-radius: 1.25rem; + border: 1px solid var(--border-subtle); + box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.2); +} + +.adv-number-sm { + font-family: "DMSans", sans-serif; + font-size: 0.75rem; + font-weight: var(--font-weight-hv); + color: var(--text-dim); + background: transparent; + border: none; + outline: none; +} + +.adv-info-icon { + position: relative; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + cursor: help; + color: var(--text-dim); + transition: color 0.15s ease; + user-select: none; + z-index: 2000; + isolation: isolate; +} + +.adv-info-icon:hover { + color: var(--text-muted); +} + +.adv-info-tooltip { + visibility: hidden; + opacity: 0; + position: absolute; + bottom: calc(100% + 6px); + left: 0; + transform: none; + width: 220px; + padding: 6px 8px; + background: var(--bg-elevated); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + font-family: "DMSans", sans-serif; + font-size: 0.6875rem; + font-weight: var(--font-weight-li); + color: var(--text-muted); + line-height: 1.5; + letter-spacing: 0.02em; + text-align: left; + z-index: 3000; + pointer-events: none; + transition: + opacity 0.15s ease, + visibility 0.15s ease; + box-shadow: var(--container-shadow); +} + +.adv-info-icon:hover .adv-info-tooltip { + visibility: visible; + opacity: 1; +} + +.adv-info-tooltip--portal { + position: fixed; + visibility: visible; + opacity: 1; + bottom: auto; + z-index: 9999; +} + +.adv-dropdown-menu { + position: absolute; + top: calc(100% + 4px); + left: 0; + z-index: 100; + background: var(--bg-elevated); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-sm); + box-shadow: var(--container-shadow); + overflow: hidden; + min-width: fit-content; +} + +.adv-dropdown { + position: relative; + display: inline-flex; + flex-shrink: 0; +} + +.adv-dropdown-trigger { + display: inline-flex; + align-items: center; + justify-content: space-between; + width: 100%; + min-height: calc(var(--adv-control-height) - 2px); + padding: 0.125rem 0.5rem 0.125rem 0.375rem; + background: transparent; + border: none; + color: var(--text-primary); + font-family: "DMSans", sans-serif; + font-size: 1rem; + font-weight: var(--font-weight-md); + cursor: pointer; + transition: color 0.15s ease; + gap: 2px; +} + +.adv-dropdown-trigger:hover { + color: var(--text-primary); +} + +.adv-dropdown-item { + display: block; + width: 100%; + padding: 0.375rem 0.75rem; + background: transparent; + border: none; + color: var(--text-dim); + font-family: "DMSans", sans-serif; + font-size: 1rem; + font-weight: var(--font-weight-md); + cursor: pointer; + text-align: left; + transition: + color 0.1s ease, + background 0.1s ease; +} + +.adv-dropdown-item:hover { + color: var(--text-primary); + background: var(--bg-input); +} + +.adv-dropdown-item.active { + color: var(--text-primary); + background: var(--bg-input); +} diff --git a/src/components/panels/advanced/sections/shared.tsx b/src/components/panels/advanced/sections/shared.tsx index 81af6be1..f01ec3b2 100644 --- a/src/components/panels/advanced/sections/shared.tsx +++ b/src/components/panels/advanced/sections/shared.tsx @@ -16,6 +16,8 @@ import { createPortal } from "react-dom"; import { normalizeIntegerRaw } from "../../../../numberInput"; import UnavailableReason from "../../../UnavailableReason"; +import "./shared.css"; + // ToggleBtn ← These are here just for some visual space const ToggleBtn = memo( diff --git a/src/components/panels/zones/ZonesPanel.css b/src/components/panels/zones/ZonesPanel.css index f9742ea2..2e4a684d 100644 --- a/src/components/panels/zones/ZonesPanel.css +++ b/src/components/panels/zones/ZonesPanel.css @@ -1,3 +1,7 @@ +/* =============================== */ +/* Original zones panel styles */ +/* =============================== */ + .adv-zones-row { display: flex; gap: 0.625rem; @@ -424,449 +428,3 @@ .adv-proc-search:focus { border-color: var(--accent); } - -/* ============================= */ -/* Shared panel structure */ -/* ============================= */ - -.adv-panel { - --adv-control-height: 32px; -} - -.adv-panel { - display: flex; - flex-direction: column; - height: 100%; -} - -.adv-row { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.adv-row--grow { - flex: 1; - overflow: hidden; - align-items: stretch; -} - -/* ============================= */ -/* Card / section container */ -/* ============================= */ - -.adv-sectioncontainer { - background: var(--bg-surface); - border: 0px solid var(--border); - border-radius: 0.4375rem; - padding: 0.625rem 0.75rem; - margin-bottom: 0.625rem; - flex: 1; - box-shadow: var(--container-shadow); -} - -.adv-card-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.5rem; -} - -.adv-card-title { - font-family: "DMSans", sans-serif; - font-size: 0.875rem; - font-weight: var(--font-weight-hv); - color: var(--text-primary); - letter-spacing: -0.02em; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.adv-card-divider { - width: 100%; - border-radius: 1px; - height: 1px; - background: var(--border-subtle); - margin: 0.5rem 0; -} - -/* ============================= */ -/* Input controls */ -/* ============================= */ - -input[type="number"]::-webkit-inner-spin-button, -input[type="number"]::-webkit-outer-spin-button { - -webkit-appearance: none; - margin: 0; -} - -.adv-numbox-sm { - display: flex; - align-items: center; - gap: 2px; - background: var(--bg-elevated); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - min-height: var(--adv-control-height); - padding: 0 0.5rem; -} - -.adv-number-sm { - font-family: "DMSans", sans-serif; - font-size: 0.875rem; - font-weight: var(--font-weight-hv); - color: var(--text-primary); -} - -.adv-unit { - font-family: "DMSans", sans-serif; - font-size: 0.625rem; - font-weight: var(--font-weight-hv); - letter-spacing: 0.05em; - color: var(--text-dim); - margin-left: 3px; -} - -.adv-secondary-btn { - display: inline-flex; - align-items: center; - justify-content: center; - height: 30px; - box-sizing: border-box; - font-family: "DMSans", sans-serif; - font-size: 12px; - font-weight: var(--font-weight-hv); - padding: 0 8px; - line-height: 1; - background: transparent; - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - color: var(--text-muted); - cursor: pointer; - transition: - border-color 0.15s ease, - color 0.15s ease, - opacity 0.15s ease; -} - -.adv-secondary-btn:hover { - border-color: var(--text-dim); - color: var(--text-primary); -} - -.adv-secondary-btn:disabled, -.adv-pick-btn:disabled { - opacity: 0.6; - cursor: progress; -} - -/* ============================= */ -/* Segmented button group */ -/* ============================= */ - -.adv-seg-group { - display: flex; - background: var(--bg-elevated); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - min-height: var(--adv-control-height); - overflow: hidden; - transition-duration: 0.2s; - flex-shrink: 0; -} - -.adv-seg-group:hover { - border-color: var(--border-focus); -} - -.adv-seg-btn { - flex: 1; - min-width: 4.5rem; - height: calc(var(--adv-control-height) - 2px); - font-family: "DMSans", sans-serif; - font-size: 1rem; - font-weight: var(--font-weight-md); - line-height: 1.125rem; - padding: 0.25rem 0.75rem; - background: transparent; - border: none; - color: var(--text-dim); - cursor: pointer; - transition: - color 0.2s ease, - background 0.2s ease; -} - -.adv-seg-btn:hover { - color: var(--text-muted); -} - -.adv-seg-btn.active { - background: var(--bg-input); - color: var(--text-primary); -} - -.adv-seg-btn:focus, -.adv-seg-btn:focus-visible { - outline: none; - box-shadow: none; -} - -/* ============================= */ -/* Toggle buttons (On/Off) */ -/* ============================= */ - -.adv-toggle-group { - display: flex; - background: var(--bg-elevated); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - min-height: var(--adv-control-height); - overflow: hidden; - flex-shrink: 0; -} - -.adv-toggle-group:hover { - border-color: var(--); -} - -.adv-toggle-btn { - font-family: "DMSans", sans-serif; - font-size: 0.75rem; - font-weight: 800; - letter-spacing: 4%; - text-transform: uppercase; - height: calc(var(--adv-control-height) - 2px); - padding: 0 0.5rem; - min-width: 2.1875rem; - text-align: center; - background: transparent; - border: none; - color: var(--text-dim); - cursor: pointer; - transition: - color 50ms ease, - background 50ms ease; -} - -.adv-toggle-btn:hover { - color: var(--text-muted); -} - -.adv-toggle-on.active { - background: #3a8a5c; - color: var(--text-primary); -} - -.adv-toggle-off.active { - background: var(--bg-input); - color: #a33c3c; -} - -.adv-toggle-btn.adv-disabled { - opacity: 0.5; - cursor: not-allowed; - pointer-events: none; -} - -/* ============================= */ -/* Sequence item (used by proc */ -/* list) */ -/* ============================= */ - -.adv-sequence-item { - display: grid; - grid-template-columns: - 28px minmax(0, auto) minmax(0, auto) minmax(0, auto) - auto; - gap: 6px; - align-items: center; - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - padding: 6px; - background: var(--bg-elevated); - width: 100%; - box-sizing: border-box; - flex: 0 0 auto; - min-height: fit-content; - position: relative; - overflow: hidden; - --sequence-step-duration: 1ms; - --sequence-step-clicks: 1; - transition: - border-color 220ms ease, - box-shadow 280ms ease, - background-color 220ms ease; -} - -.adv-sequence-item::before { - content: ""; - position: absolute; - left: 0; - right: 0; - bottom: 0; - height: 2px; - background: color-mix(in srgb, var(--border-subtle) 80%, transparent); - pointer-events: none; -} - -.adv-sequence-empty { - font-family: "DMSans", sans-serif; - font-size: 12px; - color: var(--text-dim); - border: 1px dashed var(--border-subtle); - border-radius: var(--radius-sm); - padding: 10px 12px; - width: 100%; - box-sizing: border-box; - overflow-wrap: break-word; - word-break: break-word; -} - -.adv-sequence-coord { - min-width: 0; - min-height: 26px; - padding: 0 0.35rem; -} - -.adv-sequence-coord .adv-number-sm { - height: 26px; - line-height: 26px; - box-sizing: border-box; - padding: 0; -} - -.adv-sequence-actions { - display: flex; - gap: 6px; - flex-wrap: wrap; - margin-left: auto; -} - -/* ============================= */ -/* Disabled overlay */ -/* ============================= */ - -.adv-disabled-container { - position: relative; - transition: opacity 0.3s ease; -} - -.adv-disabled-content { - opacity: 0.35; - pointer-events: none; -} - -.adv-disabled-overlay { - position: absolute; - inset: -0.25rem; - display: flex; - align-items: center; - justify-content: center; - background: rgba(255, 255, 255, 0.01); - backdrop-filter: blur(0px); - border-radius: var(--radius-md); - opacity: 0; - pointer-events: none; - transition: all 0.2s ease; - z-index: 10; -} - -.adv-disabled-container:hover .adv-disabled-overlay { - opacity: 1; - backdrop-filter: blur(2px); - background: rgba(0, 0, 0, 0.1); - pointer-events: auto; -} - -.adv-disabled-label { - font-family: "DMSans", sans-serif; - font-size: 0.6875rem; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--text-muted); - background: var(--bg-surface); - padding: 0.25rem 0.625rem; - border-radius: 1.25rem; - border: 1px solid var(--border-subtle); - box-shadow: 0 0.25rem 0.75rem rgba(0, 0, 0, 0.2); -} - -/* ============================= */ -/* Number input (NumInput) */ -/* ============================= */ - -.adv-number-sm { - font-family: "DMSans", sans-serif; - font-size: 0.75rem; - font-weight: var(--font-weight-hv); - color: var(--text-dim); - background: transparent; - border: none; - outline: none; -} - -/* ============================= */ -/* Info icon / tooltip */ -/* ============================= */ - -.adv-info-icon { - position: relative; - display: inline-flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - cursor: help; - color: var(--text-dim); - transition: color 0.15s ease; - user-select: none; - z-index: 2000; - isolation: isolate; -} - -.adv-info-icon:hover { - color: var(--text-muted); -} - -.adv-info-tooltip { - visibility: hidden; - opacity: 0; - position: absolute; - bottom: calc(100% + 6px); - left: 0; - transform: none; - width: 220px; - padding: 6px 8px; - background: var(--bg-elevated); - border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); - font-family: "DMSans", sans-serif; - font-size: 0.6875rem; - font-weight: var(--font-weight-li); - color: var(--text-muted); - line-height: 1.5; - letter-spacing: 0.02em; - text-align: left; - z-index: 3000; - pointer-events: none; - transition: - opacity 0.15s ease, - visibility 0.15s ease; - box-shadow: var(--container-shadow); -} - -.adv-info-icon:hover .adv-info-tooltip { - visibility: visible; - opacity: 1; -} - -.adv-info-tooltip--portal { - position: fixed; - visibility: visible; - opacity: 1; - bottom: auto; - z-index: 9999; -} diff --git a/src/components/panels/zones/ZonesPanel.tsx b/src/components/panels/zones/ZonesPanel.tsx index e368bad0..e30f1c0b 100644 --- a/src/components/panels/zones/ZonesPanel.tsx +++ b/src/components/panels/zones/ZonesPanel.tsx @@ -1,3 +1,4 @@ +import "../advanced/sections/shared.css"; import "./ZonesPanel.css"; import type { Settings } from "../../../store"; import FailsafeSection from "./sections/FailsafeSection"; From 052db15aadf36f02c59c0e218bc8665ade0056ee Mon Sep 17 00:00:00 2001 From: Blur Date: Fri, 3 Jul 2026 01:25:20 +0200 Subject: [PATCH 16/45] Replaced font with RobotoFlex for support for Tabular Enums or however you spell that --- CHANGELOG.md | 1 + overlay.html | 9 ++++--- public/fonts/DMSans.ttf | Bin 238984 -> 0 bytes public/fonts/Manrope-VariableFont_wght.ttf | Bin 0 -> 167160 bytes public/fonts/RobotoFlex.ttf | Bin 0 -> 1684624 bytes src/assets/DMSans.ttf | Bin 238984 -> 0 bytes src/components/ConfirmDialog.css | 6 ++--- src/components/UnavailableReason.css | 2 +- src/components/Updatebanner.css | 10 ++++---- src/components/panels/SimplePanel.css | 2 +- .../panels/advanced/AdvancedPanel.css | 18 ++++++------- .../panels/advanced/sections/shared.css | 24 +++++++++--------- .../panels/settings/SettingsPanel.css | 2 -- src/components/panels/zones/ZonesPanel.css | 8 +++--- src/index.css | 9 ++++--- 15 files changed, 46 insertions(+), 45 deletions(-) delete mode 100644 public/fonts/DMSans.ttf create mode 100644 public/fonts/Manrope-VariableFont_wght.ttf create mode 100644 public/fonts/RobotoFlex.ttf delete mode 100644 src/assets/DMSans.ttf diff --git a/CHANGELOG.md b/CHANGELOG.md index a2b56786..d6bb6f3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # v3.9.0 - 02.07.2026 (d.m.y) ## Changed - Settings panel now has a side menu for easier navigation and organization of settings. +- Font switch from DM Sans to RobotoFlex to support tabular enums (e.g. numbers that don't shift when changing values). # Fixed - Fixed simple tab button in the title bar showing the wrong background color when a custom accent color is selected. diff --git a/overlay.html b/overlay.html index 793ee87f..001dd564 100644 --- a/overlay.html +++ b/overlay.html @@ -6,9 +6,9 @@ Overlay @@ -140,6 +159,8 @@ const stopZonePrompt = document.getElementById('stop-zone-prompt'); let pickModeActive = false; let stopZonePickModeActive = false; + let zoneFadeAnim = null; + let hasFadedIn = false; function setupTauriListener() { if (!window.__TAURI__ || !window.__TAURI__.event) { @@ -154,7 +175,10 @@ window.__TAURI__.event.listen('clear-zones', () => { zoneLayer.innerHTML = ''; - zoneLayer.classList.remove('fade-out'); + if (zoneFadeAnim) { + zoneFadeAnim.cancel(); + zoneFadeAnim = null; + } }); window.__TAURI__.event.listen('click-points-data', (event) => { @@ -186,14 +210,24 @@ function renderZones(data) { zoneLayer.innerHTML = ''; - zoneLayer.classList.remove('fade-out'); - void zoneLayer.offsetWidth; - if (!stopZonePickModeActive) { - zoneLayer.classList.add('fade-out'); + if (zoneFadeAnim) { + if (zoneFadeAnim.playState === 'finished') { + hasFadedIn = false; + } + zoneFadeAnim.cancel(); + zoneFadeAnim = null; } if (!data) return; + if (!stopZonePickModeActive) { + zoneFadeAnim = zoneLayer.animate([ + { opacity: 1 }, + { opacity: 1, offset: 0.5 }, + { opacity: 0 } + ], { duration: 3000, easing: 'ease', fill: 'forwards' }); + } + const sw = data.screenWidth; const sh = data.screenHeight; const monitors = Array.isArray(data.monitors) && data.monitors.length > 0 @@ -205,17 +239,27 @@ } if (Array.isArray(data.stopZones)) { - for (const zone of data.stopZones) { + const disabled = data.stopZonesEnabled === false; + data.stopZones.forEach((zone, index) => { let cls = 'zone-custom'; if (zone.action === 'pause') cls = 'zone-pause'; else if (zone.action === 'start') cls = 'zone-start'; - addZone(zone.x, zone.y, zone.width, zone.height, cls); - } + if (disabled) cls += ' zone-disabled'; + addZone(zone.x, zone.y, zone.width, zone.height, cls, String(index + 1)); + }); } for (const monitor of monitors) { renderMonitorZones(monitor, data); } + + if (!hasFadedIn) { + hasFadedIn = true; + zoneLayer.animate([ + { opacity: 0 }, + { opacity: 1 } + ], { duration: 250, easing: 'ease' }); + } } function renderMonitorZones(monitor, data) { @@ -281,7 +325,7 @@ } } - function addZone(x, y, w, h, cls) { + function addZone(x, y, w, h, cls, label) { const scale = window.devicePixelRatio || 1; x = x / scale; y = y / scale; @@ -295,6 +339,16 @@ div.style.top = y + 'px'; div.style.width = w + 'px'; div.style.height = h + 'px'; + if (label !== undefined) { + div.classList.add('zone--labeled'); + const span = document.createElement('span'); + span.className = 'zone-label'; + span.textContent = label; + const maxPx = 160; + const fitPx = Math.min(w, h) * 0.65 / (1 + Math.max(0, label.length - 1) * 0.2); + span.style.fontSize = Math.min(fitPx, maxPx) + 'px'; + div.appendChild(span); + } zoneLayer.appendChild(div); return div; } @@ -376,7 +430,10 @@ stopZonePickModeActive = active; stopZonePrompt.classList.toggle('active', active); if (active) { - zoneLayer.classList.remove('fade-out'); + if (zoneFadeAnim) { + zoneFadeAnim.cancel(); + zoneFadeAnim = null; + } } else { clearStopZonePreview(); } diff --git a/src-tauri/src/engine/worker.rs b/src-tauri/src/engine/worker.rs index 3b902b41..f00fcc67 100644 --- a/src-tauri/src/engine/worker.rs +++ b/src-tauri/src/engine/worker.rs @@ -232,6 +232,7 @@ pub fn stop_clicker_inner( state.active_click_point_index.store(-1, Ordering::SeqCst); state.active_click_point_tick.store(0, Ordering::SeqCst); state.zone_started_clicker.store(false, Ordering::SeqCst); + state.paused_by_zone.store(false, Ordering::SeqCst); if was_running { state.run_generation.fetch_add(1, Ordering::SeqCst); } @@ -815,33 +816,45 @@ pub fn start_clicker(config: ClickerConfig, control: RunControl) -> RunOutcome { let clicker_state = control.app.state::(); while control.is_active() { - if let Some(reason) = check_abort(&config, start_time) { + if config.limit > 0 && st.click_count >= config.limit as i64 { + st.stop_reason = format!("Click limit reached ({})", config.limit); + break; + } + + let zone_hit = if config.stop_zones_enabled && !config.stop_zones.is_empty() { + current_cursor_position().and_then(|cursor| detect_stop_zones(cursor, &config)) + } else { + None + }; + + if let Some((crate::engine::ZoneAction::Stop, _)) = zone_hit { + st.stop_reason = String::from("Custom stop zone failsafe"); + break; + } + + if let Some(reason) = should_stop_for_failsafe(&config) { st.stop_reason = reason; break; } - if config.limit > 0 && st.click_count >= config.limit as i64 { - st.stop_reason = format!("Click limit reached ({})", config.limit); + if config.task_switcher_stop_enabled && process::is_task_switcher_active() { + st.stop_reason = String::from("Blocked by Alt+Tab"); + break; + } + if config.process_list_enabled && process::check_process_list(&config).is_some() { + st.stop_reason = String::from("Blocked by process list"); + break; + } + if config.time_limit > 0.0 && start_time.elapsed().as_secs_f64() >= config.time_limit { + st.stop_reason = format!("Time limit reached ({:.1}s)", config.time_limit); break; } - // Zone pause/resume (check every frame) - if config.stop_zones_enabled && !config.stop_zones.is_empty() { - if let Some(cursor) = current_cursor_position() { - match detect_stop_zones(cursor, &config) { - Some((crate::engine::ZoneAction::Stop, _)) => { - st.stop_reason = String::from("Custom stop zone failsafe"); - break; - } - Some((crate::engine::ZoneAction::Pause, _)) => { - clicker_state.paused_by_zone.store(true, Ordering::SeqCst); - } - Some((crate::engine::ZoneAction::Start, _)) => { - clicker_state.paused_by_zone.store(false, Ordering::SeqCst); - } - None => { - clicker_state.paused_by_zone.store(false, Ordering::SeqCst); - } - } + match zone_hit { + Some((crate::engine::ZoneAction::Pause, _)) => { + clicker_state.paused_by_zone.store(true, Ordering::SeqCst); + } + _ => { + clicker_state.paused_by_zone.store(false, Ordering::SeqCst); } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1a6b0062..d9ecaaba 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -194,16 +194,22 @@ fn spawn_start_zone_monitor(app: &AppHandle) { ZONE_MONITOR_RUNNING.store(true, std::sync::atomic::Ordering::SeqCst); std::thread::spawn(move || { let mut prev_in_start_zone = false; + let mut prev_has_start_zones = false; while ZONE_MONITOR_RUNNING.load(std::sync::atomic::Ordering::SeqCst) { std::thread::sleep(std::time::Duration::from_millis(100)); let state = handle.state::(); - let settings = state.settings.lock().unwrap_or_else(poisoned_inner).clone(); - let has_start_zones = settings.stop_zones_enabled - && settings.stop_zones.iter().any(|z| z.action == "start"); + let (has_start_zones, zones) = { + let settings = state.settings.lock().unwrap_or_else(poisoned_inner); + let has = settings.stop_zones_enabled + && settings.stop_zones.iter().any(|z| z.action == "start"); + (has, settings.stop_zones.clone()) + }; + if !has_start_zones { prev_in_start_zone = false; + prev_has_start_zones = false; continue; } @@ -212,7 +218,7 @@ fn spawn_start_zone_monitor(app: &AppHandle) { None => continue, }; - let in_start_zone = settings.stop_zones.iter().any(|z| { + let in_start_zone = zones.iter().any(|z| { z.action == "start" && cursor.0 >= z.x && cursor.0 < z.x + z.width @@ -220,6 +226,11 @@ fn spawn_start_zone_monitor(app: &AppHandle) { && cursor.1 < z.y + z.height }); + if !prev_has_start_zones { + prev_in_start_zone = in_start_zone; + } + prev_has_start_zones = true; + let running = state.running.load(std::sync::atomic::Ordering::SeqCst); let zone_started = state .zone_started_clicker diff --git a/src-tauri/src/overlay.rs b/src-tauri/src/overlay.rs index 81ae0d68..c305a406 100644 --- a/src-tauri/src/overlay.rs +++ b/src-tauri/src/overlay.rs @@ -146,6 +146,7 @@ pub fn show_overlay(app: &AppHandle) -> AppResult<()> { "cornerStopBL": settings.corner_stop_bl, "cornerStopBR": settings.corner_stop_br, "stopZones": stop_zones_payload, + "stopZonesEnabled": settings.stop_zones_enabled, "screenWidth": bounds.width, "screenHeight": bounds.height, "monitors": monitor_payload, diff --git a/src/App.tsx b/src/App.tsx index 5e573c61..76be66b9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -72,10 +72,10 @@ type DropdownOverflowDetail = { function getPanelSize(tab: Tab, hasUpdate: boolean) { const extra = hasUpdate ? 30 : 0; if (tab === "simple") { - return { width: 716, height: 175 + extra }; + return { width: 750, height: 175 + extra }; } if (tab === "settings") return { width: 700, height: 720 + extra }; - if (tab === "zones") return { width: 750, height: 720 + extra }; + if (tab === "zones") return { width: 750, height: 700 + extra }; if (tab === "click-points") return { width: 550, height: 600 + extra }; return { width: 900, height: 469 + extra }; } diff --git a/src/components/panels/advanced/sections/shared.tsx b/src/components/panels/advanced/sections/shared.tsx index 2408a847..3cd06ad8 100644 --- a/src/components/panels/advanced/sections/shared.tsx +++ b/src/components/panels/advanced/sections/shared.tsx @@ -331,12 +331,14 @@ export function AdvDropdown({ value, options, onChange, + hoverWheel = true, allowWindowOverflow = false, windowOverflowBottom = 190, }: { value: string; options: readonly { value: string; label: string }[]; onChange: (value: string) => void; + hoverWheel?: boolean; allowWindowOverflow?: boolean; windowOverflowBottom?: number; }) { @@ -438,7 +440,7 @@ export function AdvDropdown({ }, [open]); const handleWheel = (e: WheelEvent) => { - if (open) return; + if (!hoverWheel || open) return; e.preventDefault(); const currentIndex = options.findIndex((o) => o.value === value); if (currentIndex === -1) return; diff --git a/src/components/panels/zones/ZonesPanel.css b/src/components/panels/zones/ZonesPanel.css index cd7c7692..42c2a15a 100644 --- a/src/components/panels/zones/ZonesPanel.css +++ b/src/components/panels/zones/ZonesPanel.css @@ -4,7 +4,7 @@ .adv-zones-row { display: flex; - gap: 0.625rem; + gap: 0.5rem; flex: none; align-items: stretch; min-height: 0; @@ -14,7 +14,7 @@ flex: 1; display: flex; flex-direction: column; - gap: 0.625rem; + gap: 0.5rem; min-width: 0; } @@ -35,9 +35,19 @@ } .adv-panel--zones { + flex: 1; overflow: hidden; } +.adv-panel--zones .adv-sectioncontainer { + box-shadow: none; + margin-bottom: 0.5rem; +} + +.adv-panel--zones > .adv-sectioncontainer:last-child { + margin-bottom: 0; +} + .adv-corner-grid { display: grid; grid-template-columns: 1fr 1fr; @@ -197,16 +207,17 @@ .adv-zone-row { display: grid; - grid-template-columns: 1.5rem 3.25rem 3.25rem 3.25rem 3.25rem 5rem auto 1.5rem; - gap: 0.3125rem; + grid-template-columns: 1.5rem 4rem 4rem 4rem 4rem 1fr 5rem 8rem 2rem; + gap: 0.25rem; align-items: center; border: 0.0625rem solid var(--border-subtle); border-radius: var(--radius-sm); - padding: 0.3125rem 0.375rem; + padding: 0.25rem 0.5rem; background: var(--bg-elevated); width: 100%; box-sizing: border-box; flex: 0 0 auto; + min-height: 2.5rem; } .adv-zone-index { @@ -217,24 +228,30 @@ text-align: center; width: 1.5rem; opacity: 1; + grid-column: 1; } .adv-zone-coord { gap: 0.125rem; - padding: 0 0.3rem; + padding: 0 0.25rem; min-width: 0; } .adv-zone-coord .adv-number-sm { - max-width: 4ch; - padding: 0; + max-width: 7ch; + width: 7ch; text-overflow: clip; } +.adv-zone-row .adv-zone-draw-btn { + grid-column: 8; + width: 100%; +} + .adv-zone-draw-btn { - height: 1.5rem; - padding: 0 0.375rem; - font-size: 0.7rem; + height: 2rem; + padding: 0 0.5rem; + font-size: 0.75rem; line-height: 1; white-space: nowrap; border: 0.0625rem solid var(--border-subtle); @@ -256,17 +273,60 @@ } .adv-zone-row .adv-numbox-sm { - height: 1.5rem; - min-height: 1.5rem; + height: 2rem; + min-height: 2rem; } .adv-zone-row .adv-number-sm { - height: 1.375rem; - line-height: 1.375rem; + height: 2rem; + line-height: 2rem; + width: 7ch !important; + text-align: right; + padding-right: 0.6rem; } .adv-zone-row .adv-dropdown { + grid-column: 7; + justify-self: end; min-width: 0; + width: 100%; +} + +.adv-zone-row .adv-dropdown-trigger { + height: 2rem; + min-height: 2rem; + border: 0.0625rem solid var(--border-subtle); + border-radius: var(--radius-sm); + background: var(--bg-elevated); + box-sizing: border-box; + padding: 0 0.5rem; +} + +.adv-zone-row .adv-dropdown-trigger:hover { + border-color: var(--accent-ring); +} + +.adv-zone-row .adv-click-points-delete { + grid-column: 9; + justify-self: end; + width: 2rem; + height: 2rem; + background: transparent; + border-radius: var(--radius-sm); + border: 0.0625rem solid var(--border-subtle); + color: var(--text-muted); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: + color 0.15s ease, + border-color 0.15s ease; +} + +.adv-zone-row .adv-click-points-delete:hover { + color: #e24d4d; + border-color: #e24d4d; } .adv-zones-empty { @@ -275,10 +335,110 @@ align-items: center; justify-content: center; gap: 0.375rem; + flex: 1; + min-height: 0; padding: 1.5rem 1rem; text-align: center; } +.adv-stop-zones-card { + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; +} + +.adv-stop-zones-card .adv-disabled-container { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; +} + +.adv-stop-zones-card .adv-disabled-container > :first-child { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; +} + +.adv-stop-zones-card .adv-disabled-content { + flex: 1; + min-height: 0; +} + +.adv-stop-zone-body { + display: flex; + flex-direction: column; + gap: 0.625rem; + align-items: stretch; + min-height: 0; + height: 100%; +} + +.adv-stop-zone-controls { + display: flex; + flex-direction: column; + gap: 0.5rem; + min-width: 0; + flex: 1; + width: 100%; + min-height: 0; +} + +.adv-stop-zone-list-shell { + position: relative; + width: 100%; + flex: 1 1 auto; + height: 0; + min-height: 0; +} + +.adv-stop-zone-list { + display: flex; + flex-direction: column; + position: relative; + z-index: 0; + width: 100%; + overflow-y: hidden; + min-height: 0; + height: 100%; + padding: 0 2px 0 0; + box-sizing: border-box; + scrollbar-gutter: stable; + animation: adv-cplist-enable 0.01s 350ms forwards; +} + +.adv-stop-zone-list::-webkit-scrollbar { + width: 0.375rem; + background: var(--bg-elevated); + border-radius: var(--radius-md); +} + +.adv-stop-zone-list::-webkit-scrollbar-thumb { + background: var(--text-dim); + border-radius: var(--radius-sm); +} + +.adv-stop-zone-list::-webkit-scrollbar-thumb:hover { + background: #777; +} + +.adv-stop-zone-list > * + * { + margin-top: 6px; +} + +.adv-stop-zone-list-fade { + position: absolute; + z-index: 2; + left: 0; + right: 0; + bottom: 0; + height: 1.75rem; + pointer-events: none; + background: linear-gradient(to bottom, transparent 0%, var(--bg-base) 100%); +} + .adv-zones-empty-icon { color: var(--text-dim); opacity: 0.5; diff --git a/src/components/panels/zones/ZonesPanel.tsx b/src/components/panels/zones/ZonesPanel.tsx index 707b4676..ffe73e43 100644 --- a/src/components/panels/zones/ZonesPanel.tsx +++ b/src/components/panels/zones/ZonesPanel.tsx @@ -2,7 +2,7 @@ import "../advanced/sections/shared.css"; import "./ZonesPanel.css"; import type { Settings } from "../../../store"; import FailsafeSection from "./sections/FailsafeSection"; -import StopZonesSection from "./sections/CustomStopZoneSection"; +import StopZonesSection from "./sections/StopZonesSection"; interface Props { settings: Settings; diff --git a/src/components/panels/zones/sections/CustomStopZoneSection.tsx b/src/components/panels/zones/sections/CustomStopZoneSection.tsx deleted file mode 100644 index e233a443..00000000 --- a/src/components/panels/zones/sections/CustomStopZoneSection.tsx +++ /dev/null @@ -1,367 +0,0 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import type { Settings, StopZone } from "../../../../store"; -import { error } from "@tauri-apps/plugin-log"; -import { invoke } from "@tauri-apps/api/core"; -import { listen } from "@tauri-apps/api/event"; -import { - NumInput, - AdvDropdown, - CardDivider, - Disableable, - ToggleBtn, -} from "../../advanced/sections/shared"; - -interface Props { - settings: Settings; - update: (patch: Partial) => void; - showInfo: boolean; -} - -interface StopZonePickedPayload { - x: number; - y: number; - width: number; - height: number; -} - -const ACTION_OPTIONS = [ - { value: "stop", label: "Stop" }, - { value: "pause", label: "Pause" }, - { value: "start", label: "Start" }, -] as const; - -function createZoneId(): string { - return ( - globalThis.crypto?.randomUUID?.() ?? - `sz-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` - ); -} - -export default function StopZonesSection({ settings, update }: Props) { - const [drawingIndex, setDrawingIndex] = useState(null); - const updateRef = useRef(update); - const zonesRef = useRef(settings.stopZones ?? []); - const pendingIndexRef = useRef(null); - - const zones = useMemo(() => settings.stopZones ?? [], [settings.stopZones]); - - useEffect(() => { - updateRef.current = update; - }, [update]); - - useEffect(() => { - zonesRef.current = zones; - }, [zones]); - - useEffect(() => { - let disposed = false; - let unlistenPicked: (() => void) | null = null; - let unlistenEnded: (() => void) | null = null; - - void listen("custom-stop-zone-picked", (event) => { - const idx = pendingIndexRef.current; - if (idx === null) return; - const current = [...(zonesRef.current ?? [])]; - if (idx < 0 || idx >= current.length) return; - current[idx] = { - ...current[idx], - x: event.payload.x, - y: event.payload.y, - width: Math.max(1, event.payload.width), - height: Math.max(1, event.payload.height), - }; - updateRef.current({ stopZones: current }); - pendingIndexRef.current = null; - setDrawingIndex(null); - }).then((cleanup) => { - if (disposed) { - cleanup(); - } else { - unlistenPicked = cleanup; - } - }); - - void listen("custom-stop-zone-pick-ended", () => { - pendingIndexRef.current = null; - setDrawingIndex(null); - }).then((cleanup) => { - if (disposed) { - cleanup(); - } else { - unlistenEnded = cleanup; - } - }); - - return () => { - disposed = true; - unlistenPicked?.(); - unlistenEnded?.(); - void invoke("cancel_custom_stop_zone_pick"); - }; - }, []); - - const startDraw = useCallback(async (index: number) => { - pendingIndexRef.current = index; - setDrawingIndex(index); - try { - await invoke("start_custom_stop_zone_pick"); - } catch (err) { - pendingIndexRef.current = null; - setDrawingIndex(null); - error( - JSON.stringify({ - source: "StopZonesSection.startDraw", - error: String(err), - }), - ); - } - }, []); - - const cancelDraw = useCallback(async () => { - pendingIndexRef.current = null; - setDrawingIndex(null); - try { - await invoke("cancel_custom_stop_zone_pick"); - } catch (err) { - error( - JSON.stringify({ - source: "StopZonesSection.cancelDraw", - error: String(err), - }), - ); - } - }, []); - - useEffect(() => { - if (drawingIndex === null) return; - const handleKeyDown = (event: KeyboardEvent) => { - if (event.key !== "Escape") return; - event.preventDefault(); - event.stopPropagation(); - void cancelDraw(); - }; - window.addEventListener("keydown", handleKeyDown, true); - return () => window.removeEventListener("keydown", handleKeyDown, true); - }, [cancelDraw, drawingIndex]); - - const updateZone = (index: number, patch: Partial) => { - const next = zones.map((z, i) => (i === index ? { ...z, ...patch } : z)); - update({ stopZones: next }); - }; - - const removeZone = (index: number) => { - const next = zones.filter((_, i) => i !== index); - update({ stopZones: next }); - }; - - const addZone = () => { - const next = [ - ...zones, - { - id: createZoneId(), - x: 0, - y: 0, - width: 100, - height: 100, - action: "stop" as const, - }, - ]; - update({ stopZones: next }); - }; - - return ( -
-
- Custom Stop Zones - { - if (drawingIndex !== null && !v) { - void cancelDraw(); - } - update({ stopZonesEnabled: v }); - }} - /> -
- -
- Draw zones on screen. The clicker stops, pauses, or starts whenever your - cursor moves inside a zone. -
- - {zones.length === 0 ? ( -
- -
-
- - - - - -
-
No zones yet
-
- Click Add Zone above, then click{" "} - Draw on a row to define its area on screen. -
-
-
- ) : ( -
- {zones.map((zone, index) => ( -
- {index + 1} - - - - -
- - updateZone(index, { - action: v as "stop" | "pause" | "start", - }) - } - /> -
- - -
- ))} -
- -
-
- )} -
-
- ); -} diff --git a/src/components/panels/zones/sections/StopZonesSection.tsx b/src/components/panels/zones/sections/StopZonesSection.tsx new file mode 100644 index 00000000..c6f4271e --- /dev/null +++ b/src/components/panels/zones/sections/StopZonesSection.tsx @@ -0,0 +1,392 @@ +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import type { Settings, StopZone } from "../../../../store"; +import { createStopZoneId } from "../../../../settingsSchema"; +import { error } from "@tauri-apps/plugin-log"; +import { invoke } from "@tauri-apps/api/core"; +import { listen } from "@tauri-apps/api/event"; +import { + NumInput, + AdvDropdown, + CardDivider, + Disableable, + ToggleBtn, +} from "../../advanced/sections/shared"; + +interface Props { + settings: Settings; + update: (patch: Partial) => void; + showInfo: boolean; +} + +interface StopZonePickedPayload { + x: number; + y: number; + width: number; + height: number; +} + +const ACTION_OPTIONS = [ + { value: "stop", label: "Stop" }, + { value: "pause", label: "Pause" }, + { value: "start", label: "Start" }, +] as const; + +export default function StopZonesSection({ settings, update }: Props) { + const [drawingIndex, setDrawingIndex] = useState(null); + const [showBottomFade, setShowBottomFade] = useState(false); + const updateRef = useRef(update); + const zonesRef = useRef(settings.stopZones ?? []); + const pendingIndexRef = useRef(null); + const listViewportRef = useRef(null); + + const zones = useMemo(() => settings.stopZones ?? [], [settings.stopZones]); + + useEffect(() => { + updateRef.current = update; + }, [update]); + + useEffect(() => { + zonesRef.current = zones; + }, [zones]); + + useEffect(() => { + let disposed = false; + let unlistenPicked: (() => void) | null = null; + let unlistenEnded: (() => void) | null = null; + + void listen("custom-stop-zone-picked", (event) => { + const idx = pendingIndexRef.current; + if (idx === null) return; + const current = [...(zonesRef.current ?? [])]; + if (idx < 0 || idx >= current.length) return; + current[idx] = { + ...current[idx], + x: event.payload.x, + y: event.payload.y, + width: Math.max(1, event.payload.width), + height: Math.max(1, event.payload.height), + }; + updateRef.current({ stopZones: current }); + pendingIndexRef.current = null; + setDrawingIndex(null); + }).then((cleanup) => { + if (disposed) { + cleanup(); + } else { + unlistenPicked = cleanup; + } + }); + + void listen("custom-stop-zone-pick-ended", () => { + pendingIndexRef.current = null; + setDrawingIndex(null); + }).then((cleanup) => { + if (disposed) { + cleanup(); + } else { + unlistenEnded = cleanup; + } + }); + + return () => { + disposed = true; + unlistenPicked?.(); + unlistenEnded?.(); + void invoke("cancel_custom_stop_zone_pick"); + }; + }, []); + + const startDraw = useCallback(async (index: number) => { + pendingIndexRef.current = index; + setDrawingIndex(index); + try { + await invoke("start_custom_stop_zone_pick"); + } catch (err) { + pendingIndexRef.current = null; + setDrawingIndex(null); + error( + JSON.stringify({ + source: "StopZonesSection.startDraw", + error: String(err), + }), + ); + } + }, []); + + const cancelDraw = useCallback(async () => { + pendingIndexRef.current = null; + setDrawingIndex(null); + try { + await invoke("cancel_custom_stop_zone_pick"); + } catch (err) { + error( + JSON.stringify({ + source: "StopZonesSection.cancelDraw", + error: String(err), + }), + ); + } + }, []); + + const updateBottomFade = useCallback(() => { + const viewport = listViewportRef.current; + if (!viewport) { + setShowBottomFade(false); + return; + } + const hasOverflow = viewport.scrollHeight - viewport.clientHeight > 6; + const hasMoreBelow = + hasOverflow && + viewport.scrollHeight - viewport.scrollTop - viewport.clientHeight > 6; + setShowBottomFade(hasMoreBelow); + }, []); + + useEffect(() => { + const viewport = listViewportRef.current; + if (!viewport) return; + + const handleScroll = () => { + updateBottomFade(); + }; + + viewport.addEventListener("scroll", handleScroll, { passive: true }); + const resizeObserver = new ResizeObserver(() => { + updateBottomFade(); + }); + resizeObserver.observe(viewport); + + return () => { + viewport.removeEventListener("scroll", handleScroll); + resizeObserver.disconnect(); + }; + }, [updateBottomFade]); + + useEffect(() => { + const frame = window.requestAnimationFrame(() => { + updateBottomFade(); + }); + return () => window.cancelAnimationFrame(frame); + }, [zones, updateBottomFade]); + + useEffect(() => { + if (drawingIndex === null) return; + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key !== "Escape") return; + event.preventDefault(); + event.stopPropagation(); + void cancelDraw(); + }; + window.addEventListener("keydown", handleKeyDown, true); + return () => window.removeEventListener("keydown", handleKeyDown, true); + }, [cancelDraw, drawingIndex]); + + const updateZone = (index: number, patch: Partial) => { + const next = zones.map((z, i) => (i === index ? { ...z, ...patch } : z)); + update({ stopZones: next }); + }; + + const removeZone = (index: number) => { + const next = zones.filter((_, i) => i !== index); + update({ stopZones: next }); + }; + + const addZone = () => { + const next = [ + ...zones, + { + id: createStopZoneId(), + x: 0, + y: 0, + width: 100, + height: 100, + action: "stop" as const, + }, + ]; + update({ stopZones: next }); + }; + + return ( +
+
+ Custom Stop Zones + { + if (drawingIndex !== null && !v) { + void cancelDraw(); + } + update({ stopZonesEnabled: v }); + }} + /> +
+ +
+ Draw zones on screen. The clicker stops, pauses, or starts whenever your + cursor moves inside a zone. +
+ +
+
+ {zones.length > 0 && ( +
+ +
+ )} +
+
+ {zones.length === 0 ? ( +
+ +
+ + + + + +
+
No zones yet
+
+ Click Add Zone above, then click{" "} + Draw on a row to define its area on + screen. +
+
+ ) : ( + zones.map((zone, index) => ( +
+ {index + 1} + + + + + + updateZone(index, { + action: v as "stop" | "pause" | "start", + }) + } + /> + + +
+ )) + )} +
+ {showBottomFade ? ( +
+ ) : null} +
+
+
+ +
+ ); +} diff --git a/src/settingsSchema.ts b/src/settingsSchema.ts index 2d02c3b4..69920a3c 100644 --- a/src/settingsSchema.ts +++ b/src/settingsSchema.ts @@ -107,7 +107,7 @@ function createClickPointId(): string { ); } -function createStopZoneId(): string { +export function createStopZoneId(): string { return ( globalThis.crypto?.randomUUID?.() ?? `sz-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` @@ -616,8 +616,6 @@ const FIELD_LIMITS = { export const SETTINGS_LIMITS = { ...FIELD_LIMITS, stopBoundary: PRESET_FIELDS.cornerStopTL.limit, - position: { min: 0 }, - stopZoneDimension: { min: 1 }, clickPointClicks: { min: 1, max: 100000 }, }; From c937917d4c1a706503fad2cecb3ae23f69fb7c63 Mon Sep 17 00:00:00 2001 From: Blur Date: Fri, 17 Jul 2026 21:01:26 +0200 Subject: [PATCH 35/45] adjusted click points spacing --- .../panels/advanced/sections/shared.css | 15 ++----- .../panels/click-points/ClickPointRow.tsx | 6 +-- .../panels/click-points/ClickPointsPanel.css | 40 ++++++++++++------- 3 files changed, 33 insertions(+), 28 deletions(-) diff --git a/src/components/panels/advanced/sections/shared.css b/src/components/panels/advanced/sections/shared.css index 40036e31..f0c4843b 100644 --- a/src/components/panels/advanced/sections/shared.css +++ b/src/components/panels/advanced/sections/shared.css @@ -243,24 +243,17 @@ input[type="number"]::-webkit-outer-spin-button { .adv-click-points-coord { min-width: 0; - min-height: 26px; - padding: 0 0.35rem; + min-height: 2rem; + padding: 0 0.5rem; } .adv-click-points-coord .adv-number-sm { - height: 26px; - line-height: 26px; + height: 2rem; + line-height: 2rem; box-sizing: border-box; padding: 0; } -.adv-click-points-actions { - display: flex; - gap: 6px; - flex-wrap: wrap; - margin-left: auto; -} - .adv-click-points-item { border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); diff --git a/src/components/panels/click-points/ClickPointRow.tsx b/src/components/panels/click-points/ClickPointRow.tsx index 69820ebf..cc138df5 100644 --- a/src/components/panels/click-points/ClickPointRow.tsx +++ b/src/components/panels/click-points/ClickPointRow.tsx @@ -83,7 +83,7 @@ export default function ClickPointRow({
-
- {onOffOptions.map((option) => ( - - ))} +
+
@@ -151,18 +142,17 @@ export default function BehaviorSection({ Stop clicking when switching to another window.
-
- {onOffOptions.map((option) => ( - - ))} +
+
@@ -173,18 +163,17 @@ export default function BehaviorSection({ Allow click speeds up to 1000 CPS (may affect performance).
-
- {onOffOptions.map((option) => ( - - ))} +
+
@@ -251,16 +240,15 @@ export default function BehaviorSection({ Minimize to the system tray instead of the taskbar.
-
- {onOffOptions.map((option) => ( - - ))} +
+
@@ -271,28 +259,26 @@ export default function BehaviorSection({ Start clicking when the app opens.
-
- {onOffOptions.map((option) => ( - - ))} +
+
diff --git a/src/components/panels/settings/sections/ProcessListSection.tsx b/src/components/panels/settings/sections/ProcessListSection.tsx index db42e350..8a70dc7f 100644 --- a/src/components/panels/settings/sections/ProcessListSection.tsx +++ b/src/components/panels/settings/sections/ProcessListSection.tsx @@ -106,19 +106,15 @@ export default function ProcessListSection({ settings, update }: Props) { Stop clicking based on the active application.
-
- {[ - { value: false, label: "Off" }, - { value: true, label: "On" }, - ].map((option) => ( - - ))} +
+
From c887b48fcdfd8ba268b11f773362647d5d3c6298 Mon Sep 17 00:00:00 2001 From: Blur Date: Sat, 18 Jul 2026 08:59:50 +0200 Subject: [PATCH 39/45] Enhance UI with animations for tab icons and settings panel elements --- CHANGELOG.md | 1 + src/components/TitleBar.css | 134 ++++++++++++ src/components/TitleBar.tsx | 71 ++++-- .../panels/settings/SettingsPanel.css | 145 +++++++++++++ .../panels/settings/SettingsPanel.tsx | 204 ++++++++++++------ 5 files changed, 467 insertions(+), 88 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f2754a7..83b50c5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - Overhauled Custom Stop Zones. - Zones overlay now shows a different color for disabled state of zones - Replaced On/Off toggles with switches in the settings panel and gave them an animation cuz im fancy like that. +- Added animations to some ui elements and icons ## Fixed - Fixed alt menu opening when the autoclicker is launched while tabbed into another app. - Fixed simple tab button in the title bar showing the wrong background color when a custom accent color is selected. diff --git a/src/components/TitleBar.css b/src/components/TitleBar.css index 8448e5c4..d83be192 100644 --- a/src/components/TitleBar.css +++ b/src/components/TitleBar.css @@ -125,6 +125,123 @@ display: block; } +.tab-icon-btn--animate svg { + pointer-events: none; +} + +/* Simple - inner bar slides to the right edge */ +.tab-icon-btn--animate[data-tab="simple"] .simple-bar { + animation: nav-simple-bar 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) 1 forwards; +} +@keyframes nav-simple-bar { + 0% { + transform: translateX(0); + } + 40% { + transform: translateX(2.5px); + } + 70% { + transform: translateX(-0.5px); + } + 100% { + transform: translateX(0); + } +} + +/* Advanced - layers stagger apart */ +.tab-icon-btn--animate[data-tab="advanced"] .adv-layer1 { + animation: nav-adv-l1 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) 1 forwards; +} +.tab-icon-btn--animate[data-tab="advanced"] .adv-layer2 { + animation: nav-adv-l2 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) 1 forwards; +} +.tab-icon-btn--animate[data-tab="advanced"] .adv-layer3 { + animation: nav-adv-l3 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) 1 forwards; +} +@keyframes nav-adv-l1 { + 0% { + transform: translate(0, 0); + } + 40% { + transform: translate(2px, -1.5px); + } + 70% { + transform: translate(-0.5px, 0.5px); + } + 100% { + transform: translate(0, 0); + } +} +@keyframes nav-adv-l2 { + 0% { + transform: translateX(0); + } + 40% { + transform: translateX(-2px); + } + 70% { + transform: translateX(0.5px); + } + 100% { + transform: translateX(0); + } +} +@keyframes nav-adv-l3 { + 0% { + transform: translate(0, 0); + } + 40% { + transform: translate(2px, 1.5px); + } + 70% { + transform: translate(-0.5px, -0.5px); + } + 100% { + transform: translate(0, 0); + } +} + +/* Zones - small size bounce */ +.tab-icon-btn--animate[data-tab="zones"] svg { + animation: nav-zones-bounce 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) 1 forwards; +} +@keyframes nav-zones-bounce { + 0% { + transform: scale(1); + } + 40% { + transform: scale(1.15); + } + 70% { + transform: scale(0.95); + } + 100% { + transform: scale(1); + } +} + +/* Click Points - lines wave, dots bounce */ +.tab-icon-btn--animate[data-tab="click-points"] .cp-dot1 { + animation: nav-cp-dot 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) 1 forwards; +} +.tab-icon-btn--animate[data-tab="click-points"] .cp-dot2 { + animation: nav-cp-dot 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) 0.12s 1 forwards; +} +@keyframes nav-cp-dot { + 0% { + transform: translateY(0); + } + 40% { + transform: translateY(-3px); + } + 70% { + transform: translateY(1px); + } + 100% { + transform: translateY(0); + } +} + .window-btn { /* Layout */ width: 2rem; @@ -164,6 +281,23 @@ background: var(--accent-soft); } +.pin-body { + transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); +} + +.window-btn.active .pin-body { + transform: translateY(1px); +} + +.pin-needle { + transform: translate(12px, 17px) scaleY(0); + transition: transform 0.35s cubic-bezier(0.34, 1.8, 0.64, 1); +} + +.window-btn.active .pin-needle { + transform: translate(12px, 17px) scaleY(1); +} + /* -- Title Horizontal Reveal -- */ .title-flipper { diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx index 0ce35fb5..2a1e76e5 100644 --- a/src/components/TitleBar.tsx +++ b/src/components/TitleBar.tsx @@ -109,7 +109,7 @@ const SimpleIcon = memo(function SimpleIcon({ active }: TabIconProps) { aria-hidden="true" > - + ); }); @@ -127,9 +127,9 @@ const AdvancedIcon = memo(function AdvancedIcon({ active }: TabIconProps) { strokeLinejoin="round" aria-hidden="true" > - - - + + + ); }); @@ -167,11 +167,25 @@ const ClickPointsIcon = memo(function ClickPointsIcon({ strokeLinejoin="round" aria-hidden="true" > - - - - - + + + + + ); }); @@ -323,18 +337,21 @@ const TitleBar = memo(function TitleBar({ } label={ - - - + + + + } /> @@ -500,13 +517,33 @@ const TabIconButton = memo(function TabIconButton({ activeBg: string; activeFocusRing: string; }) { + const btnRef = useRef(null); + + const handleMouseEnter = () => { + if (Math.random() < 0.1) { + const el = btnRef.current; + if (!el) return; + el.classList.remove("tab-icon-btn--animate"); + void el.offsetWidth; + el.classList.add("tab-icon-btn--animate"); + } + }; + + const handleAnimationEnd = () => { + btnRef.current?.classList.remove("tab-icon-btn--animate"); + }; + return (
Date: Sat, 18 Jul 2026 09:03:44 +0200 Subject: [PATCH 40/45] Bump version to 3.9.0 and update changelog --- CHANGELOG.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- src-tauri/Cargo.lock | 2 +- src-tauri/Cargo.toml | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83b50c5a..c8744ccf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# v3.9.0 - 02.07.2026 (d.m.y) +# v3.9.0 - 18.07.2026 (d.m.y) ## New - Added Hold mode in duty cycle since people couldn't figure out that 100% is the same as Holding down the mouse continuously. - Added slider for window opacity in the appearance settings. diff --git a/package-lock.json b/package-lock.json index 4aa536d8..0cd59e66 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "blur-autoclicker", - "version": "3.8.3", + "version": "3.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "blur-autoclicker", - "version": "3.8.3", + "version": "3.9.0", "dependencies": { "@tauri-apps/api": "^2.10.1", "@tauri-apps/cli": "^2.10.1", diff --git a/package.json b/package.json index bed8e2a5..1f94394a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "blur-autoclicker", "private": true, - "version": "3.8.3", + "version": "3.9.0", "type": "module", "scripts": { "dev": "tauri dev", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4612c3e0..c8e5c18c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4,7 +4,7 @@ version = 3 [[package]] name = "BlurAutoClicker" -version = "3.8.3" +version = "3.9.0" dependencies = [ "base64 0.22.1", "chrono", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7b3b698b..d936d9b9 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "BlurAutoClicker" -version = "3.8.3" +version = "3.9.0" description = "BlurAutoClicker - accuracy and performance focused auto clicker for Windows." authors = ["Blur009"] license = "GPL-3.0" From 468968dbef895b6d07ca80eac02ff6b10b4bed1f Mon Sep 17 00:00:00 2001 From: Blur Date: Sat, 18 Jul 2026 09:25:59 +0200 Subject: [PATCH 41/45] Add 'Remember Window Position' toggle and related functionality --- CHANGELOG.md | 2 + src-tauri/capabilities/default.json | 2 + src-tauri/gen/schemas/capabilities.json | 2 +- src/App.tsx | 40 ++++++++++++++----- .../settings/sections/BehaviorSection.tsx | 21 ++++++++++ src/settingsSchema.ts | 23 ++++++++++- 6 files changed, 77 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c8744ccf..a3672598 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Added the ability to have multiple custom stop zones. - Added a radius to the click points that randomizes the click position within that radius. - Added Task-bar icon customization in the appearance settings. The Icon now follows the theme color, and accent color on activation. You can toggle all of these options in the settings. +- Added Remember window position toggle (Settings → Behaviour → Startup) On by default. ## Changed - Overhauled Advanced panel. - Settings panel now has a side menu for easier navigation and organization of settings. @@ -27,6 +28,7 @@ - Fixed simple tab button in the title bar showing the wrong background color when a custom accent color is selected. - Fixed click points picker overlay persisting when the click points are toggled off while the overlay is open. - Fixed click speed not being reached at high CPS. Now it turns out that i fixed it somehow on accident, but im just going to say that i fixed it on purpose because i am a genius and i am very smart and very good at programming. +- Window position and size appearing in the wrong location on launch for about 1 second, should now just appear normally. # v3.8.3 - 28.06.2026 (d.m.y) ## Changed diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 0fb57bc5..52a28544 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -11,6 +11,8 @@ "core:window:allow-close", "core:window:allow-minimize", "core:window:allow-center", + "core:window:allow-outer-position", + "core:window:allow-set-position", "core:window:allow-inner-size", "core:window:allow-set-always-on-top", "core:window:allow-set-size", diff --git a/src-tauri/gen/schemas/capabilities.json b/src-tauri/gen/schemas/capabilities.json index 8fceed36..fcfa9edb 100644 --- a/src-tauri/gen/schemas/capabilities.json +++ b/src-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{"default":{"identifier":"default","description":"enables the default permissions","local":true,"windows":["main","overlay"],"permissions":["core:default","core:event:default","core:event:allow-listen","core:window:allow-close","core:window:allow-minimize","core:window:allow-center","core:window:allow-inner-size","core:window:allow-set-always-on-top","core:window:allow-set-size","core:window:allow-start-dragging","core:window:allow-hide","core:window:allow-show","core:window:allow-set-ignore-cursor-events","core:window:allow-set-icon","store:default","store:allow-get","store:allow-load","store:allow-save","store:allow-set","opener:default","process:default","updater:default","global-shortcut:allow-register","global-shortcut:allow-is-registered","dialog:default","dialog:allow-open","log:default"]}} \ No newline at end of file +{"default":{"identifier":"default","description":"enables the default permissions","local":true,"windows":["main","overlay"],"permissions":["core:default","core:event:default","core:event:allow-listen","core:window:allow-close","core:window:allow-minimize","core:window:allow-center","core:window:allow-outer-position","core:window:allow-set-position","core:window:allow-inner-size","core:window:allow-set-always-on-top","core:window:allow-set-size","core:window:allow-start-dragging","core:window:allow-hide","core:window:allow-show","core:window:allow-set-ignore-cursor-events","core:window:allow-set-icon","store:default","store:allow-get","store:allow-load","store:allow-save","store:allow-set","opener:default","process:default","updater:default","global-shortcut:allow-register","global-shortcut:allow-is-registered","dialog:default","dialog:allow-open","log:default"]}} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index ac97741f..5692300a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,7 @@ import { currentMonitor, getCurrentWindow, LogicalSize, + PhysicalPosition, } from "@tauri-apps/api/window"; import { lazy, @@ -197,6 +198,7 @@ export default function App() { const committedSettingsRef = useRef(DEFAULT_SETTINGS); const lastValidHotkeyRef = useRef(DEFAULT_SETTINGS.hotkey); const launchWindowPlacementDone = useRef(false); + const pendingShowWindowRef = useRef(false); const saveTimerRef = useRef | null>(null); const resizeTimeout = useRef | null>(null); const toggleTimerRef = useRef | null>(null); @@ -361,10 +363,30 @@ export default function App() { ); const applyStartupWindowPlacement = async () => { - await getCurrentWindow().center(); + const pos = committedSettingsRef.current.windowPosition; + if ( + committedSettingsRef.current.rememberWindowPosition && + pos.x !== null && + pos.y !== null + ) { + await getCurrentWindow().setPosition(new PhysicalPosition(pos.x, pos.y)); + } else { + await getCurrentWindow().center(); + } }; const handleWindowClose = async () => { + if (committedSettingsRef.current.rememberWindowPosition) { + const pos = await getCurrentWindow().outerPosition(); + const next = sanitizeSettings( + { + ...committedSettingsRef.current, + windowPosition: { x: pos.x, y: pos.y }, + }, + APP_VERSION, + ); + await saveSettings(next); + } if (uiSettingsRef.current.minimizeToTray) { await invoke("hide_main_window"); } else { @@ -640,9 +662,7 @@ export default function App() { await saveSettings(hydratedSettings); } - if (!autostartLaunch) { - await getCurrentWindow().show(); - } + pendingShowWindowRef.current = !autostartLaunch; emit("frontend-ready", {}).catch((err) => error( JSON.stringify({ @@ -657,13 +677,7 @@ export default function App() { error(JSON.stringify({ source: "App.boot", error: String(err) })); if (!mounted) return; setSettingsLoaded(true); - getCurrentWindow() - .show() - .catch((err) => - error( - JSON.stringify({ source: "App.bootShow", error: String(err) }), - ), - ); + pendingShowWindowRef.current = true; emit("frontend-ready", {}).catch((err) => error(JSON.stringify({ source: "App.bootEmit", error: String(err) })), ); @@ -768,6 +782,10 @@ export default function App() { await wait(30); if (cancelled) return; await applyStartupWindowPlacement(); + if (pendingShowWindowRef.current) { + await appWindow.show(); + pendingShowWindowRef.current = false; + } launchWindowPlacementDone.current = true; return; } diff --git a/src/components/panels/settings/sections/BehaviorSection.tsx b/src/components/panels/settings/sections/BehaviorSection.tsx index efb73f4c..1bb0b92b 100644 --- a/src/components/panels/settings/sections/BehaviorSection.tsx +++ b/src/components/panels/settings/sections/BehaviorSection.tsx @@ -252,6 +252,27 @@ export default function BehaviorSection({
+
+
+ Remember Window Position + + Open the window at its last position on startup. + +
+
+ +
+
+
Run on Startup diff --git a/src/settingsSchema.ts b/src/settingsSchema.ts index a8d4b39e..5c4bfb27 100644 --- a/src/settingsSchema.ts +++ b/src/settingsSchema.ts @@ -358,6 +358,13 @@ const SETTINGS_ONLY_FIELDS = { default: false, ui: { section: "startup", control: "toggle" }, }, + rememberWindowPosition: { + default: true, + ui: { section: "startup", control: "toggle" }, + }, + windowPosition: { + default: { x: null, y: null } as { x: number | null; y: number | null }, + }, theme: { default: "dark" as Theme, ui: { section: "appearance", control: "select" }, @@ -659,7 +666,7 @@ export const SETTINGS_UI_SCHEMA = [ }, { id: "startup", - fields: ["minimizeToTray"], + fields: ["minimizeToTray", "rememberWindowPosition"], }, { id: "appearance", @@ -1148,6 +1155,20 @@ export function sanitizeSettings( settingsOnly.stopZones = sanitizeStopZones(rawStopZones); } + const rawWindowPos = savedRecord.windowPosition; + if ( + rawWindowPos && + typeof rawWindowPos === "object" && + "x" in (rawWindowPos as Record) && + "y" in (rawWindowPos as Record) + ) { + const r = rawWindowPos as Record; + settingsOnly.windowPosition = { + x: typeof r.x === "number" && Number.isFinite(r.x) ? r.x : null, + y: typeof r.y === "number" && Number.isFinite(r.y) ? r.y : null, + }; + } + // Migration: old customStopZoneEnabled + coords → stopZones[0] if (settingsOnly.stopZones.length === 0) { const oldEnabled = savedRecord.customStopZoneEnabled as boolean | undefined; From 3099a489744273267aff9cccfa68b4517fe5508f Mon Sep 17 00:00:00 2001 From: Blur Date: Sat, 18 Jul 2026 09:43:56 +0200 Subject: [PATCH 42/45] crude presets system --- package-lock.json | 10 + package.json | 1 + src-tauri/capabilities/default.json | 3 + src-tauri/gen/schemas/capabilities.json | 2 +- src/App.tsx | 212 ++++++++++++++---- src/components/TitleBar.css | 16 ++ src/components/TitleBar.tsx | 5 + .../panels/settings/SettingsPanel.css | 38 ++++ .../panels/settings/SettingsPanel.tsx | 9 + .../settings/sections/PresetsSection.tsx | 148 +++++++++++- src/settingsSchema.ts | 158 ++++++++++++- 11 files changed, 544 insertions(+), 58 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0cd59e66..771999d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@tauri-apps/api": "^2.10.1", "@tauri-apps/cli": "^2.10.1", "@tauri-apps/plugin-dialog": "^2.7.1", + "@tauri-apps/plugin-fs": "^2.5.1", "@tauri-apps/plugin-log": "^2.8.0", "@tauri-apps/plugin-opener": "^2.5.2", "@tauri-apps/plugin-process": "^2.3.1", @@ -1342,6 +1343,15 @@ "@tauri-apps/api": "^2.11.0" } }, + "node_modules/@tauri-apps/plugin-fs": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-fs/-/plugin-fs-2.5.1.tgz", + "integrity": "sha512-9Lz+Jopp6QyeEWhlpkMx4R/+P9HgR+AVAI4vOZhlT8Xaymtz8iVI/Ov984/XTqgJz/5gz5NretqPB/XEMS3NhQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } + }, "node_modules/@tauri-apps/plugin-log": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-log/-/plugin-log-2.8.0.tgz", diff --git a/package.json b/package.json index 1f94394a..bf8bffe5 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@tauri-apps/api": "^2.10.1", "@tauri-apps/cli": "^2.10.1", "@tauri-apps/plugin-dialog": "^2.7.1", + "@tauri-apps/plugin-fs": "^2.5.1", "@tauri-apps/plugin-log": "^2.8.0", "@tauri-apps/plugin-opener": "^2.5.2", "@tauri-apps/plugin-process": "^2.3.1", diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 52a28544..5e510f5c 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -37,6 +37,9 @@ "dialog:default", "dialog:allow-open", + "dialog:allow-save", + + "fs:default", "log:default" ] diff --git a/src-tauri/gen/schemas/capabilities.json b/src-tauri/gen/schemas/capabilities.json index fcfa9edb..4bc31bde 100644 --- a/src-tauri/gen/schemas/capabilities.json +++ b/src-tauri/gen/schemas/capabilities.json @@ -1 +1 @@ -{"default":{"identifier":"default","description":"enables the default permissions","local":true,"windows":["main","overlay"],"permissions":["core:default","core:event:default","core:event:allow-listen","core:window:allow-close","core:window:allow-minimize","core:window:allow-center","core:window:allow-outer-position","core:window:allow-set-position","core:window:allow-inner-size","core:window:allow-set-always-on-top","core:window:allow-set-size","core:window:allow-start-dragging","core:window:allow-hide","core:window:allow-show","core:window:allow-set-ignore-cursor-events","core:window:allow-set-icon","store:default","store:allow-get","store:allow-load","store:allow-save","store:allow-set","opener:default","process:default","updater:default","global-shortcut:allow-register","global-shortcut:allow-is-registered","dialog:default","dialog:allow-open","log:default"]}} \ No newline at end of file +{"default":{"identifier":"default","description":"enables the default permissions","local":true,"windows":["main","overlay"],"permissions":["core:default","core:event:default","core:event:allow-listen","core:window:allow-close","core:window:allow-minimize","core:window:allow-center","core:window:allow-outer-position","core:window:allow-set-position","core:window:allow-inner-size","core:window:allow-set-always-on-top","core:window:allow-set-size","core:window:allow-start-dragging","core:window:allow-hide","core:window:allow-show","core:window:allow-set-ignore-cursor-events","core:window:allow-set-icon","store:default","store:allow-get","store:allow-load","store:allow-save","store:allow-set","opener:default","process:default","updater:default","global-shortcut:allow-register","global-shortcut:allow-is-registered","dialog:default","dialog:allow-open","dialog:allow-save","fs:default","log:default"]}} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 5692300a..3daeee6a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,13 +24,17 @@ import { } from "./hotkeys"; import { + applyPresetSnapshot, buildPresetSnapshot, createPresetDefinition, - MAX_PRESETS, + sanitizePresetSnapshot, sanitizeSettings, sanitizePresetName, + type PresetDefinition, type PresetId, } from "./settingsSchema"; +import { save, open } from "@tauri-apps/plugin-dialog"; +import { writeTextFile, readTextFile } from "@tauri-apps/plugin-fs"; import { APP_VERSION, DEFAULT_SETTINGS, @@ -61,9 +65,6 @@ export type Tab = "simple" | "advanced" | "zones" | "settings" | "click-points"; const BACKEND_SETTINGS_SCHEMA_VERSION = 10; const MAX_DROPDOWN_OVERFLOW_BOTTOM = 220; -const OPERATIONAL_SETTING_KEYS = new Set( - Object.keys(buildPresetSnapshot(DEFAULT_SETTINGS)), -); type DropdownOverflowDetail = { active: boolean; @@ -123,10 +124,6 @@ const DEFAULT_APP_INFO: AppInfo = { screenshotProtectionSupported: false, }; -type UpdateSettingsOptions = { - preserveActivePreset?: boolean; -}; - async function syncSettingsToBackend(settings: Settings) { await invoke("update_settings", { settings: { @@ -319,29 +316,16 @@ export default function App() { }); const updateSettings = useCallback( - (patch: Partial, options: UpdateSettingsOptions = {}) => { + (patch: Partial) => { const { hotkey, ...rest } = patch; - const shouldClearActivePreset = - !options.preserveActivePreset && - (hotkey !== undefined || - Object.keys(rest).some((key) => OPERATIONAL_SETTING_KEYS.has(key))); - - const restPatch: Partial = { ...rest }; - if ( - shouldClearActivePreset && - patch.activePresetId === undefined && - committedSettingsRef.current.activePresetId !== null - ) { - restPatch.activePresetId = null; - } - if (Object.keys(restPatch).length > 0) { + if (Object.keys(rest).length > 0) { const nextUiSettings = sanitizeSettings( - { ...uiSettingsRef.current, ...restPatch }, + { ...uiSettingsRef.current, ...rest }, APP_VERSION, ); const nextCommittedSettings = sanitizeSettings( - { ...committedSettingsRef.current, ...restPatch }, + { ...committedSettingsRef.current, ...rest }, APP_VERSION, ); @@ -399,12 +383,9 @@ export default function App() { try { await getCurrentWindow().setAlwaysOnTop(nextValue); - updateSettings( - { - alwaysOnTop: nextValue, - }, - { preserveActivePreset: true }, - ); + updateSettings({ + alwaysOnTop: nextValue, + }); } catch (err) { error(JSON.stringify({ source: "App.alwaysOnTop", error: String(err) })); } @@ -415,10 +396,6 @@ export default function App() { return false; } - if (committedSettingsRef.current.presets.length >= MAX_PRESETS) { - return false; - } - const preset = createPresetDefinition(name, committedSettingsRef.current); if (!preset.name) { return false; @@ -452,13 +429,10 @@ export default function App() { return false; } - updateSettings( - { - ...preset.settings, - activePresetId: presetId, - }, - { preserveActivePreset: true }, - ); + updateSettings({ + ...preset.settings, + activePresetId: presetId, + }); return true; }; @@ -575,6 +549,152 @@ export default function App() { return true; }; + const handleDuplicatePreset = (presetId: PresetId) => { + if (status.running) { + return false; + } + + const source = committedSettingsRef.current.presets.find( + (p) => p.id === presetId, + ); + if (!source) { + return false; + } + + const baseName = source.name.replace(/\s*\(\d+\)$/, ""); + let newName = `${baseName} (2)`; + let counter = 2; + while ( + committedSettingsRef.current.presets.some((p) => p.name === newName) + ) { + counter++; + newName = `${baseName} (${counter})`; + } + + const preset = createPresetDefinition( + newName, + applyPresetSnapshot(committedSettingsRef.current, source.settings), + ); + if (!preset.name) { + return false; + } + + const nextPresets = [...committedSettingsRef.current.presets, preset]; + const nextCommittedSettings = { + ...committedSettingsRef.current, + presets: nextPresets, + }; + const nextUiSettings = { + ...uiSettingsRef.current, + presets: nextPresets, + }; + + persistCommittedSettings(nextCommittedSettings, nextUiSettings); + return true; + }; + + const handleExportPreset = async (presetId: PresetId) => { + const preset = committedSettingsRef.current.presets.find( + (p) => p.id === presetId, + ); + if (!preset) { + return false; + } + + try { + const filePath = await save({ + defaultPath: `${preset.name.replace(/[^a-zA-Z0-9_-]/g, "_")}.json`, + filters: [{ name: "Preset JSON", extensions: ["json"] }], + }); + if (!filePath) { + return false; + } + + const data = JSON.stringify( + { type: "blur-autoclicker-preset", version: 1, preset }, + null, + 2, + ); + await writeTextFile(filePath, data); + return true; + } catch { + return false; + } + }; + + const handleImportPreset = async () => { + try { + const filePath = await open({ + multiple: false, + filters: [{ name: "Preset JSON", extensions: ["json"] }], + }); + if (!filePath) { + return null; + } + + const content = await readTextFile(filePath as string); + const parsed = JSON.parse(content) as { + type?: string; + version?: number; + preset?: PresetDefinition; + }; + + if (parsed.type !== "blur-autoclicker-preset" || !parsed.preset) { + return null; + } + + const importName = sanitizePresetName(parsed.preset.name); + if (!importName) { + return null; + } + + let finalName = importName; + let counter = 1; + while ( + committedSettingsRef.current.presets.some((p) => p.name === finalName) + ) { + counter++; + finalName = `${importName} (${counter})`; + } + + const now = new Date().toISOString(); + const id = + globalThis.crypto?.randomUUID?.() ?? + `preset-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + + const defaultSnapshot = buildPresetSnapshot( + committedSettingsRef.current, + ); + const sanitizedSettings = sanitizePresetSnapshot( + parsed.preset.settings, + defaultSnapshot, + ); + + const preset: PresetDefinition = { + id, + name: finalName, + createdAt: now, + updatedAt: now, + settings: sanitizedSettings, + }; + + const nextPresets = [...committedSettingsRef.current.presets, preset]; + const nextCommittedSettings = { + ...committedSettingsRef.current, + presets: nextPresets, + }; + const nextUiSettings = { + ...uiSettingsRef.current, + presets: nextPresets, + }; + + persistCommittedSettings(nextCommittedSettings, nextUiSettings); + return true; + } catch { + return null; + } + }; + useEffect(() => { let mounted = true; mountedRef.current = true; @@ -1230,6 +1350,10 @@ export default function App() { return () => window.removeEventListener("keydown", handleKeyDown); }, []); + const activePreset = settings.presets.find( + (p) => p.id === settings.activePresetId, + ); + return (
{updateInfo && ( Promise; onRequestClose: () => Promise; warning?: string | null; + activePresetName?: string | null; } type NavTab = Exclude; @@ -236,6 +237,7 @@ const TitleBar = memo(function TitleBar({ onToggleAlwaysOnTop, onRequestClose, warning, + activePresetName, }: Props) { const setTabRef = useRef(setTab); useEffect(() => { @@ -305,6 +307,9 @@ const TitleBar = memo(function TitleBar({ ); })}
+ {activePresetName && ( + {activePresetName} + )}
diff --git a/src/components/panels/settings/SettingsPanel.css b/src/components/panels/settings/SettingsPanel.css index bcf6d2c3..87cb47b1 100644 --- a/src/components/panels/settings/SettingsPanel.css +++ b/src/components/panels/settings/SettingsPanel.css @@ -742,6 +742,44 @@ align-items: flex-start; } +.preset-summary { + font-size: 0.6875rem; + color: var(--text-dim); + line-height: 1.4; + margin-top: 0.125rem; +} + +.preset-badge--modified { + background: transparent; + border: 1px solid var(--accent-yellow, #febc2e); + color: var(--accent-yellow, #febc2e); +} + +.preset-toolbar { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.preset-toolbar-btn { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.3125rem 0.625rem; + font-size: 0.8125rem; +} + +.preset-count { + font-size: 0.75rem; + color: var(--text-dim); +} + +.preset-rename-error { + font-size: 0.6875rem; + color: var(--accent-red); + margin-top: 0.375rem; +} + /* -- Social Links -- */ .social-links { diff --git a/src/components/panels/settings/SettingsPanel.tsx b/src/components/panels/settings/SettingsPanel.tsx index ca05515a..9c48617b 100644 --- a/src/components/panels/settings/SettingsPanel.tsx +++ b/src/components/panels/settings/SettingsPanel.tsx @@ -30,6 +30,9 @@ interface Props { onUpdatePreset: (presetId: PresetId) => boolean; onRenamePreset: (presetId: PresetId, name: string) => boolean; onDeletePreset: (presetId: PresetId) => boolean; + onDuplicatePreset: (presetId: PresetId) => boolean; + onExportPreset: (presetId: PresetId) => Promise; + onImportPreset: () => Promise; onToggleAlwaysOnTop: () => Promise; onReset: () => Promise; updateCheckStatus: @@ -51,6 +54,9 @@ export default function SettingsPanel({ onUpdatePreset, onRenamePreset, onDeletePreset, + onDuplicatePreset, + onExportPreset, + onImportPreset, onToggleAlwaysOnTop, onReset, updateCheckStatus, @@ -304,6 +310,9 @@ export default function SettingsPanel({ onUpdatePreset={onUpdatePreset} onRenamePreset={onRenamePreset} onDeletePreset={onDeletePreset} + onDuplicatePreset={onDuplicatePreset} + onExportPreset={onExportPreset} + onImportPreset={onImportPreset} /> )} {activeTab === "maintenance" && ( diff --git a/src/components/panels/settings/sections/PresetsSection.tsx b/src/components/panels/settings/sections/PresetsSection.tsx index caecaad2..7768d4cf 100644 --- a/src/components/panels/settings/sections/PresetsSection.tsx +++ b/src/components/panels/settings/sections/PresetsSection.tsx @@ -1,7 +1,8 @@ import { useEffect, useRef, useState } from "react"; import { - MAX_PRESETS, PRESET_NAME_MAX_LENGTH, + buildPresetSnapshot, + getPresetSummary, } from "../../../../settingsSchema"; import type { PresetDefinition, PresetId, Settings } from "../../../../store"; import { SettingsCard } from "./shared"; @@ -9,10 +10,12 @@ import { SettingsCard } from "./shared"; function PresetRow({ preset, isActive, + isModified, isEditing, isConfirmingDelete, running, renameDraft, + renameError, onRenameDraftChange, onStartRename, onCancelRename, @@ -22,13 +25,17 @@ function PresetRow({ onRequestDelete, onCancelDelete, onConfirmDelete, + onDuplicate, + onExport, }: { preset: PresetDefinition; isActive: boolean; + isModified: boolean; isEditing: boolean; isConfirmingDelete: boolean; running: boolean; renameDraft: string; + renameError: string; onRenameDraftChange: (value: string) => void; onStartRename: () => void; onCancelRename: () => void; @@ -38,6 +45,8 @@ function PresetRow({ onRequestDelete: () => void; onCancelDelete: () => void; onConfirmDelete: () => void; + onDuplicate: () => void; + onExport: () => void; }) { return (
{preset.name} )}
- {isActive && ( + {isActive && !isModified && ( Active )} + {isActive && isModified && ( + + Modified + + )} {new Date(preset.updatedAt).toLocaleDateString()}
+
+ {getPresetSummary(preset.settings)} +
{isEditing ? ( @@ -126,6 +143,13 @@ function PresetRow({ > Rename + + )}
+ {isEditing && renameError && ( +
{renameError}
+ )}
); } @@ -149,6 +196,9 @@ interface Props { onUpdatePreset: (presetId: PresetId) => boolean; onRenamePreset: (presetId: PresetId, name: string) => boolean; onDeletePreset: (presetId: PresetId) => boolean; + onDuplicatePreset: (presetId: PresetId) => boolean; + onExportPreset: (presetId: PresetId) => Promise; + onImportPreset: () => Promise; } export default function PresetsSection({ @@ -159,20 +209,33 @@ export default function PresetsSection({ onUpdatePreset, onRenamePreset, onDeletePreset, + onDuplicatePreset, + onExportPreset, + onImportPreset, }: Props) { const [newPresetName, setNewPresetName] = useState(""); const [editingPresetId, setEditingPresetId] = useState(null); const [renameDraft, setRenameDraft] = useState(""); + const [renameError, setRenameError] = useState(""); const [confirmingDeleteId, setConfirmingDeleteId] = useState( null, ); const [presetsAtBottom, setPresetsAtBottom] = useState(true); const presetsListRef = useRef(null); - const presetLimitReached = settings.presets.length >= MAX_PRESETS; const activeEditingPresetId = running ? null : editingPresetId; const activeConfirmingDeleteId = running ? null : confirmingDeleteId; + const activePreset = + settings.activePresetId + ? settings.presets.find((p) => p.id === settings.activePresetId) ?? null + : null; + + const currentSnapshot = buildPresetSnapshot(settings); + const isActiveModified = + activePreset !== null && + JSON.stringify(activePreset.settings) !== JSON.stringify(currentSnapshot); + useEffect(() => { if (!confirmingDeleteId) return; @@ -214,11 +277,30 @@ export default function PresetsSection({ setConfirmingDeleteId(null); setEditingPresetId(preset.id); setRenameDraft(preset.name); + setRenameError(""); }; const handleCommitRename = () => { if (!editingPresetId) return; - if (onRenamePreset(editingPresetId, renameDraft)) { + + const trimmed = renameDraft.trim(); + if (!trimmed) { + setRenameError("Name cannot be empty"); + return; + } + + const duplicate = settings.presets.some( + (p) => + p.id !== editingPresetId && + p.name.toLowerCase() === trimmed.toLowerCase(), + ); + if (duplicate) { + setRenameError("A preset with this name already exists"); + return; + } + + setRenameError(""); + if (onRenamePreset(editingPresetId, trimmed)) { setEditingPresetId(null); setRenameDraft(""); } @@ -227,11 +309,13 @@ export default function PresetsSection({ const handleCancelRename = () => { setEditingPresetId(null); setRenameDraft(""); + setRenameError(""); }; const handleRequestDelete = (presetId: PresetId) => { setEditingPresetId(null); setRenameDraft(""); + setRenameError(""); setConfirmingDeleteId(presetId); }; @@ -241,6 +325,10 @@ export default function PresetsSection({ } }; + const handleImport = async () => { + await onImportPreset(); + }; + return (
@@ -260,7 +348,7 @@ export default function PresetsSection({ onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); - if (!running && !presetLimitReached && newPresetName.trim()) { + if (!running && newPresetName.trim()) { handleSavePreset(); } } @@ -274,16 +362,40 @@ export default function PresetsSection({
- {presetLimitReached && ( - Max 6 presets allowed - )} +
+ + {settings.presets.length > 0 && ( + + {settings.presets.length} preset + {settings.presets.length !== 1 ? "s" : ""} + + )} +
{running && ( Disabled while clicking )} @@ -299,6 +411,10 @@ export default function PresetsSection({ key={preset.id} preset={preset} isActive={settings.activePresetId === preset.id} + isModified={ + settings.activePresetId === preset.id && + isActiveModified + } isEditing={activeEditingPresetId === preset.id} isConfirmingDelete={activeConfirmingDeleteId === preset.id} running={running} @@ -307,6 +423,9 @@ export default function PresetsSection({ ? renameDraft : preset.name } + renameError={ + activeEditingPresetId === preset.id ? renameError : "" + } onRenameDraftChange={setRenameDraft} onStartRename={() => handleStartRename(preset)} onCancelRename={handleCancelRename} @@ -322,6 +441,13 @@ export default function PresetsSection({ onRequestDelete={() => handleRequestDelete(preset.id)} onCancelDelete={() => setConfirmingDeleteId(null)} onConfirmDelete={() => handleConfirmDelete(preset.id)} + onDuplicate={() => { + setConfirmingDeleteId(null); + onDuplicatePreset(preset.id); + }} + onExport={() => { + void onExportPreset(preset.id); + }} /> ))}
diff --git a/src/settingsSchema.ts b/src/settingsSchema.ts index 5c4bfb27..1e6d0c8b 100644 --- a/src/settingsSchema.ts +++ b/src/settingsSchema.ts @@ -36,7 +36,6 @@ export interface StopZone { } export const DEFAULT_ACCENT_COLOR = "#22c55e"; -export const MAX_PRESETS = 20; export const PRESET_NAME_MAX_LENGTH = 40; export const DEFAULT_MAX_CLICK_SPEED = 500; export const EXTENDED_MAX_CLICK_SPEED = 1000; @@ -636,6 +635,138 @@ export type Settings = PresetFieldValues & version: string; }; +export const FACTORY_PRESETS: PresetDefinition[] = [ + { + id: "factory-standard", + name: "Standard Clicking", + createdAt: "2025-01-01T00:00:00.000Z", + updatedAt: "2025-01-01T00:00:00.000Z", + settings: { + clickSpeed: 25, + clickInterval: "s", + inputType: "mouse", + keyboardKey: "", + keyboardKeyCase: "lower", + mouseButton: "Left", + mode: "Toggle", + dutyCycleMode: "Click", + dutyCycleEnabled: true, + dutyCycle: 45, + speedRandomizationEnabled: true, + speedRandomization: 35, + doubleClickEnabled: false, + clickLimitEnabled: false, + clickLimit: 1000, + timeLimitEnabled: false, + timeLimit: 60, + timeLimitUnit: "s", + cornerStopEnabled: true, + cornerStopTL: 50, + cornerStopTR: 50, + cornerStopBL: 50, + cornerStopBR: 50, + edgeStopEnabled: true, + edgeStopTop: 40, + edgeStopBottom: 40, + edgeStopLeft: 40, + edgeStopRight: 40, + clickPointsEnabled: false, + stopZonesEnabled: false, + stopWhenComplete: false, + clickPoints: [], + processListEnabled: false, + processListMode: "whitelist", + processListEntries: [], + }, + }, + { + id: "factory-rapid", + name: "Rapid Fire", + createdAt: "2025-01-01T00:00:00.000Z", + updatedAt: "2025-01-01T00:00:00.000Z", + settings: { + clickSpeed: 100, + clickInterval: "s", + inputType: "mouse", + keyboardKey: "", + keyboardKeyCase: "lower", + mouseButton: "Left", + mode: "Toggle", + dutyCycleMode: "Click", + dutyCycleEnabled: true, + dutyCycle: 20, + speedRandomizationEnabled: true, + speedRandomization: 15, + doubleClickEnabled: false, + clickLimitEnabled: true, + clickLimit: 5000, + timeLimitEnabled: false, + timeLimit: 60, + timeLimitUnit: "s", + cornerStopEnabled: true, + cornerStopTL: 50, + cornerStopTR: 50, + cornerStopBL: 50, + cornerStopBR: 50, + edgeStopEnabled: true, + edgeStopTop: 40, + edgeStopBottom: 40, + edgeStopLeft: 40, + edgeStopRight: 40, + clickPointsEnabled: false, + stopZonesEnabled: false, + stopWhenComplete: false, + clickPoints: [], + processListEnabled: false, + processListMode: "whitelist", + processListEntries: [], + }, + }, + { + id: "factory-precision", + name: "Precision Mode", + createdAt: "2025-01-01T00:00:00.000Z", + updatedAt: "2025-01-01T00:00:00.000Z", + settings: { + clickSpeed: 10, + clickInterval: "s", + inputType: "mouse", + keyboardKey: "", + keyboardKeyCase: "lower", + mouseButton: "Left", + mode: "Toggle", + dutyCycleMode: "Click", + dutyCycleEnabled: true, + dutyCycle: 60, + speedRandomizationEnabled: true, + speedRandomization: 10, + doubleClickEnabled: false, + clickLimitEnabled: false, + clickLimit: 1000, + timeLimitEnabled: false, + timeLimit: 60, + timeLimitUnit: "s", + cornerStopEnabled: true, + cornerStopTL: 50, + cornerStopTR: 50, + cornerStopBL: 50, + cornerStopBR: 50, + edgeStopEnabled: true, + edgeStopTop: 40, + edgeStopBottom: 40, + edgeStopLeft: 40, + edgeStopRight: 40, + clickPointsEnabled: false, + stopZonesEnabled: false, + stopWhenComplete: false, + clickPoints: [], + processListEnabled: false, + processListMode: "whitelist", + processListEntries: [], + }, + }, +]; + export const PRESET_SNAPSHOT_KEYS = Object.keys(PRESET_FIELDS) as ReadonlyArray< keyof PresetSnapshot >; @@ -960,7 +1091,27 @@ export function createPresetDefinition( }; } -function sanitizePresetSnapshot( +export function getPresetSummary(snapshot: PresetSnapshot): string { + const parts: string[] = []; + + const speedStr = `${snapshot.clickSpeed}/${snapshot.clickInterval}`; + const inputStr = + snapshot.inputType === "keyboard" && snapshot.keyboardKey + ? `${snapshot.keyboardKey}` + : snapshot.mouseButton; + parts.push(`${speedStr} ${inputStr}`); + + if (snapshot.clickLimitEnabled) + parts.push(`Limit:${snapshot.clickLimit.toLocaleString()}`); + if (snapshot.timeLimitEnabled) + parts.push(`Time:${snapshot.timeLimit}${snapshot.timeLimitUnit}`); + if (snapshot.clickPoints.length > 0) + parts.push(`${snapshot.clickPoints.length} pts`); + + return parts.join(" | "); +} + +export function sanitizePresetSnapshot( input: unknown, defaults: PresetSnapshot, ): PresetSnapshot { @@ -1016,13 +1167,12 @@ function sanitizePresets( defaults: Settings, ): PresetDefinition[] { if (!Array.isArray(input)) { - return []; + return FACTORY_PRESETS; } const defaultSnapshot = buildPresetSnapshot(defaults); return input - .slice(0, MAX_PRESETS) .map((preset, index) => { if (!preset || typeof preset !== "object") { return null; From dfe7bd0ac54f49cc792b2d93b0c89a00002de384 Mon Sep 17 00:00:00 2001 From: Blur Date: Sat, 18 Jul 2026 10:18:03 +0200 Subject: [PATCH 43/45] Add status bar to display clicker status and reason for stopping --- CHANGELOG.md | 1 + src-tauri/tauri.conf.json | 2 +- src/App.tsx | 151 ++++++++----- src/components/StatusBar.css | 119 ++++++++++ src/components/StatusBar.tsx | 211 ++++++++++++++++++ src/components/TitleBar.css | 84 +++---- src/components/TitleBar.tsx | 99 +++----- .../panels/settings/SettingsPanel.tsx | 19 ++ .../settings/sections/AppearanceSection.tsx | 24 ++ .../settings/sections/PresetsSection.tsx | 10 +- src/settingsSchema.ts | 4 + 11 files changed, 542 insertions(+), 182 deletions(-) create mode 100644 src/components/StatusBar.css create mode 100644 src/components/StatusBar.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index a3672598..9a5c15a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Added a radius to the click points that randomizes the click position within that radius. - Added Task-bar icon customization in the appearance settings. The Icon now follows the theme color, and accent color on activation. You can toggle all of these options in the settings. - Added Remember window position toggle (Settings → Behaviour → Startup) On by default. +- Added Status bar at the bottom of the window to show the current status of the clicker, and the reason for stopping if it is stopped. ## Changed - Overhauled Advanced panel. - Settings panel now has a side menu for easier navigation and organization of settings. diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 527e5dc0..b46afda0 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "BlurAutoClicker", - "version": "3.8.3", + "version": "3.9.0", "identifier": "BlurAutoClicker", "build": { "frontendDist": "../dist", diff --git a/src/App.tsx b/src/App.tsx index 3daeee6a..ab9dc9d6 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -61,10 +61,12 @@ const ClickPointsPanel = lazy( () => import("./components/panels/click-points/ClickPointsPanel"), ); const TitleBar = lazy(() => import("./components/TitleBar")); +const StatusBar = lazy(() => import("./components/StatusBar")); export type Tab = "simple" | "advanced" | "zones" | "settings" | "click-points"; const BACKEND_SETTINGS_SCHEMA_VERSION = 10; const MAX_DROPDOWN_OVERFLOW_BOTTOM = 220; +const STATUS_BAR_HEIGHT = 26; type DropdownOverflowDetail = { active: boolean; @@ -188,6 +190,9 @@ export default function App() { "idle" | "checking" | "available" | "unavailable" | "error" >("idle"); const [dropdownOverflowBottom, setDropdownOverflowBottom] = useState(0); + const [settingsInitialTab, setSettingsInitialTab] = useState< + string | undefined + >(); const hotkeyTimer = useRef(null); const hotkeyRequestIdRef = useRef(0); @@ -315,36 +320,33 @@ export default function App() { queueHotkeyRegistrationRef.current = queueHotkeyRegistration; }); - const updateSettings = useCallback( - (patch: Partial) => { - const { hotkey, ...rest } = patch; + const updateSettings = useCallback((patch: Partial) => { + const { hotkey, ...rest } = patch; - if (Object.keys(rest).length > 0) { - const nextUiSettings = sanitizeSettings( - { ...uiSettingsRef.current, ...rest }, - APP_VERSION, - ); - const nextCommittedSettings = sanitizeSettings( - { ...committedSettingsRef.current, ...rest }, - APP_VERSION, - ); + if (Object.keys(rest).length > 0) { + const nextUiSettings = sanitizeSettings( + { ...uiSettingsRef.current, ...rest }, + APP_VERSION, + ); + const nextCommittedSettings = sanitizeSettings( + { ...committedSettingsRef.current, ...rest }, + APP_VERSION, + ); - persistCommittedSettingsRef.current( - nextCommittedSettings, - nextUiSettings, - ); - } + persistCommittedSettingsRef.current( + nextCommittedSettings, + nextUiSettings, + ); + } - if (hotkey !== undefined) { - setUiSettingsRef.current({ - ...uiSettingsRef.current, - hotkey, - }); - queueHotkeyRegistrationRef.current(hotkey); - } - }, - [], - ); + if (hotkey !== undefined) { + setUiSettingsRef.current({ + ...uiSettingsRef.current, + hotkey, + }); + queueHotkeyRegistrationRef.current(hotkey); + } + }, []); const applyStartupWindowPlacement = async () => { const pos = committedSettingsRef.current.windowPosition; @@ -662,9 +664,7 @@ export default function App() { globalThis.crypto?.randomUUID?.() ?? `preset-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; - const defaultSnapshot = buildPresetSnapshot( - committedSettingsRef.current, - ); + const defaultSnapshot = buildPresetSnapshot(committedSettingsRef.current); const sanitizedSettings = sanitizePresetSnapshot( parsed.preset.settings, defaultSnapshot, @@ -887,7 +887,11 @@ export default function App() { preferredSize, textScale, ); - const windowHeight = height + dropdownOverflowBottom; + const statusBarOffset = settings.statusBarEnabled + ? STATUS_BAR_HEIGHT + : 0; + const appHeight = height + statusBarOffset; + const windowHeight = appHeight + dropdownOverflowBottom; const appWindow = getCurrentWindow(); @@ -897,7 +901,7 @@ export default function App() { if (cancelled) return; root.style.width = `${width}px`; - root.style.height = `${height}px`; + root.style.height = `${appHeight}px`; await wait(30); if (cancelled) return; @@ -926,7 +930,7 @@ export default function App() { if (cancelled) return; root.style.width = `${width}px`; - root.style.height = `${height}px`; + root.style.height = `${appHeight}px`; const changedProps: string[] = []; if (width !== currentW) changedProps.push("width"); @@ -988,7 +992,7 @@ export default function App() { void root.offsetHeight; root.style.width = `${width}px`; - root.style.height = `${height}px`; + root.style.height = `${appHeight}px`; } } catch (err) { if (!cancelled) { @@ -1009,7 +1013,13 @@ export default function App() { resizeTimeout.current = null; } }; - }, [tab, updateInfo, dropdownOverflowBottom, settingsLoaded]); + }, [ + tab, + updateInfo, + dropdownOverflowBottom, + settingsLoaded, + settings.statusBarEnabled, + ]); useEffect(() => { if (import.meta.env.DEV) return; @@ -1282,15 +1292,19 @@ export default function App() { } }; - const [stopKey, setStopKey] = useState(0); - const prevStopReasonRef = useRef(status.stopReason); + const handleGoToPresets = useCallback(() => { + setSettingsInitialTab("presets"); + setTab("settings"); + }, []); - useEffect(() => { - if (status.stopReason && status.stopReason !== prevStopReasonRef.current) { - prevStopReasonRef.current = status.stopReason; - setStopKey((k) => k + 1); - } - }, [status.stopReason]); + const handleGoToVersionInfo = useCallback(() => { + setSettingsInitialTab("general"); + setTab("settings"); + }, []); + + const handleInitialTabConsumed = useCallback(() => { + setSettingsInitialTab(undefined); + }, []); const handleTabChangeRef = useRef(handleTabChange); handleTabChangeRef.current = handleTabChange; @@ -1354,28 +1368,33 @@ export default function App() { (p) => p.id === settings.activePresetId, ); + function computeTimeLimitMs(): number { + if (!settings.timeLimitEnabled) return 0; + const unit = settings.timeLimitUnit; + const val = settings.timeLimit; + switch (unit) { + case "s": + return val * 1000; + case "m": + return val * 60_000; + case "h": + return val * 3_600_000; + default: + return val * 1000; + } + } + return (
{updateInfo && ( )} + {settings.statusBarEnabled && ( + + )}
); } diff --git a/src/components/StatusBar.css b/src/components/StatusBar.css new file mode 100644 index 00000000..2a21b258 --- /dev/null +++ b/src/components/StatusBar.css @@ -0,0 +1,119 @@ +.status-bar { + display: flex; + align-items: center; + justify-content: space-between; + height: 1.625rem; + padding-inline: 0.75rem; + background: var(--bg-surface); + border-top: 1px solid var(--border-subtle); + flex-shrink: 0; + position: relative; + z-index: 10; + gap: 1rem; +} + +.status-bar[data-running="true"] { + border-top-color: var(--accent-ring); +} + +.status-bar-left { + display: flex; + align-items: center; + min-width: 0; + flex: 1; +} + +.status-bar-preset-btn { + display: inline-flex; + align-items: center; + gap: 0.375rem; + font-family: var(--font-family-base); + font-size: 0.6875rem; + font-weight: var(--font-weight-md); + color: var(--accent-strong); + background: var(--accent-soft); + border: 1px solid var(--accent-ring); + border-radius: var(--radius-sm); + padding: 0.0625rem 0.5rem; + cursor: pointer; + white-space: nowrap; + max-width: 14rem; + overflow: hidden; + text-overflow: ellipsis; + transition: + background 0.15s ease, + border-color 0.15s ease; +} + +.status-bar-preset-btn:hover { + background: var(--accent); + color: #fff; + border-color: var(--accent-strong); +} + +.status-bar-preset-btn svg { + flex-shrink: 0; + opacity: 0.8; +} + +.status-bar-hint-btn { + font-family: var(--font-family-base); + font-size: 0.6875rem; + color: var(--text-dim); + background: none; + border: none; + padding: 0; + cursor: pointer; + white-space: nowrap; + max-width: 14rem; + overflow: hidden; + text-overflow: ellipsis; + transition: color 0.15s ease; +} + +.status-bar-hint-btn:hover { + color: var(--text-primary); +} + +.status-bar-center { + font-size: 0.6875rem; + font-weight: var(--font-weight-md); + color: var(--text-muted); + text-align: center; + flex: 2; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.status-bar-center--empty { + visibility: hidden; +} + +.status-bar-center--warning { + color: var(--accent-yellow); +} + +.status-bar-right { + font-size: 0.6875rem; + color: var(--text-dim); + flex: 1; + text-align: right; + white-space: nowrap; +} + +.status-bar-version-btn { + font-family: var(--font-family-base); + font-size: 0.6875rem; + color: var(--text-dim); + background: none; + border: none; + padding: 0; + cursor: pointer; + transition: color 0.15s ease; +} + +.status-bar-version-btn:hover { + color: var(--accent-strong); +} diff --git a/src/components/StatusBar.tsx b/src/components/StatusBar.tsx new file mode 100644 index 00000000..2afc6dcf --- /dev/null +++ b/src/components/StatusBar.tsx @@ -0,0 +1,211 @@ +import { useEffect, useRef, useState } from "react"; +import "./StatusBar.css"; + +interface Props { + activePresetName: string | null; + version: string; + stopReason: string | null; + warning: string | null; + running: boolean; + paused: boolean; + clickCount: number; + activeClickPointIndex: number | null; + totalClickPoints: number; + clickLimit: number; + clickLimitEnabled: boolean; + timeLimitMs: number; + onGoToPresets: () => void; + onGoToVersionInfo: () => void; +} + +const STOP_REASON_TEXTS: Record = { + "Stopped from UI": "Stopped from UI", + "Stopped from toggle": "Stopped from toggle", + "Stopped from hotkey": "Stopped from hotkey", + "Stopped from hold hotkey": "Stopped from hold hotkey", + Stopped: "Stopped", + "Top-left corner failsafe": "Top-left corner failsafe", + "Top-right corner failsafe": "Top-right corner failsafe", + "Bottom-left corner failsafe": "Bottom-left corner failsafe", + "Bottom-right corner failsafe": "Bottom-right corner failsafe", + "Top edge failsafe": "Top edge failsafe", + "Right edge failsafe": "Right edge failsafe", + "Bottom edge failsafe": "Bottom edge failsafe", + "Left edge failsafe": "Left edge failsafe", + "Blocked by Alt+Tab": "Blocked by Alt+Tab", + "Blocked by process list": "Blocked by process list", +}; + +function translateStopReason(stopReason: string | null | undefined): string { + if (!stopReason) return ""; + const staticText = STOP_REASON_TEXTS[stopReason]; + if (staticText) return staticText; + + const clickLimit = stopReason.match(/^Click limit reached \((.+)\)$/); + if (clickLimit) { + return `Click limit reached (${clickLimit[1]})`; + } + + const timeLimit = stopReason.match(/^Time limit reached \((.+)\)$/); + if (timeLimit) { + return `Time limit reached (${timeLimit[1]})`; + } + + return stopReason; +} + +function formatElapsed(ms: number): string { + const totalSec = Math.floor(ms / 1000); + const h = Math.floor(totalSec / 3600); + const m = Math.floor((totalSec % 3600) / 60); + const s = totalSec % 60; + if (h > 0) { + return `${h}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`; + } + return `${m}:${String(s).padStart(2, "0")}`; +} + +export default function StatusBar({ + activePresetName, + version, + stopReason, + warning, + running, + paused, + clickCount, + activeClickPointIndex, + totalClickPoints, + clickLimit, + clickLimitEnabled, + timeLimitMs, + onGoToPresets, + onGoToVersionInfo, +}: Props) { + const [elapsedMs, setElapsedMs] = useState(0); + const intervalRef = useRef | null>(null); + const baseElapsedRef = useRef(0); + const segmentStartRef = useRef(null); + + useEffect(() => { + if (running && !paused) { + segmentStartRef.current = Date.now(); + const tick = () => { + setElapsedMs( + baseElapsedRef.current + (Date.now() - segmentStartRef.current!), + ); + }; + tick(); + intervalRef.current = setInterval(tick, 200); + } else if (running && paused) { + if (segmentStartRef.current !== null) { + baseElapsedRef.current += Date.now() - segmentStartRef.current; + segmentStartRef.current = null; + } + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + } else { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + baseElapsedRef.current = 0; + segmentStartRef.current = null; + } + + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + intervalRef.current = null; + } + }; + }, [running, paused]); + + let centerText = ""; + let hasWarning = false; + + if (warning) { + centerText = `⚠ ${warning}`; + hasWarning = true; + } else if (running) { + const parts: string[] = []; + + if (paused) { + parts.push("Paused"); + } + + if (totalClickPoints > 1 && activeClickPointIndex !== null) { + parts.push(`Pt ${activeClickPointIndex + 1}/${totalClickPoints}`); + } + + const elapsed = formatElapsed(elapsedMs); + if (timeLimitMs > 0) { + parts.push(`${elapsed} / ${formatElapsed(timeLimitMs)}`); + } else { + parts.push(elapsed); + } + + if (clickLimitEnabled && clickLimit > 0) { + parts.push(`${clickCount} / ${clickLimit} clicks`); + } else if (clickCount > 0) { + parts.push(`${clickCount} clicks`); + } + + if (paused && stopReason) { + parts.push(translateStopReason(stopReason)); + } + + centerText = parts.join("\u00A0\u00A0•\u00A0\u00A0"); + } else if (stopReason) { + const reason = translateStopReason(stopReason); + centerText = clickCount > 0 ? `${reason} • ${clickCount} clicks` : reason; + } + + return ( +
+
+ {activePresetName ? ( + + ) : ( + + )} +
+
+ {centerText} +
+
+ +
+
+ ); +} diff --git a/src/components/TitleBar.css b/src/components/TitleBar.css index 9c207f35..8462010b 100644 --- a/src/components/TitleBar.css +++ b/src/components/TitleBar.css @@ -3,7 +3,7 @@ align-items: center; justify-content: space-between; height: 2.5rem; - padding-inline: 1rem; + padding-inline: 0.75rem; width: 100%; box-sizing: border-box; background: var(--bg-surface); @@ -12,6 +12,7 @@ z-index: 10; flex-shrink: 0; backdrop-filter: blur(var(--bg-panel-blur, 0px)); + transition: border-color 0.3s ease; } .window-title-background[data-tab="settings"] { @@ -19,6 +20,11 @@ height: calc(2.5rem - 2px); } +.window-title-background[data-running="true"] { + border-bottom-color: var(--accent-strong); + box-shadow: inset 0 -1px 0 0 var(--accent-glow-1); +} + .title-wrapper { position: absolute; left: 50%; @@ -45,6 +51,33 @@ 0 0 18px var(--accent-glow-2); } +/* -- Title Horizontal Reveal -- */ + +.title-flipper { + display: inline-block; + transform-origin: center center; +} + +.title-flipper.squish-in { + animation: squishIn 0.25s ease-out both; +} + +@keyframes squishIn { + 0% { + transform: scaleX(0); + opacity: 0; + } + 100% { + transform: scaleX(1); + opacity: 1; + } +} + +.window-title.is-reason { + color: var(--accent-yellow); + text-shadow: 0 0 0.5rem rgba(254, 188, 46, 0.15); +} + .settings-button { background: transparent; border: none; @@ -243,20 +276,15 @@ } .window-btn { - /* Layout */ width: 2rem; height: 2rem; display: flex; align-items: center; justify-content: center; - - /* Reset */ background: transparent; border: none; border-radius: var(--radius-sm); cursor: pointer; - - /* Visuals */ color: var(--text-dim); transition: all 0.15s ease; -webkit-app-region: no-drag; @@ -274,7 +302,6 @@ .window-btn.active { color: var(--accent-strong); - /* background: rgba(255, 255, 255, 0.08); */ } .window-btn.active:hover { @@ -297,46 +324,3 @@ .window-btn.active .pin-needle { transform: translate(12px, 17px) scaleY(1); } - -.titlebar-preset-badge { - font-size: 0.6875rem; - font-weight: var(--font-weight-md); - color: var(--accent-strong); - background: var(--accent-soft); - padding: 0.125rem 0.5rem; - border-radius: var(--radius-md); - white-space: nowrap; - max-width: 8rem; - overflow: hidden; - text-overflow: ellipsis; - line-height: 1.4; - pointer-events: none; - border: 1px solid var(--accent-ring); -} - -/* -- Title Horizontal Reveal -- */ - -.title-flipper { - display: inline-block; - transform-origin: center center; -} - -.title-flipper.squish-in { - animation: squishIn 0.25s ease-out both; -} - -@keyframes squishIn { - 0% { - transform: scaleX(0); - opacity: 0; - } - 100% { - transform: scaleX(1); - opacity: 1; - } -} - -.window-title.is-reason { - color: var(--accent-yellow); - text-shadow: 0 0 0.5rem rgba(254, 188, 46, 0.15); -} diff --git a/src/components/TitleBar.tsx b/src/components/TitleBar.tsx index ca9fb983..7e25fd9d 100644 --- a/src/components/TitleBar.tsx +++ b/src/components/TitleBar.tsx @@ -23,14 +23,11 @@ interface Props { tab: Tab; setTab: (t: Tab) => void; running: boolean; - paused: boolean; - stopReason?: string | null; - stopKey: number; isAlwaysOnTop: boolean; onToggleAlwaysOnTop: () => Promise; onRequestClose: () => Promise; - warning?: string | null; - activePresetName?: string | null; + stopReason: string | null; + statusBarHidden: boolean; } type NavTab = Exclude; @@ -61,19 +58,19 @@ const DEFAULT_TITLE_STATE: TitleViewState = { }; const STOP_REASON_TEXTS: Record = { - "Stopped from UI": "Stopped from UI", - "Stopped from toggle": "Stopped from toggle", - "Stopped from hotkey": "Stopped from hotkey", - "Stopped from hold hotkey": "Stopped from hold hotkey", + "Stopped from UI": "Stopped", + "Stopped from toggle": "Stopped", + "Stopped from hotkey": "Stopped", + "Stopped from hold hotkey": "Stopped", Stopped: "Stopped", - "Top-left corner failsafe": "Top-left corner failsafe", - "Top-right corner failsafe": "Top-right corner failsafe", - "Bottom-left corner failsafe": "Bottom-left corner failsafe", - "Bottom-right corner failsafe": "Bottom-right corner failsafe", - "Top edge failsafe": "Top edge failsafe", - "Right edge failsafe": "Right edge failsafe", - "Bottom edge failsafe": "Bottom edge failsafe", - "Left edge failsafe": "Left edge failsafe", + "Top-left corner failsafe": "Corner failsafe", + "Top-right corner failsafe": "Corner failsafe", + "Bottom-left corner failsafe": "Corner failsafe", + "Bottom-right corner failsafe": "Corner failsafe", + "Top edge failsafe": "Edge failsafe", + "Right edge failsafe": "Edge failsafe", + "Bottom edge failsafe": "Edge failsafe", + "Left edge failsafe": "Edge failsafe", "Blocked by Alt+Tab": "Blocked by Alt+Tab", "Blocked by process list": "Blocked by process list", }; @@ -84,14 +81,10 @@ function translateStopReason(stopReason: string | null | undefined): string { if (staticText) return staticText; const clickLimit = stopReason.match(/^Click limit reached \((.+)\)$/); - if (clickLimit) { - return `Click limit reached (${clickLimit[1]})`; - } + if (clickLimit) return "Click limit reached"; const timeLimit = stopReason.match(/^Time limit reached \((.+)\)$/); - if (timeLimit) { - return `Time limit reached (${timeLimit[1]})`; - } + if (timeLimit) return "Time limit reached"; return stopReason; } @@ -230,14 +223,11 @@ const TitleBar = memo(function TitleBar({ tab, setTab, running, - paused, - stopReason, - stopKey, isAlwaysOnTop, onToggleAlwaysOnTop, onRequestClose, - warning, - activePresetName, + stopReason, + statusBarHidden, }: Props) { const setTabRef = useRef(setTab); useEffect(() => { @@ -307,18 +297,13 @@ const TitleBar = memo(function TitleBar({ ); })}
- {activePresetName && ( - {activePresetName} - )}
@@ -393,12 +378,14 @@ const TitleBar = memo(function TitleBar({ }); function AnimatedTitle({ - running, - paused, + statusBarHidden, stopReason, - stopKey, - warning, -}: Pick) { + running, +}: { + statusBarHidden: boolean; + stopReason: string | null; + running: boolean; +}) { const [titleState, setTitleState] = useState(DEFAULT_TITLE_STATE); const frameIdsRef = useRef([]); const timeoutIdsRef = useRef([]); @@ -424,19 +411,7 @@ function AnimatedTitle({ useEffect(() => { clearScheduledWork(); - if (warning) { - lastShownStopReasonRef.current = null; - queueFrame(() => { - setTitleState({ - text: `⚠ ${warning}`, - isReason: true, - flipClass: "", - }); - }); - return clearScheduledWork; - } - - if (running && !paused && !stopReason) { + if (!statusBarHidden || !stopReason) { lastShownStopReasonRef.current = null; queueFrame(() => { setTitleState(DEFAULT_TITLE_STATE); @@ -444,21 +419,7 @@ function AnimatedTitle({ return clearScheduledWork; } - if (paused) { - lastShownStopReasonRef.current = null; - queueFrame(() => { - setTitleState({ - text: stopReason - ? `Paused: ${translateStopReason(stopReason)}` - : "Paused", - isReason: true, - flipClass: "", - }); - }); - return clearScheduledWork; - } - - if (!stopReason) { + if (running) { lastShownStopReasonRef.current = null; queueFrame(() => { setTitleState(DEFAULT_TITLE_STATE); @@ -492,7 +453,7 @@ function AnimatedTitle({ }, 5000); return clearScheduledWork; - }, [running, stopKey, warning, paused, stopReason]); + }, [statusBarHidden, stopReason, running]); return ( void; + initialSettingsTab?: string; + onInitialTabConsumed?: () => void; } export default function SettingsPanel({ @@ -61,10 +63,27 @@ export default function SettingsPanel({ onReset, updateCheckStatus, onCheckForUpdate, + initialSettingsTab, + onInitialTabConsumed, }: Props) { const [activeTab, setActiveTab] = useState("general"); const [atBottom, setAtBottom] = useState(true); const panelRef = useRef(null); + const prevInitialTabRef = useRef(undefined); + + useEffect(() => { + if ( + initialSettingsTab && + initialSettingsTab !== prevInitialTabRef.current + ) { + prevInitialTabRef.current = initialSettingsTab; + onInitialTabConsumed?.(); + const tab = initialSettingsTab as SettingsTab; + requestAnimationFrame(() => { + setActiveTab(tab); + }); + } + }, [initialSettingsTab, onInitialTabConsumed]); const handleScroll = () => { const panel = panelRef.current; diff --git a/src/components/panels/settings/sections/AppearanceSection.tsx b/src/components/panels/settings/sections/AppearanceSection.tsx index 71c8e0f3..5d55a66a 100644 --- a/src/components/panels/settings/sections/AppearanceSection.tsx +++ b/src/components/panels/settings/sections/AppearanceSection.tsx @@ -356,6 +356,30 @@ export default function AppearanceSection({ settings, update }: Props) {
+ + +
+
+ Footer + + Shows active preset, version, and stop reasons. + +
+
+ +
+
+
); } diff --git a/src/components/panels/settings/sections/PresetsSection.tsx b/src/components/panels/settings/sections/PresetsSection.tsx index 7768d4cf..d89a23af 100644 --- a/src/components/panels/settings/sections/PresetsSection.tsx +++ b/src/components/panels/settings/sections/PresetsSection.tsx @@ -226,10 +226,9 @@ export default function PresetsSection({ const activeEditingPresetId = running ? null : editingPresetId; const activeConfirmingDeleteId = running ? null : confirmingDeleteId; - const activePreset = - settings.activePresetId - ? settings.presets.find((p) => p.id === settings.activePresetId) ?? null - : null; + const activePreset = settings.activePresetId + ? (settings.presets.find((p) => p.id === settings.activePresetId) ?? null) + : null; const currentSnapshot = buildPresetSnapshot(settings); const isActiveModified = @@ -412,8 +411,7 @@ export default function PresetsSection({ preset={preset} isActive={settings.activePresetId === preset.id} isModified={ - settings.activePresetId === preset.id && - isActiveModified + settings.activePresetId === preset.id && isActiveModified } isEditing={activeEditingPresetId === preset.id} isConfirmingDelete={activeConfirmingDeleteId === preset.id} diff --git a/src/settingsSchema.ts b/src/settingsSchema.ts index 1e6d0c8b..6277e398 100644 --- a/src/settingsSchema.ts +++ b/src/settingsSchema.ts @@ -574,6 +574,10 @@ const SETTINGS_ONLY_FIELDS = { default: "theme" as IconColor, ui: { section: "appearance", control: "select" }, }, + statusBarEnabled: { + default: true, + ui: { section: "appearance", control: "toggle" }, + }, } satisfies Record>; export const SETTINGS_FIELD_DEFS = { From db2588f74c0dea94fd7665381d7f771ef022b189 Mon Sep 17 00:00:00 2001 From: Blur Date: Sat, 18 Jul 2026 10:27:01 +0200 Subject: [PATCH 44/45] Implement minimized state handling and UI updates for main window --- src-tauri/src/lib.rs | 38 +++++++++++++++++++++----------------- src/App.tsx | 11 +++++++++++ src/index.css | 5 +++++ 3 files changed, 37 insertions(+), 17 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 60122bef..de7dc2a2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -25,7 +25,7 @@ use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64}; use std::sync::{Arc, Mutex}; use tauri::menu::{Menu, MenuItem}; use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; -use tauri::{AppHandle, Listener, Manager}; +use tauri::{AppHandle, Emitter, Listener, Manager}; pub static ZONE_MONITOR_RUNNING: AtomicBool = AtomicBool::new(false); @@ -427,22 +427,26 @@ pub fn run() { .build(tauri::generate_context!()) .expect("error while building tauri application") .run(|app_handle, event| { - if let tauri::RunEvent::WindowEvent { - event: tauri::WindowEvent::CloseRequested { api, .. }, - label, - .. - } = &event - { - if label == "main" { - api.prevent_close(); - crate::app_events::APP_EVENTS_SHUTDOWN - .store(true, std::sync::atomic::Ordering::SeqCst); - crate::overlay::OVERLAY_THREAD_RUNNING - .store(false, std::sync::atomic::Ordering::SeqCst); - ZONE_MONITOR_RUNNING.store(false, std::sync::atomic::Ordering::SeqCst); - crate::click_point_picker::cancel_click_point_pick_inner(app_handle); - crate::custom_stop_zone_picker::cancel_custom_stop_zone_pick_inner(app_handle); - app_handle.exit(0); + if let tauri::RunEvent::WindowEvent { event, label, .. } = &event { + match event { + tauri::WindowEvent::CloseRequested { api, .. } if label == "main" => { + api.prevent_close(); + crate::app_events::APP_EVENTS_SHUTDOWN + .store(true, std::sync::atomic::Ordering::SeqCst); + crate::overlay::OVERLAY_THREAD_RUNNING + .store(false, std::sync::atomic::Ordering::SeqCst); + ZONE_MONITOR_RUNNING.store(false, std::sync::atomic::Ordering::SeqCst); + crate::click_point_picker::cancel_click_point_pick_inner(app_handle); + crate::custom_stop_zone_picker::cancel_custom_stop_zone_pick_inner( + app_handle, + ); + app_handle.exit(0); + } + tauri::WindowEvent::Resized(size) if label == "main" => { + let minimized = size.width == 0 || size.height == 0; + let _ = app_handle.emit("minimized-changed", minimized); + } + _ => {} } } }); diff --git a/src/App.tsx b/src/App.tsx index ab9dc9d6..572cea76 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1196,6 +1196,17 @@ export default function App() { tab, ]); + useEffect(() => { + const cleanup = listen("minimized-changed", (event) => { + const root = document.querySelector(".app-root") as HTMLElement | null; + if (!root) return; + root.toggleAttribute("data-minimized", event.payload); + }); + return () => { + cleanup.then((fn) => fn()); + }; + }, []); + useLayoutEffect(() => { const root = document.querySelector(".app-root") as HTMLElement | null; if (!root) return; diff --git a/src/index.css b/src/index.css index bb2c4b20..7589be4f 100644 --- a/src/index.css +++ b/src/index.css @@ -141,6 +141,11 @@ body { transition: opacity 0.2s ease; } +.app-root[data-minimized]::before { + background-image: none !important; + opacity: 0 !important; +} + .app-root > * { position: relative; z-index: 1; From a809e5dea67cf9116d9f320c5d8b7c54189289e4 Mon Sep 17 00:00:00 2001 From: Blur Date: Sat, 18 Jul 2026 11:33:59 +0200 Subject: [PATCH 45/45] fix task bar and tray icon theme color not applying in release --- src-tauri/src/engine/worker.rs | 4 ++-- src-tauri/src/lib.rs | 4 +++- src/App.tsx | 41 +++++++++++++++++----------------- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/src-tauri/src/engine/worker.rs b/src-tauri/src/engine/worker.rs index bfea085f..48155c26 100644 --- a/src-tauri/src/engine/worker.rs +++ b/src-tauri/src/engine/worker.rs @@ -82,7 +82,7 @@ fn resolve_icon_theme(icon_state: &IconState) -> String { } } -fn set_app_icons(app: &AppHandle) { +pub fn set_app_icons(app: &AppHandle) { let state = app.state::(); let active = state.running.load(Ordering::SeqCst); let icon_state = state.icon_state.lock().unwrap_or_else(poisoned_inner); @@ -121,7 +121,6 @@ fn set_app_icons(app: &AppHandle) { ICON_DEACTIVATED_LIGHT } }; - drop(icon_state); let Ok(img) = Image::from_bytes(bytes) else { return; @@ -146,6 +145,7 @@ pub fn set_icon_theme_inner( let state = app.state::(); let use_tint = icon_enabled && icon_color == "theme"; + let (dark, light) = if use_tint { ( tint_icon(ICON_ACTIVATED_DARK, MASK_PNG_BYTES, hex_color), diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index de7dc2a2..6b67e69d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -132,7 +132,7 @@ fn setup_tray(app: &AppHandle) -> Result<(), tauri::Error> { let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?; let menu = Menu::with_items(app, &[&show_item, &quit_item])?; - TrayIconBuilder::new() + TrayIconBuilder::with_id("main") .icon(app.default_window_icon().unwrap().clone()) .menu(&menu) .tooltip("BlurAutoClicker") @@ -143,6 +143,7 @@ fn setup_tray(app: &AppHandle) -> Result<(), tauri::Error> { #[cfg(target_os = "windows")] apply_ws_ex_noactivate(&window, false); let _ = window.show(); + crate::engine::worker::set_app_icons(app); let _ = window.set_focus(); } } @@ -170,6 +171,7 @@ fn setup_tray(app: &AppHandle) -> Result<(), tauri::Error> { #[cfg(target_os = "windows")] apply_ws_ex_noactivate(&window, false); let _ = window.show(); + crate::engine::worker::set_app_icons(app); let _ = window.set_focus(); } } diff --git a/src/App.tsx b/src/App.tsx index 572cea76..9cff96e3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -205,9 +205,7 @@ export default function App() { const resizeTimeout = useRef | null>(null); const toggleTimerRef = useRef | null>(null); const cooldownTimerRef = useRef | null>(null); - const accentColorTimerRef = useRef | null>( - null, - ); + const mountedRef = useRef(false); const setUiSettings = (nextSettings: Settings) => { @@ -769,6 +767,18 @@ export default function App() { setTab(hydratedSettings.lastPanel); setSettings(hydratedSettings); + { + const theme = hydratedSettings.theme ?? "dark"; + document.documentElement.dataset.theme = theme; + applyAccentTheme(hydratedSettings.accentColor, theme); + invoke("set_accent_color", { + color: hydratedSettings.accentColor, + theme, + iconEnabled: hydratedSettings.taskbarIconEnabled, + iconTheme: hydratedSettings.taskbarIconTheme, + iconColor: hydratedSettings.taskbarIconColor, + }); + } setAppInfo(loadedAppInfo); setStatus(loadedStatus); setSettingsLoaded(true); @@ -1079,24 +1089,13 @@ export default function App() { document.documentElement.dataset.theme = theme; applyAccentTheme(settings.accentColor, theme); - if (accentColorTimerRef.current) { - clearTimeout(accentColorTimerRef.current); - } - accentColorTimerRef.current = setTimeout(() => { - invoke("set_accent_color", { - color: settings.accentColor, - theme, - iconEnabled: settings.taskbarIconEnabled, - iconTheme: settings.taskbarIconTheme, - iconColor: settings.taskbarIconColor, - }); - }, 100); - - return () => { - if (accentColorTimerRef.current) { - clearTimeout(accentColorTimerRef.current); - } - }; + invoke("set_accent_color", { + color: settings.accentColor, + theme, + iconEnabled: settings.taskbarIconEnabled, + iconTheme: settings.taskbarIconTheme, + iconColor: settings.taskbarIconColor, + }); }, [ settings.accentColor, settings.theme,