From 7af290880269f8fbef56a69ff1546e87b322488d Mon Sep 17 00:00:00 2001 From: Alex TYRODE Date: Mon, 28 Apr 2025 00:15:24 +0000 Subject: [PATCH 01/10] feat: add SettingsDialog and integrate into ExcalidrawWrapper and MainMenu - Introduced a new SettingsDialog component for managing application settings. - Updated ExcalidrawWrapper to handle the visibility of the SettingsDialog. - Enhanced MainMenu to include a Settings option that triggers the SettingsDialog. - Added corresponding styles for the SettingsDialog to ensure a cohesive UI experience. --- src/frontend/src/ExcalidrawWrapper.tsx | 18 ++++++++- src/frontend/src/ui/MainMenu.tsx | 18 ++++++++- src/frontend/src/ui/SettingsDialog.scss | 40 +++++++++++++++++++ src/frontend/src/ui/SettingsDialog.tsx | 51 +++++++++++++++++++++++++ 4 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 src/frontend/src/ui/SettingsDialog.scss create mode 100644 src/frontend/src/ui/SettingsDialog.tsx diff --git a/src/frontend/src/ExcalidrawWrapper.tsx b/src/frontend/src/ExcalidrawWrapper.tsx index b2095e1..32ac001 100644 --- a/src/frontend/src/ExcalidrawWrapper.tsx +++ b/src/frontend/src/ExcalidrawWrapper.tsx @@ -8,6 +8,7 @@ import { MainMenuConfig } from './ui/MainMenu'; import { lockEmbeddables, renderCustomEmbeddable } from './CustomEmbeddableRenderer'; import AuthDialog from './ui/AuthDialog'; import BackupsModal from './ui/BackupsDialog'; +import SettingsDialog from './ui/SettingsDialog'; const defaultInitialData = { elements: [], @@ -47,8 +48,9 @@ export const ExcalidrawWrapper: React.FC = ({ // Add state for modal animation const [isExiting, setIsExiting] = useState(false); - // State for BackupsModal + // State for modals const [showBackupsModal, setShowBackupsModal] = useState(false); + const [showSettingsModal, setShowSettingsModal] = useState(false); // Handle auth state changes useEffect(() => { @@ -57,11 +59,15 @@ export const ExcalidrawWrapper: React.FC = ({ } }, [isAuthenticated]); - // Handler for closing the backups modal + // Handlers for closing modals const handleCloseBackupsModal = () => { setShowBackupsModal(false); }; + const handleCloseSettingsModal = () => { + setShowSettingsModal(false); + }; + const renderExcalidraw = (children: React.ReactNode) => { const Excalidraw = Children.toArray(children).find( (child: any) => @@ -98,6 +104,8 @@ export const ExcalidrawWrapper: React.FC = ({ excalidrawAPI={excalidrawAPI} showBackupsModal={showBackupsModal} setShowBackupsModal={setShowBackupsModal} + showSettingsModal={showSettingsModal} + setShowSettingsModal={setShowSettingsModal} /> {!isAuthLoading && isAuthenticated === false && ( = ({ onClose={handleCloseBackupsModal} /> )} + + {showSettingsModal && ( + + )} ); }; diff --git a/src/frontend/src/ui/MainMenu.tsx b/src/frontend/src/ui/MainMenu.tsx index da211ac..ba6f1b3 100644 --- a/src/frontend/src/ui/MainMenu.tsx +++ b/src/frontend/src/ui/MainMenu.tsx @@ -3,17 +3,20 @@ import React, { useState } from 'react'; import type { ExcalidrawImperativeAPI } from '@atyrode/excalidraw/types'; import type { MainMenu as MainMenuType } from '@atyrode/excalidraw'; -import { LogOut, SquarePlus, LayoutDashboard, SquareCode, Eye, Coffee, Grid2x2, User, Text, ArchiveRestore } from 'lucide-react'; +import { LogOut, SquarePlus, LayoutDashboard, SquareCode, Eye, Coffee, Grid2x2, User, Text, ArchiveRestore, Settings } from 'lucide-react'; import { capture } from '../utils/posthog'; import { ExcalidrawElementFactory, PlacementMode } from '../lib/ExcalidrawElementFactory'; import { useUserProfile } from "../api/hooks"; import { queryClient } from "../api/queryClient"; +import SettingsDialog from "./SettingsDialog"; import "./MainMenu.scss"; interface MainMenuConfigProps { MainMenu: typeof MainMenuType; excalidrawAPI: ExcalidrawImperativeAPI | null; showBackupsModal: boolean; setShowBackupsModal: (show: boolean) => void; + showSettingsModal?: boolean; + setShowSettingsModal?: (show: boolean) => void; } export const MainMenuConfig: React.FC = ({ @@ -21,6 +24,8 @@ export const MainMenuConfig: React.FC = ({ excalidrawAPI, showBackupsModal, setShowBackupsModal, + showSettingsModal = false, + setShowSettingsModal = (show: boolean) => {}, }) => { const { data, isLoading, isError } = useUserProfile(); @@ -100,6 +105,10 @@ export const MainMenuConfig: React.FC = ({ setShowBackupsModal(true); }; + const handleSettingsClick = () => { + setShowSettingsModal(true); + }; + const handleGridToggle = () => { if (!excalidrawAPI) return; const appState = excalidrawAPI.getAppState(); @@ -207,6 +216,13 @@ export const MainMenuConfig: React.FC = ({ + } + onClick={handleSettingsClick} + > + Settings + + } onClick={async () => { diff --git a/src/frontend/src/ui/SettingsDialog.scss b/src/frontend/src/ui/SettingsDialog.scss new file mode 100644 index 0000000..accbb0c --- /dev/null +++ b/src/frontend/src/ui/SettingsDialog.scss @@ -0,0 +1,40 @@ +.settings-dialog { + &__wrapper { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 1000; + background-color: rgba(0, 0, 0, 0.2); + backdrop-filter: blur(1px); + } + + &__title-container { + display: flex; + align-items: center; + justify-content: space-between; + } + + &__title { + margin: 0; + font-size: 1.2rem; + font-weight: 600; + } + + &__content { + padding: 1rem; + min-height: 200px; + display: flex; + flex-direction: column; + } + + &__empty { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: #888; + font-style: italic; + } +} diff --git a/src/frontend/src/ui/SettingsDialog.tsx b/src/frontend/src/ui/SettingsDialog.tsx new file mode 100644 index 0000000..28b99a1 --- /dev/null +++ b/src/frontend/src/ui/SettingsDialog.tsx @@ -0,0 +1,51 @@ +import React, { useState, useCallback } from "react"; +import { Dialog } from "@atyrode/excalidraw"; +import "./SettingsDialog.scss"; + +interface SettingsDialogProps { + onClose?: () => void; +} + +const SettingsDialog: React.FC = ({ + onClose, +}) => { + const [modalIsShown, setModalIsShown] = useState(true); + + const handleClose = useCallback(() => { + setModalIsShown(false); + if (onClose) { + onClose(); + } + }, [onClose]); + + // Dialog content + const dialogContent = ( +
+ {/* Settings content will go here in the future */} +
Settings dialog is empty for now
+
+ ); + + return ( + <> + {modalIsShown && ( +
+ +

Settings

+
+ } + closeOnClickOutside={true} + children={dialogContent} + /> + + )} + + ); +}; + +export default SettingsDialog; From 386e93ef0b825a6a5f109c2d979554e46517c392 Mon Sep 17 00:00:00 2001 From: Alex TYRODE Date: Mon, 28 Apr 2025 00:19:23 +0000 Subject: [PATCH 02/10] chore: update @atyrode/excalidraw dependency to version 0.18.0-4 --- src/frontend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/package.json b/src/frontend/package.json index d68c2be..98f8653 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "private": true, "dependencies": { - "@atyrode/excalidraw": "^0.18.0-3", + "@atyrode/excalidraw": "^0.18.0-4", "@monaco-editor/react": "^4.7.0", "@tanstack/react-query": "^5.74.3", "@tanstack/react-query-devtools": "^5.74.3", From 929dfde316d05e4fa9a4daa20e1dd51d0afebd09 Mon Sep 17 00:00:00 2001 From: Alex TYRODE Date: Mon, 28 Apr 2025 00:41:15 +0000 Subject: [PATCH 03/10] feat: enhance SettingsDialog with user settings management - Added UserSettings interface and default settings for embed lock debounce time. - Integrated settings management into SettingsDialog, allowing users to adjust embed lock debounce time via a range input. - Updated normalizeCanvasData to preserve user settings when normalizing canvas data. - Improved styles for SettingsDialog to enhance user experience and organization. --- src/frontend/src/ExcalidrawWrapper.tsx | 2 + src/frontend/src/types/settings.ts | 16 ++++++ src/frontend/src/ui/SettingsDialog.scss | 39 ++++++++++++- src/frontend/src/ui/SettingsDialog.tsx | 74 +++++++++++++++++++++++-- src/frontend/src/utils/canvasUtils.ts | 17 ++++-- src/frontend/yarn.lock | 8 +-- 6 files changed, 141 insertions(+), 15 deletions(-) create mode 100644 src/frontend/src/types/settings.ts diff --git a/src/frontend/src/ExcalidrawWrapper.tsx b/src/frontend/src/ExcalidrawWrapper.tsx index 32ac001..8d4ded8 100644 --- a/src/frontend/src/ExcalidrawWrapper.tsx +++ b/src/frontend/src/ExcalidrawWrapper.tsx @@ -59,6 +59,7 @@ export const ExcalidrawWrapper: React.FC = ({ } }, [isAuthenticated]); + // Handlers for closing modals const handleCloseBackupsModal = () => { setShowBackupsModal(false); @@ -122,6 +123,7 @@ export const ExcalidrawWrapper: React.FC = ({ {showSettingsModal && ( )} diff --git a/src/frontend/src/types/settings.ts b/src/frontend/src/types/settings.ts new file mode 100644 index 0000000..35e7400 --- /dev/null +++ b/src/frontend/src/types/settings.ts @@ -0,0 +1,16 @@ +/** + * Types for user settings + */ + +export interface UserSettings { + /** + * The debounce time in milliseconds for the embed lock + * Range: 150ms to 5000ms (5 seconds) + * Default: 350ms + */ + embedLockDebounceTime?: number; +} + +export const DEFAULT_SETTINGS: UserSettings = { + embedLockDebounceTime: 350, // Default value from CustomEmbeddableRenderer.tsx +}; diff --git a/src/frontend/src/ui/SettingsDialog.scss b/src/frontend/src/ui/SettingsDialog.scss index accbb0c..759252a 100644 --- a/src/frontend/src/ui/SettingsDialog.scss +++ b/src/frontend/src/ui/SettingsDialog.scss @@ -6,8 +6,6 @@ right: 0; bottom: 0; z-index: 1000; - background-color: rgba(0, 0, 0, 0.2); - backdrop-filter: blur(1px); } &__title-container { @@ -37,4 +35,41 @@ color: #888; font-style: italic; } + + &__section { + margin-bottom: 1.5rem; + } + + &__section-title { + margin: 0 0 1rem 0; + font-size: 1rem; + font-weight: 600; + color: var(--text-primary-color); + } + + &__setting { + margin-bottom: 1rem; + padding: 0.5rem; + border-radius: 4px; + background-color: var(--dialog-bg-color); + } + + &__label { + display: block; + margin-bottom: 0.5rem; + font-size: 0.9rem; + color: var(--text-primary-color); + } + + &__range-container { + margin: 1rem 0; + } + + &__range-labels { + display: flex; + justify-content: space-between; + font-size: 0.8rem; + color: var(--text-primary-color); + opacity: 0.7; + } } diff --git a/src/frontend/src/ui/SettingsDialog.tsx b/src/frontend/src/ui/SettingsDialog.tsx index 28b99a1..3ee95ce 100644 --- a/src/frontend/src/ui/SettingsDialog.tsx +++ b/src/frontend/src/ui/SettingsDialog.tsx @@ -1,15 +1,31 @@ -import React, { useState, useCallback } from "react"; -import { Dialog } from "@atyrode/excalidraw"; +import React, { useState, useCallback, useEffect } from "react"; +import { Dialog, Range } from "@atyrode/excalidraw"; +import { UserSettings, DEFAULT_SETTINGS } from "../types/settings"; import "./SettingsDialog.scss"; interface SettingsDialogProps { + excalidrawAPI?: any; onClose?: () => void; } const SettingsDialog: React.FC = ({ + excalidrawAPI, onClose, }) => { const [modalIsShown, setModalIsShown] = useState(true); + const [settings, setSettings] = useState(DEFAULT_SETTINGS); + + // Get current settings from excalidrawAPI when component mounts + useEffect(() => { + if (excalidrawAPI) { + const appState = excalidrawAPI.getAppState(); + const userSettings = appState?.pad?.userSettings || {}; + setSettings({ + ...DEFAULT_SETTINGS, + ...userSettings + }); + } + }, [excalidrawAPI]); const handleClose = useCallback(() => { setModalIsShown(false); @@ -18,11 +34,61 @@ const SettingsDialog: React.FC = ({ } }, [onClose]); + const handleEmbedLockDebounceTimeChange = (value: number) => { + if (!excalidrawAPI) return; + + const newSettings = { + ...settings, + embedLockDebounceTime: value + }; + + setSettings(newSettings); + + // Update the appState + const appState = excalidrawAPI.getAppState(); + const updatedAppState = { + ...appState, + pad: { + ...appState.pad, + userSettings: newSettings + } + }; + + excalidrawAPI.updateScene({ + appState: updatedAppState + }); + }; + // Dialog content const dialogContent = (
- {/* Settings content will go here in the future */} -
Settings dialog is empty for now
+
+

Embed Settings

+
+ +
+ handleEmbedLockDebounceTimeChange( + // Map 0-100 range to 150-5000ms + Math.round(150 + (value / 100) * 4850) + )} + appState={{ + currentItemOpacity: + // Map 150-5000ms to 0-100 range + Math.round(((settings.embedLockDebounceTime || 350) - 150) / 4850 * 100) + }} + elements={[]} + testId="embed-lock-debounce-time" + /> +
+
+ 150ms + 5000ms +
+
+
); diff --git a/src/frontend/src/utils/canvasUtils.ts b/src/frontend/src/utils/canvasUtils.ts index 41b8ad9..79d2c02 100644 --- a/src/frontend/src/utils/canvasUtils.ts +++ b/src/frontend/src/utils/canvasUtils.ts @@ -1,9 +1,6 @@ +import { DEFAULT_SETTINGS } from '../types/settings'; + /** - * Normalizes canvas data by removing width and height properties from appState - * and resetting collaborators to an empty Map. - * - * This is necessary when loading canvas data to ensure it fits properly in the current viewport - * and doesn't carry over collaborator information that might be stale. * * @param data The canvas data to normalize * @returns Normalized canvas data @@ -21,6 +18,11 @@ export function normalizeCanvasData(data: any) { delete appState.height; } + // Preserve existing pad settings if they exist, otherwise create new ones + const existingPad = appState.pad || {}; + const existingUserSettings = existingPad.userSettings || {}; + + // Merge existing user settings with default settings appState.pad = { moduleBorderOffset: { left: 10, @@ -28,6 +30,11 @@ export function normalizeCanvasData(data: any) { top: 40, bottom: 10, }, + // Merge existing user settings with default settings + userSettings: { + ...DEFAULT_SETTINGS, + ...existingUserSettings + } }; // Reset collaborators (https://github.com/excalidraw/excalidraw/issues/8637) diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock index 723d6a2..b069aa3 100644 --- a/src/frontend/yarn.lock +++ b/src/frontend/yarn.lock @@ -2,10 +2,10 @@ # yarn lockfile v1 -"@atyrode/excalidraw@^0.18.0-3": - version "0.18.0-3" - resolved "https://registry.yarnpkg.com/@atyrode/excalidraw/-/excalidraw-0.18.0-3.tgz#445176d5b9828f033205a46f227fd76e722019ec" - integrity sha512-iWLyYMZNV9VQCcWAtrLmg52NkfCw1CDrigXXRrxa3H2KW6nyrlbFm3KZAF5Y0mOJDYzAtxclPvHoTvcRruMjGg== +"@atyrode/excalidraw@^0.18.0-4": + version "0.18.0-4" + resolved "https://registry.yarnpkg.com/@atyrode/excalidraw/-/excalidraw-0.18.0-4.tgz#3350bd09533cb424105fa615ec0e2e4e243f798e" + integrity sha512-MDYAXT34cNmhoc49eC7iuoweGHsirKKH0VnwDT9CJPWTjUfOrXp/NsL1PHyFIyhBu14NI1osSpGTD0qi1FPegQ== dependencies: "@braintree/sanitize-url" "6.0.2" "@excalidraw/laser-pointer" "1.3.1" From d84ef9ab96ba5db1d71b2b10901a39c34af53473 Mon Sep 17 00:00:00 2001 From: Alex TYRODE Date: Mon, 28 Apr 2025 00:54:05 +0000 Subject: [PATCH 04/10] feat: enhance lockEmbeddables function to read from settings - Introduced a memoized debounced function to manage scrolling state more efficiently. - Updated lockEmbeddables to accept appState for customizable debounce timing. - Modified ExcalidrawWrapper to integrate the new scrolling behavior, ensuring smoother user experience during scrolling events. --- src/frontend/src/CustomEmbeddableRenderer.tsx | 40 +++++++++++++------ src/frontend/src/ExcalidrawWrapper.tsx | 5 ++- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/frontend/src/CustomEmbeddableRenderer.tsx b/src/frontend/src/CustomEmbeddableRenderer.tsx index f53c98e..0fcb1c4 100644 --- a/src/frontend/src/CustomEmbeddableRenderer.tsx +++ b/src/frontend/src/CustomEmbeddableRenderer.tsx @@ -120,7 +120,32 @@ let isScrolling = false; // Create a custom event for scrolling state changes const scrollStateChangeEvent = new CustomEvent('scrollStateChange', { detail: { isScrolling: false } }); -export const lockEmbeddables = () => { +// Memoized debounced function factory +const getDebouncedScrollEnd = (() => { + let lastDebounceTime = 0; + let debouncedFn: ReturnType | null = null; + + return (currentDebounceTime: number) => { + // Only recreate if the time has changed + if (currentDebounceTime !== lastDebounceTime || !debouncedFn) { + lastDebounceTime = currentDebounceTime; + debouncedFn = debounce(() => { + isScrolling = false; + // Set pointer-events back to all when not scrolling + document.documentElement.style.setProperty('--embeddable-pointer-events', 'all'); + // Dispatch event with updated scrolling state + scrollStateChangeEvent.detail.isScrolling = false; + document.dispatchEvent(scrollStateChangeEvent); + }, currentDebounceTime); + } + return debouncedFn; + }; +})(); + +export const lockEmbeddables = (appState?: AppState) => { + // Get the debounce time from settings, with fallback to default + const debounceTime = appState?.pad?.userSettings?.embedLockDebounceTime || 350; + if (!isScrolling) { isScrolling = true; // Set pointer-events to none during scrolling @@ -130,16 +155,7 @@ export const lockEmbeddables = () => { document.dispatchEvent(scrollStateChangeEvent); } - // Reset the pointer-events after scrolling stops + // Get the current debounced function and call it + const debouncedScrollEnd = getDebouncedScrollEnd(debounceTime); debouncedScrollEnd(); }; - -// Create a debounced function to detect when scrolling ends -const debouncedScrollEnd = debounce(() => { - isScrolling = false; - // Set pointer-events back to all when not scrolling - document.documentElement.style.setProperty('--embeddable-pointer-events', 'all'); - // Dispatch event with updated scrolling state - scrollStateChangeEvent.detail.isScrolling = false; - document.dispatchEvent(scrollStateChangeEvent); -}, 350); \ No newline at end of file diff --git a/src/frontend/src/ExcalidrawWrapper.tsx b/src/frontend/src/ExcalidrawWrapper.tsx index 8d4ded8..99f51cc 100644 --- a/src/frontend/src/ExcalidrawWrapper.tsx +++ b/src/frontend/src/ExcalidrawWrapper.tsx @@ -89,7 +89,10 @@ export const ExcalidrawWrapper: React.FC = ({ initialData: initialData ?? defaultInitialData, onChange: onChange, name: "Pad.ws", - onScrollChange: lockEmbeddables, + onScrollChange: (scrollX, scrollY) => { + lockEmbeddables(excalidrawAPI?.getAppState()); + if (onScrollChange) onScrollChange(scrollX, scrollY); + }, validateEmbeddable: true, renderEmbeddable: (element, appState) => renderCustomEmbeddable(element, appState, excalidrawAPI), renderTopRightUI: renderTopRightUI ?? (() => ( From 63e03544c6ab81b9605416340b78a1928d491602 Mon Sep 17 00:00:00 2001 From: Alex TYRODE Date: Mon, 28 Apr 2025 00:54:17 +0000 Subject: [PATCH 05/10] style: update SettingsDialog styles for improved UI - Introduced background color and backdrop filter to the dialog wrapper for better visual appeal. --- src/frontend/src/ui/SettingsDialog.scss | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/frontend/src/ui/SettingsDialog.scss b/src/frontend/src/ui/SettingsDialog.scss index 759252a..dfbf848 100644 --- a/src/frontend/src/ui/SettingsDialog.scss +++ b/src/frontend/src/ui/SettingsDialog.scss @@ -1,4 +1,9 @@ .settings-dialog { + + .control-label { + font-size: 0px; + } + &__wrapper { position: absolute; top: 0; @@ -6,6 +11,8 @@ right: 0; bottom: 0; z-index: 1000; + background-color: rgba(0, 0, 0, 0.2); + backdrop-filter: blur(1px); } &__title-container { From 7b093fb98465bc6709cd2856cc0f270ce39ae822 Mon Sep 17 00:00:00 2001 From: Alex TYRODE Date: Mon, 28 Apr 2025 01:14:44 +0000 Subject: [PATCH 06/10] feat: add Range component for adjustable settings - Introduced a new Range component for user input, allowing adjustable values with labels and a value bubble. - Integrated the Range component into SettingsDialog for managing embed lock debounce time, enhancing user experience with visual feedback. - Added corresponding styles in Range.scss for improved UI consistency. --- src/frontend/src/ui/Range.scss | 71 +++++++++++++++++++++++ src/frontend/src/ui/Range.tsx | 79 ++++++++++++++++++++++++++ src/frontend/src/ui/SettingsDialog.tsx | 34 ++++++----- 3 files changed, 166 insertions(+), 18 deletions(-) create mode 100644 src/frontend/src/ui/Range.scss create mode 100644 src/frontend/src/ui/Range.tsx diff --git a/src/frontend/src/ui/Range.scss b/src/frontend/src/ui/Range.scss new file mode 100644 index 0000000..876356c --- /dev/null +++ b/src/frontend/src/ui/Range.scss @@ -0,0 +1,71 @@ +:root { + --slider-thumb-size: 16px; +} + +.control-label { + display: flex; + flex-direction: column; + width: 100%; + font-size: 0.9rem; + color: var(--text-primary-color); +} + +.range-wrapper { + position: relative; + padding-top: 10px; + padding-bottom: 25px; + width: 100%; +} + +.range-input { + width: 100%; + height: 4px; + -webkit-appearance: none; + background: var(--color-slider-track); + border-radius: 2px; + outline: none; +} + +.range-input::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: var(--slider-thumb-size); + height: var(--slider-thumb-size); + background: var(--color-slider-thumb); + border-radius: 50%; + cursor: pointer; + border: none; +} + +.range-input::-moz-range-thumb { + width: var(--slider-thumb-size); + height: var(--slider-thumb-size); + background: var(--color-slider-thumb); + border-radius: 50%; + cursor: pointer; + border: none; +} + +.value-bubble { + position: absolute; + bottom: 0; + transform: translateX(-50%); + font-size: 12px; + color: var(--text-primary-color); +} + +.min-label, .zero-label { + position: absolute; + bottom: 0; + left: 4px; + font-size: 12px; + color: var(--text-primary-color); +} + +.max-label { + position: absolute; + bottom: 0; + right: 4px; + font-size: 12px; + color: var(--text-primary-color); +} diff --git a/src/frontend/src/ui/Range.tsx b/src/frontend/src/ui/Range.tsx new file mode 100644 index 0000000..ef9bf02 --- /dev/null +++ b/src/frontend/src/ui/Range.tsx @@ -0,0 +1,79 @@ +import React, { useEffect } from "react"; + +import "./Range.scss"; + +export type RangeProps = { + value: number; + onChange: (value: number) => void; + min?: number; + max?: number; + step?: number; + label?: string; + minLabel?: string; + maxLabel?: string; + showValueBubble?: boolean; +}; + +export const Range = ({ + value, + onChange, + min = 0, + max = 100, + step = 1, + minLabel, + maxLabel, + showValueBubble = true, +}: RangeProps) => { + const rangeRef = React.useRef(null); + const valueRef = React.useRef(null); + + useEffect(() => { + if (rangeRef.current) { + const rangeElement = rangeRef.current; + + // Update value bubble position if it exists + if (showValueBubble && valueRef.current) { + const valueElement = valueRef.current; + const inputWidth = rangeElement.offsetWidth; + const thumbWidth = 16; // Match the --slider-thumb-size CSS variable + const position = + ((value - min) / (max - min)) * (inputWidth - thumbWidth) + thumbWidth / 2; + valueElement.style.left = `${position}px`; + } + + // Calculate percentage for gradient + const percentage = ((value - min) / (max - min)) * 100; + rangeElement.style.background = `linear-gradient(to right, var(--color-slider-track) 0%, var(--color-slider-track) ${percentage}%, var(--button-bg) ${percentage}%, var(--button-bg) 100%)`; + } + }, [value, min, max, showValueBubble]); + + return ( + + ); +}; diff --git a/src/frontend/src/ui/SettingsDialog.tsx b/src/frontend/src/ui/SettingsDialog.tsx index 3ee95ce..2eb532c 100644 --- a/src/frontend/src/ui/SettingsDialog.tsx +++ b/src/frontend/src/ui/SettingsDialog.tsx @@ -1,5 +1,6 @@ import React, { useState, useCallback, useEffect } from "react"; -import { Dialog, Range } from "@atyrode/excalidraw"; +import { Dialog } from "@atyrode/excalidraw"; +import { Range } from "./Range"; import { UserSettings, DEFAULT_SETTINGS } from "../types/settings"; import "./SettingsDialog.scss"; @@ -69,23 +70,20 @@ const SettingsDialog: React.FC = ({ Embed Lock Debounce Time: {settings.embedLockDebounceTime}ms
- handleEmbedLockDebounceTimeChange( - // Map 0-100 range to 150-5000ms - Math.round(150 + (value / 100) * 4850) - )} - appState={{ - currentItemOpacity: - // Map 150-5000ms to 0-100 range - Math.round(((settings.embedLockDebounceTime || 350) - 150) / 4850 * 100) - }} - elements={[]} - testId="embed-lock-debounce-time" - /> -
-
- 150ms - 5000ms + handleEmbedLockDebounceTimeChange( + // Map 0-100 range to 150-5000ms + Math.round(150 + (value / 100) * 4850) + )} + min={0} + max={100} + step={1} + label="Embed Lock Time" + minLabel="150ms" + maxLabel="5000ms" + showValueBubble={false} + />
From 1c48b3ae570c6e15f50dfe0c38ae5b1f04361d07 Mon Sep 17 00:00:00 2001 From: Alex TYRODE Date: Mon, 28 Apr 2025 01:39:33 +0000 Subject: [PATCH 07/10] refactor: apply BEM naming convention to Range component styles - Updated Range.scss and Range.tsx to implement BEM naming conventions for improved readability and maintainability. - Introduced new CSS variables for track colors to enhance customization. - Adjusted styles for input and labels to align with the new structure, ensuring consistent UI presentation. --- src/frontend/src/ui/Range.scss | 126 ++++++++++++++++++--------------- src/frontend/src/ui/Range.tsx | 17 +++-- 2 files changed, 75 insertions(+), 68 deletions(-) diff --git a/src/frontend/src/ui/Range.scss b/src/frontend/src/ui/Range.scss index 876356c..a8748c3 100644 --- a/src/frontend/src/ui/Range.scss +++ b/src/frontend/src/ui/Range.scss @@ -1,71 +1,79 @@ :root { --slider-thumb-size: 16px; + --range-track-filled: #cc6d24; + --range-track-unfilled: #525252; + --range-thumb-color: #a4571b; /* Slightly lighter than track-filled for contrast */ } -.control-label { - display: flex; - flex-direction: column; - width: 100%; - font-size: 0.9rem; - color: var(--text-primary-color); -} +.range { + &__control-label { + display: flex; + flex-direction: column; + width: 100%; + font-size: 0.9rem; + color: var(--text-primary-color); + } -.range-wrapper { - position: relative; - padding-top: 10px; - padding-bottom: 25px; - width: 100%; -} + &__wrapper { + position: relative; + padding-top: 10px; + padding-bottom: 25px; + width: 100%; + } -.range-input { - width: 100%; - height: 4px; - -webkit-appearance: none; - background: var(--color-slider-track); - border-radius: 2px; - outline: none; -} + &__input { + width: 100%; + height: 4px; + -webkit-appearance: none; + background: var(--range-track-filled); + border-radius: 2px; + outline: none; -.range-input::-webkit-slider-thumb { - -webkit-appearance: none; - appearance: none; - width: var(--slider-thumb-size); - height: var(--slider-thumb-size); - background: var(--color-slider-thumb); - border-radius: 50%; - cursor: pointer; - border: none; -} + &::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: var(--slider-thumb-size); + height: var(--slider-thumb-size); + background: var(--range-thumb-color); + border-radius: 50%; + cursor: pointer; + border: none; + } -.range-input::-moz-range-thumb { - width: var(--slider-thumb-size); - height: var(--slider-thumb-size); - background: var(--color-slider-thumb); - border-radius: 50%; - cursor: pointer; - border: none; -} + &::-moz-range-thumb { + width: var(--slider-thumb-size); + height: var(--slider-thumb-size); + background: var(--range-thumb-color); + border-radius: 50%; + cursor: pointer; + border: none; + } + } -.value-bubble { - position: absolute; - bottom: 0; - transform: translateX(-50%); - font-size: 12px; - color: var(--text-primary-color); -} + &__value-bubble { + position: absolute; + bottom: 0; + transform: translateX(-50%); + font-size: 12px; + color: var(--text-primary-color); + } -.min-label, .zero-label { - position: absolute; - bottom: 0; - left: 4px; - font-size: 12px; - color: var(--text-primary-color); -} + &__label { + position: absolute; + bottom: 0; + font-size: 12px; + color: var(--text-primary-color); + + &--min { + left: 4px; + } + + &--zero { + left: 4px; + } -.max-label { - position: absolute; - bottom: 0; - right: 4px; - font-size: 12px; - color: var(--text-primary-color); + &--max { + right: 4px; + } + } } diff --git a/src/frontend/src/ui/Range.tsx b/src/frontend/src/ui/Range.tsx index ef9bf02..a95dbb5 100644 --- a/src/frontend/src/ui/Range.tsx +++ b/src/frontend/src/ui/Range.tsx @@ -8,7 +8,6 @@ export type RangeProps = { min?: number; max?: number; step?: number; - label?: string; minLabel?: string; maxLabel?: string; showValueBubble?: boolean; @@ -43,13 +42,13 @@ export const Range = ({ // Calculate percentage for gradient const percentage = ((value - min) / (max - min)) * 100; - rangeElement.style.background = `linear-gradient(to right, var(--color-slider-track) 0%, var(--color-slider-track) ${percentage}%, var(--button-bg) ${percentage}%, var(--button-bg) 100%)`; + rangeElement.style.background = `linear-gradient(to right, var(--range-track-filled) 0%, var(--range-track-filled) ${percentage}%, var(--range-track-unfilled) ${percentage}%, var(--range-track-unfilled) 100%)`; } }, [value, min, max, showValueBubble]); return ( -