feat: replace mui with new style

This commit is contained in:
David Ralph
2026-01-25 18:12:05 +00:00
parent 01fcdbf9c7
commit 874866bf73
33 changed files with 1338 additions and 555 deletions

View File

@@ -1,7 +1,6 @@
import variables from 'config/variables';
import { useState, memo } from 'react';
import { TextareaAutosize } from '@mui/material';
import { MdAddLink, MdClose } from 'react-icons/md';
import { Tooltip } from 'components/Elements';
import { Button } from 'components/Elements';
@@ -26,22 +25,24 @@ function AddModal({ urlError, iconError, addLink, closeModal, edit, editData, ed
</Tooltip>
</div>
<div className="quicklinkModalTextbox">
<TextareaAutosize
maxRows={1}
<input
type="text"
className="text-field-input"
placeholder={variables.getMessage('widgets.quicklinks.name')}
value={name}
onChange={(e) => setName(e.target.value.replace(/(\r\n|\n|\r)/gm, ''))}
style={{ gridColumn: 'span 2' }}
/>
<TextareaAutosize
maxRows={10}
<input
type="text"
className="text-field-input"
placeholder={variables.getMessage('widgets.quicklinks.url')}
value={url}
onChange={(e) => setUrl(e.target.value.replace(/(\r\n|\n|\r)/gm, ''))}
/>
<TextareaAutosize
maxRows={10}
maxLines={1}
<input
type="text"
className="text-field-input"
placeholder={variables.getMessage('widgets.quicklinks.icon')}
value={icon}
onChange={(e) => setIcon(e.target.value.replace(/(\r\n|\n|\r)/gm, ''))}

View File

@@ -1,9 +1,11 @@
import variables from 'config/variables';
import { memo, useState, useCallback } from 'react';
import { Checkbox as CheckboxUI, FormControlLabel } from '@mui/material';
import { MdCheck } from 'react-icons/md';
import EventBus from 'utils/eventbus';
import './Checkbox.scss';
const Checkbox = memo((props) => {
const [checked, setChecked] = useState(localStorage.getItem(props.name) === 'true');
@@ -32,19 +34,23 @@ const Checkbox = memo((props) => {
}, [checked, props]);
return (
<FormControlLabel
control={
<CheckboxUI
name={props.name}
color="primary"
className="checkbox"
checked={checked}
onChange={handleChange}
disabled={props.disabled || false}
/>
}
label={props.text}
/>
<label className={`checkbox-wrapper ${props.disabled ? 'disabled' : ''}`}>
<span className="checkbox-label">{props.text}</span>
<div
className={`checkbox-box ${checked ? 'checked' : ''}`}
onClick={props.disabled ? undefined : handleChange}
>
{checked && <MdCheck />}
</div>
<input
type="checkbox"
name={props.name}
checked={checked}
onChange={handleChange}
disabled={props.disabled || false}
className="checkbox-input"
/>
</label>
);
});

View File

@@ -0,0 +1,63 @@
@use 'scss/variables' as *;
.checkbox-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
cursor: pointer;
padding: 8px 0;
&.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.checkbox-label {
flex: 1;
@include themed {
color: t($color);
}
}
.checkbox-box {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 6px;
transition: 0.2s ease;
cursor: pointer;
flex-shrink: 0;
@include themed {
border: 2px solid t($modal-sidebarActive);
background: t($modal-sidebar);
color: t($color);
&:hover {
border-color: t($color);
}
}
&.checked {
@include themed {
background: t($link);
border-color: t($link);
}
}
svg {
font-size: 18px;
color: white;
}
}
.checkbox-input {
position: absolute;
opacity: 0;
pointer-events: none;
}
}

View File

@@ -1,12 +1,7 @@
import { useState, memo } from 'react';
import { useState, memo, useRef, useEffect } from 'react';
import { MdExpandMore, MdClose } from 'react-icons/md';
import Box from '@mui/material/Box';
import OutlinedInput from '@mui/material/OutlinedInput';
import InputLabel from '@mui/material/InputLabel';
import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';
import Select from '@mui/material/Select';
import Chip from '@mui/material/Chip';
import './ChipSelect.scss';
function ChipSelect({ label, options, onChange }) {
let start = (localStorage.getItem('apiCategories') || '').split(',');
@@ -14,47 +9,88 @@ function ChipSelect({ label, options, onChange }) {
start = [];
}
const [optionsSelected, setoptionsSelected] = useState(start);
const [optionsSelected, setOptionsSelected] = useState(start);
const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef(null);
const handleChange = (event) => {
const {
target: { value },
} = event;
setoptionsSelected(typeof value === 'string' ? value.split(',') : value);
localStorage.setItem('apiCategories', value);
useEffect(() => {
const handleClickOutside = (event) => {
if (containerRef.current && !containerRef.current.contains(event.target)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const handleToggle = (optionName) => {
let newSelected;
if (optionsSelected.includes(optionName)) {
newSelected = optionsSelected.filter((item) => item !== optionName);
} else {
newSelected = [...optionsSelected, optionName];
}
setOptionsSelected(newSelected);
localStorage.setItem('apiCategories', newSelected.join(','));
// Call parent onChange if provided
if (onChange) {
onChange(value);
onChange(newSelected);
}
};
const handleRemoveChip = (e, optionName) => {
e.stopPropagation();
handleToggle(optionName);
};
return (
<FormControl>
<InputLabel id="chipSelect-label">{label}</InputLabel>
<Select
labelId="chipSelect-label"
id="chipSelect"
multiple
value={optionsSelected}
onChange={handleChange}
input={<OutlinedInput id="select-multiple-chip" label={label} />}
renderValue={(optionsSelected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{optionsSelected.map((value) => (
<Chip key={value} label={value} />
))}
</Box>
)}
>
{options.map((option) => (
<MenuItem key={option.name} value={option.name}>
{option.name.charAt(0).toUpperCase() + option.name.slice(1)}{' '}
{option.count && `(${option.count})`}
</MenuItem>
))}
</Select>
</FormControl>
<div className="chipSelect" ref={containerRef}>
{label && <label className="chipSelect-label">{label}</label>}
<div className="chipSelect-control" onClick={() => setIsOpen(!isOpen)}>
<div className="chipSelect-value">
{optionsSelected.length === 0 ? (
<span className="chipSelect-placeholder">Select options...</span>
) : (
<div className="chipSelect-chips">
{optionsSelected.map((value) => (
<span key={value} className="chipSelect-chip">
{value.charAt(0).toUpperCase() + value.slice(1)}
<button
type="button"
className="chipSelect-chip-remove"
onClick={(e) => handleRemoveChip(e, value)}
>
<MdClose />
</button>
</span>
))}
</div>
)}
</div>
<MdExpandMore className={`chipSelect-arrow ${isOpen ? 'open' : ''}`} />
</div>
{isOpen && (
<div className="chipSelect-dropdown">
{options.map((option) => (
<div
key={option.name}
className={`chipSelect-option ${optionsSelected.includes(option.name) ? 'selected' : ''}`}
onClick={() => handleToggle(option.name)}
>
<span className="chipSelect-option-checkbox">
{optionsSelected.includes(option.name) && '✓'}
</span>
<span className="chipSelect-option-label">
{option.name.charAt(0).toUpperCase() + option.name.slice(1)}
{option.count && ` (${option.count})`}
</span>
</div>
))}
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,178 @@
@use 'scss/variables' as *;
.chipSelect {
position: relative;
width: 300px;
margin-top: 10px;
.chipSelect-label {
display: block;
margin-bottom: 8px;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
@include themed {
color: t($subColor);
}
}
.chipSelect-control {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 56px;
padding: 8px 12px;
cursor: pointer;
transition: 0.2s ease;
@include themed {
background: t($modal-sidebar);
border: 1px solid t($modal-sidebarActive);
border-radius: t($borderRadius);
color: t($color);
&:hover {
border-color: t($color);
}
}
}
.chipSelect-value {
flex: 1;
min-width: 0;
}
.chipSelect-placeholder {
@include themed {
color: t($subColor);
}
}
.chipSelect-chips {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.chipSelect-chip {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
font-size: 13px;
text-transform: capitalize;
@include themed {
background: t($modal-sidebarActive);
border-radius: calc(t($borderRadius) / 2);
color: t($color);
}
.chipSelect-chip-remove {
display: flex;
align-items: center;
justify-content: center;
padding: 2px;
margin-left: 2px;
border: none;
background: transparent;
cursor: pointer;
border-radius: 50%;
transition: 0.2s ease;
@include themed {
color: t($subColor);
&:hover {
background: rgba(255, 255, 255, 0.1);
color: t($color);
}
}
svg {
font-size: 14px;
}
}
}
.chipSelect-arrow {
flex-shrink: 0;
font-size: 24px;
transition: transform 0.2s ease;
@include themed {
color: t($subColor);
}
&.open {
transform: rotate(180deg);
}
}
.chipSelect-dropdown {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
max-height: 250px;
overflow-y: auto;
z-index: 100;
@include themed {
background: t($modal-background);
border: 1px solid t($modal-sidebarActive);
border-radius: t($borderRadius);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
.chipSelect-option {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
cursor: pointer;
transition: 0.2s ease;
@include themed {
color: t($color);
&:hover {
background: t($modal-sidebarActive);
}
&.selected {
background: t($modal-sidebar);
}
}
.chipSelect-option-checkbox {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
font-size: 12px;
font-weight: bold;
@include themed {
border: 2px solid t($modal-sidebarActive);
border-radius: 4px;
color: t($color);
}
}
&.selected .chipSelect-option-checkbox {
@include themed {
background: t($modal-sidebarActive);
border-color: t($color);
}
}
.chipSelect-option-label {
flex: 1;
}
}
}

View File

@@ -1,69 +1,88 @@
import variables from 'config/variables';
import { memo, useState, useCallback, useRef } from 'react';
import { InputLabel, MenuItem, FormControl, Select } from '@mui/material';
import { memo, useState, useCallback, useRef, useEffect } from 'react';
import { MdExpandMore } from 'react-icons/md';
import EventBus from 'utils/eventbus';
import './Dropdown.scss';
const Dropdown = memo((props) => {
const [value, setValue] = useState(
localStorage.getItem(props.name) || props.items[0].value,
localStorage.getItem(props.name) || props.items[0]?.value,
);
const dropdown = useRef();
const [isOpen, setIsOpen] = useState(false);
const containerRef = useRef(null);
const onChange = useCallback((e) => {
const newValue = e.target.value;
if (newValue === variables.getMessage('modals.main.loading')) {
return;
}
variables.stats.postEvent('setting', `${props.name} from ${value} to ${newValue}`);
setValue(newValue);
if (!props.noSetting) {
localStorage.setItem(props.name, newValue);
localStorage.setItem(props.name2, props.value2);
}
if (props.onChange) {
props.onChange(newValue);
}
if (props.element) {
if (!document.querySelector(props.element)) {
document.querySelector('.reminder-info').style.display = 'flex';
return localStorage.setItem('showReminder', true);
useEffect(() => {
const handleClickOutside = (event) => {
if (containerRef.current && !containerRef.current.contains(event.target)) {
setIsOpen(false);
}
}
};
EventBus.emit('refresh', props.category);
}, [value, props]);
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
const onChange = useCallback(
(newValue) => {
if (newValue === variables.getMessage('modals.main.loading')) {
return;
}
variables.stats.postEvent('setting', `${props.name} from ${value} to ${newValue}`);
setValue(newValue);
setIsOpen(false);
if (!props.noSetting) {
localStorage.setItem(props.name, newValue);
localStorage.setItem(props.name2, props.value2);
}
if (props.onChange) {
props.onChange(newValue);
}
if (props.element) {
if (!document.querySelector(props.element)) {
document.querySelector('.reminder-info').style.display = 'flex';
return localStorage.setItem('showReminder', true);
}
}
EventBus.emit('refresh', props.category);
},
[value, props],
);
const id = 'dropdown' + props.name;
const label = props.label || '';
const selectedItem = props.items.find((item) => item?.value === value);
return (
<FormControl fullWidth className={id}>
<InputLabel id={id}>{label}</InputLabel>
<Select
labelId={id}
id={props.name}
value={value}
label={label}
onChange={onChange}
ref={dropdown}
key={id}
>
{props.items.map((item) =>
item !== null ? (
<MenuItem key={id + item.value} value={item.value}>
{item.text}
</MenuItem>
) : null,
)}
</Select>
</FormControl>
<div className={`dropdown ${id}`} ref={containerRef}>
{label && <label className="dropdown-label">{label}</label>}
<div className="dropdown-control" onClick={() => setIsOpen(!isOpen)}>
<span className="dropdown-value">{selectedItem?.text || value}</span>
<MdExpandMore className={`dropdown-arrow ${isOpen ? 'open' : ''}`} />
</div>
{isOpen && (
<div className="dropdown-menu">
{props.items.map((item) =>
item !== null ? (
<div
key={id + item.value}
className={`dropdown-option ${value === item.value ? 'selected' : ''}`}
onClick={() => onChange(item.value)}
>
{item.text}
</div>
) : null,
)}
</div>
)}
</div>
);
});

View File

@@ -0,0 +1,98 @@
@use 'scss/variables' as *;
.dropdown {
position: relative;
width: 300px;
margin-top: 10px;
.dropdown-label {
display: block;
margin-bottom: 8px;
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
@include themed {
color: t($subColor);
}
}
.dropdown-control {
display: flex;
align-items: center;
justify-content: space-between;
height: 56px;
padding: 0 16px;
cursor: pointer;
transition: 0.2s ease;
@include themed {
background: t($modal-sidebar);
border: 1px solid t($modal-sidebarActive);
border-radius: t($borderRadius);
color: t($color);
&:hover {
border-color: t($color);
}
}
}
.dropdown-value {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.dropdown-arrow {
flex-shrink: 0;
font-size: 24px;
transition: transform 0.2s ease;
@include themed {
color: t($subColor);
}
&.open {
transform: rotate(180deg);
}
}
.dropdown-menu {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
max-height: 250px;
overflow-y: auto;
z-index: 100;
@include themed {
background: t($modal-background);
border: 1px solid t($modal-sidebarActive);
border-radius: t($borderRadius);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
}
.dropdown-option {
padding: 12px 16px;
cursor: pointer;
transition: 0.2s ease;
@include themed {
color: t($color);
&:hover {
background: t($modal-sidebarActive);
}
&.selected {
background: t($modal-sidebar);
font-weight: 500;
}
}
}
}

View File

@@ -1,85 +1,86 @@
import variables from 'config/variables';
import { memo, useState, useCallback } from 'react';
import { useTranslation } from 'contexts/TranslationContext';
import {
Radio as RadioUI,
RadioGroup,
FormControlLabel,
FormControl,
FormLabel,
} from '@mui/material';
import EventBus from 'utils/eventbus';
import './Radio.scss';
const Radio = memo((props) => {
const { changeLanguage } = useTranslation();
const [value, setValue] = useState(localStorage.getItem(props.name));
const handleChange = useCallback(async (e) => {
const newValue = e.target.value;
const handleChange = useCallback(
async (newValue) => {
if (newValue === 'loading') {
return;
}
if (newValue === 'loading') {
return;
}
if (props.name === 'language') {
changeLanguage(newValue);
setValue(newValue);
if (props.name === 'language') {
// Use context to change language directly - no EventBus needed
changeLanguage(newValue);
variables.stats.postEvent('setting', `${props.name} from ${value} to ${newValue}`);
if (props.onChange) {
props.onChange(newValue);
}
EventBus.emit('refresh', props.category);
return;
}
localStorage.setItem(props.name, newValue);
setValue(newValue);
variables.stats.postEvent('setting', `${props.name} from ${value} to ${newValue}`);
if (props.onChange) {
props.onChange(newValue);
}
EventBus.emit('refresh', props.category);
return;
}
variables.stats.postEvent('setting', `${props.name} from ${value} to ${newValue}`);
localStorage.setItem(props.name, newValue);
setValue(newValue);
if (props.onChange) {
props.onChange(newValue);
}
variables.stats.postEvent('setting', `${props.name} from ${value} to ${newValue}`);
if (props.element) {
if (!document.querySelector(props.element)) {
document.querySelector('.reminder-info').style.display = 'flex';
return localStorage.setItem('showReminder', true);
if (props.element) {
if (!document.querySelector(props.element)) {
document.querySelector('.reminder-info').style.display = 'flex';
return localStorage.setItem('showReminder', true);
}
}
}
EventBus.emit('refresh', props.category);
}, [value, props, changeLanguage]);
EventBus.emit('refresh', props.category);
},
[value, props, changeLanguage],
);
return (
<FormControl component="fieldset">
<FormLabel
className={props.smallTitle ? 'radio-title-small' : 'radio-title'}
component="legend"
>
{props.title}
</FormLabel>
<RadioGroup
aria-label={props.name}
name={props.name}
onChange={handleChange}
value={value}
>
<div className="radio-group">
{props.title && (
<legend className={props.smallTitle ? 'radio-title-small' : 'radio-title'}>
{props.title}
</legend>
)}
<div className="radio-options" role="radiogroup" aria-label={props.name}>
{props.options.map((option) => (
<FormControlLabel
value={option.value}
control={<RadioUI />}
label={option.name}
<label
key={option.value}
/>
className={`radio-option ${value === option.value ? 'selected' : ''}`}
onClick={() => handleChange(option.value)}
>
<span className="radio-label">{option.name}</span>
<div className="radio-circle">
{value === option.value && <div className="radio-dot" />}
</div>
<input
type="radio"
name={props.name}
value={option.value}
checked={value === option.value}
onChange={() => handleChange(option.value)}
className="radio-input"
/>
</label>
))}
</RadioGroup>
</FormControl>
</div>
</div>
);
});

View File

@@ -0,0 +1,102 @@
@use 'scss/variables' as *;
.radio-group {
width: 100%;
margin-top: 10px;
.radio-title {
font-weight: bold;
font-size: 1.17rem;
margin-bottom: 12px;
display: block;
@include themed {
color: t($color);
}
}
.radio-title-small {
font-weight: bold;
font-size: 1rem;
margin-bottom: 10px;
display: block;
@include themed {
color: t($color);
}
}
.radio-options {
display: flex;
flex-direction: column;
gap: 10px;
}
.radio-option {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
padding: 16px 20px;
transition: 0.2s ease;
@include themed {
background: t($modal-sidebar);
border-radius: t($borderRadius);
box-shadow: 0 0 0 1px t($modal-sidebarActive);
&:hover {
background: t($modal-secondaryColour);
}
}
&.selected .radio-circle {
@include themed {
border-color: t($link);
}
}
}
.radio-label {
flex: 1;
font-size: 15px;
@include themed {
color: t($color);
}
}
.radio-circle {
display: flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
border-radius: 50%;
transition: 0.2s ease;
cursor: pointer;
flex-shrink: 0;
margin-left: 20px;
@include themed {
border: 2px solid t($modal-sidebarActive);
background: t($modal-secondaryColour);
}
}
.radio-dot {
width: 12px;
height: 12px;
border-radius: 50%;
@include themed {
background: t($link);
}
}
.radio-input {
position: absolute;
opacity: 0;
pointer-events: none;
}
}

View File

@@ -0,0 +1,23 @@
import { memo } from 'react';
import { MdSearch } from 'react-icons/md';
import './SearchInput.scss';
const SearchInput = memo(({ value, onChange, placeholder, fullWidth }) => {
return (
<div className={`search-input-container${fullWidth ? ' full-width' : ''}`}>
<MdSearch className="search-input-icon" />
<input
type="text"
className="search-input-field"
value={value}
onChange={onChange}
placeholder={placeholder}
/>
</div>
);
});
SearchInput.displayName = 'SearchInput';
export { SearchInput as default, SearchInput };

View File

@@ -0,0 +1,48 @@
@use 'scss/variables' as *;
.search-input-container {
position: relative;
display: flex;
align-items: center;
width: 250px;
&.full-width {
width: 100%;
}
.search-input-icon {
position: absolute;
left: 16px;
font-size: 20px;
pointer-events: none;
@include themed {
color: t($subColor);
}
}
.search-input-field {
width: 100%;
height: 48px;
padding: 0 16px 0 44px;
font-size: 15px;
outline: none;
transition: 0.2s ease;
@include themed {
background: t($modal-sidebar);
border: 1px solid t($modal-sidebarActive);
border-radius: 24px;
color: t($color);
&:hover,
&:focus {
border-color: t($color);
}
&::placeholder {
color: t($subColor);
}
}
}
}

View File

@@ -0,0 +1 @@
export * from './SearchInput';

View File

@@ -1,23 +1,19 @@
import variables from 'config/variables';
import { memo, useState, useCallback } from 'react';
import { toast } from 'react-toastify';
import { Slider } from '@mui/material';
import { MdRefresh } from 'react-icons/md';
import EventBus from 'utils/eventbus';
import './Slider.scss';
const SliderComponent = memo((props) => {
const [value, setValue] = useState(localStorage.getItem(props.name) || props.default);
const handleChange = useCallback((e, text) => {
let newValue = e.target.value;
newValue = Number(newValue);
if (text) {
if (newValue === '') {
setValue(0);
return;
}
const handleChange = useCallback(
(e) => {
let newValue = e.target.value;
newValue = Number(newValue);
if (newValue > props.max) {
newValue = props.max;
@@ -26,20 +22,21 @@ const SliderComponent = memo((props) => {
if (newValue < props.min) {
newValue = props.min;
}
}
localStorage.setItem(props.name, newValue);
setValue(newValue);
localStorage.setItem(props.name, newValue);
setValue(newValue);
if (props.element) {
if (!document.querySelector(props.element)) {
document.querySelector('.reminder-info').style.display = 'flex';
return localStorage.setItem('showReminder', true);
if (props.element) {
if (!document.querySelector(props.element)) {
document.querySelector('.reminder-info').style.display = 'flex';
return localStorage.setItem('showReminder', true);
}
}
}
EventBus.emit('refresh', props.category);
}, [props]);
EventBus.emit('refresh', props.category);
},
[props],
);
const resetItem = useCallback(() => {
handleChange({
@@ -50,9 +47,12 @@ const SliderComponent = memo((props) => {
toast(variables.getMessage('toasts.reset'));
}, [handleChange, props.default]);
const percentage =
((Number(value) - Number(props.min)) / (Number(props.max) - Number(props.min))) * 100;
return (
<>
<span className={'sliderTitle'}>
<div className="slider-container">
<span className="sliderTitle">
{props.title}
<span>{Number(value)}</span>
<span className="link" onClick={resetItem}>
@@ -60,18 +60,34 @@ const SliderComponent = memo((props) => {
{variables.getMessage('modals.main.settings.buttons.reset')}
</span>
</span>
<Slider
value={Number(value)}
onChange={handleChange}
valueLabelDisplay="auto"
default={Number(props.default)}
min={Number(props.min)}
max={Number(props.max)}
step={Number(props.step) || 1}
getAriaValueText={(value) => `${value}`}
marks={props.marks || []}
/>
</>
<div className="slider-wrapper">
<input
type="range"
className="slider-input"
value={Number(value)}
onChange={handleChange}
min={Number(props.min)}
max={Number(props.max)}
step={Number(props.step) || 1}
style={{ '--slider-percentage': `${percentage}%` }}
/>
{props.marks && props.marks.length > 0 && (
<div className="slider-marks">
{props.marks.map((mark) => (
<span
key={mark.value}
className="slider-mark"
style={{
left: `${((mark.value - Number(props.min)) / (Number(props.max) - Number(props.min))) * 100}%`,
}}
>
{mark.label}
</span>
))}
</div>
)}
</div>
</div>
);
});

View File

@@ -0,0 +1,123 @@
@use 'scss/variables' as *;
.slider-container {
width: 300px;
margin-bottom: 30px;
.sliderTitle {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 15px;
@include themed {
color: t($color);
}
> span:first-of-type {
font-weight: 600;
}
.link {
display: flex;
align-items: center;
gap: 5px;
margin-left: auto;
cursor: pointer;
font-size: 14px;
@include themed {
color: t($link);
}
&:hover {
opacity: 0.8;
}
svg {
font-size: 16px;
}
}
}
.slider-wrapper {
position: relative;
width: 100%;
}
.slider-input {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 6px;
border-radius: 3px;
outline: none;
cursor: pointer;
@include themed {
background: linear-gradient(
to right,
t($link) 0%,
t($link) var(--slider-percentage),
t($modal-sidebarActive) var(--slider-percentage),
t($modal-sidebarActive) 100%
);
}
&::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 50%;
cursor: pointer;
transition: 0.2s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
@include themed {
background: t($color);
border: 2px solid t($link);
}
&:hover {
transform: scale(1.1);
}
}
&::-moz-range-thumb {
width: 20px;
height: 20px;
border-radius: 50%;
cursor: pointer;
transition: 0.2s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.2);
border: none;
@include themed {
background: t($color);
border: 2px solid t($link);
}
&:hover {
transform: scale(1.1);
}
}
}
.slider-marks {
position: relative;
width: 100%;
height: 20px;
margin-top: 8px;
.slider-mark {
position: absolute;
transform: translateX(-50%);
font-size: 12px;
@include themed {
color: t($subColor);
}
}
}
}

View File

@@ -1,9 +1,10 @@
import variables from 'config/variables';
import { memo, useState, useCallback } from 'react';
import { Switch as SwitchUI, FormControlLabel } from '@mui/material';
import EventBus from 'utils/eventbus';
import './Switch.scss';
const Switch = memo((props) => {
const [checked, setChecked] = useState(localStorage.getItem(props.name) === 'true');
@@ -32,18 +33,19 @@ const Switch = memo((props) => {
}, [checked, props]);
return (
<FormControlLabel
control={
<SwitchUI
name={props.name}
color="primary"
checked={checked}
onChange={handleChange}
/>
}
label={props.header ? '' : props.text}
labelPlacement="start"
/>
<label className="switch-wrapper">
{!props.header && <span className="switch-label">{props.text}</span>}
<div className={`switch-track ${checked ? 'checked' : ''}`} onClick={handleChange}>
<div className="switch-thumb" />
</div>
<input
type="checkbox"
name={props.name}
checked={checked}
onChange={handleChange}
className="switch-input"
/>
</label>
);
});

View File

@@ -0,0 +1,63 @@
@use 'scss/variables' as *;
.switch-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
cursor: pointer;
padding: 8px 0;
.switch-label {
flex: 1;
@include themed {
color: t($color);
}
}
.switch-track {
position: relative;
width: 52px;
height: 32px;
border-radius: 16px;
cursor: pointer;
transition: 0.2s ease;
flex-shrink: 0;
@include themed {
background: t($modal-sidebarActive);
}
&.checked {
@include themed {
background: t($link);
}
.switch-thumb {
transform: translateX(20px);
}
}
}
.switch-thumb {
position: absolute;
top: 4px;
left: 4px;
width: 24px;
height: 24px;
border-radius: 50%;
transition: 0.2s ease;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
@include themed {
background: t($color);
}
}
.switch-input {
position: absolute;
opacity: 0;
pointer-events: none;
}
}

View File

@@ -1,79 +1,83 @@
import variables from 'config/variables';
import { memo, useState, useCallback } from 'react';
import { toast } from 'react-toastify';
import { TextField } from '@mui/material';
import { MdRefresh } from 'react-icons/md';
import EventBus from 'utils/eventbus';
import './Text.scss';
const Text = memo((props) => {
const [value, setValue] = useState(localStorage.getItem(props.name) || '');
const { name, upperCaseFirst, element, category, onChange, title, textarea, customcss, placeholder } = props;
const defaultValue = props.default;
const [value, setValue] = useState(localStorage.getItem(name) || '');
const handleChange = useCallback((e) => {
let { value } = e.target;
const handleChange = useCallback(
(e) => {
let newValue = e.target.value;
// Alex wanted font to work with montserrat and Montserrat, so I made it work
if (props.upperCaseFirst === true) {
value = value.charAt(0).toUpperCase() + value.slice(1);
}
localStorage.setItem(props.name, value);
setValue(value);
// Call parent onChange if provided
if (props.onChange) {
props.onChange(value);
}
if (props.element) {
if (!document.querySelector(props.element)) {
document.querySelector('.reminder-info').style.display = 'flex';
return localStorage.setItem('showReminder', true);
if (upperCaseFirst === true) {
newValue = newValue.charAt(0).toUpperCase() + newValue.slice(1);
}
}
EventBus.emit('refresh', props.category);
}, [props.name, props.upperCaseFirst, props.element, props.category, props.onChange]);
localStorage.setItem(name, newValue);
setValue(newValue);
if (onChange) {
onChange(newValue);
}
if (element) {
if (!document.querySelector(element)) {
document.querySelector('.reminder-info').style.display = 'flex';
return localStorage.setItem('showReminder', true);
}
}
EventBus.emit('refresh', category);
},
[name, upperCaseFirst, element, category, onChange],
);
const resetItem = useCallback(() => {
handleChange({
target: {
value: props.default || '',
value: defaultValue || '',
},
});
toast(variables.getMessage('toasts.reset'));
}, [handleChange, props.default]);
}, [handleChange, defaultValue]);
return (
<>
{props.textarea === true ? (
<TextField
label={props.title}
value={value}
onChange={handleChange}
varient="outlined"
className={props.customcss ? 'customcss' : ''}
multiline
spellCheck={false}
minRows={4}
maxRows={10}
InputLabelProps={{ shrink: true }}
/>
<div className="text-field-container">
{textarea === true ? (
<div className={`text-field ${customcss ? 'customcss' : ''}`}>
{title && <label className="text-field-label">{title}</label>}
<textarea
value={value}
onChange={handleChange}
spellCheck={false}
rows={4}
className="text-field-textarea"
/>
</div>
) : (
<TextField
label={props.title}
value={value}
onChange={handleChange}
varient="outlined"
InputLabelProps={{ shrink: true }}
placeholder={props.placeholder || ''}
/>
<div className="text-field">
{title && <label className="text-field-label">{title}</label>}
<input
type="text"
value={value}
onChange={handleChange}
placeholder={placeholder || ''}
className="text-field-input"
/>
</div>
)}
<span className="link" onClick={resetItem}>
<MdRefresh />
{variables.getMessage('modals.main.settings.buttons.reset')}
</span>
</>
</div>
);
});

View File

@@ -0,0 +1,102 @@
@use 'scss/variables' as *;
.text-field-container {
display: flex;
flex-direction: column;
gap: 10px;
width: 300px;
margin-top: 10px;
.link {
display: flex;
align-items: center;
gap: 5px;
cursor: pointer;
font-size: 14px;
width: fit-content;
@include themed {
color: t($link);
}
&:hover {
opacity: 0.8;
}
svg {
font-size: 16px;
}
}
}
.text-field {
display: flex;
flex-direction: column;
gap: 8px;
.text-field-label {
font-size: 12px;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
@include themed {
color: t($subColor);
}
}
.text-field-input {
height: 56px;
padding: 0 16px;
font-size: 16px;
outline: none;
transition: 0.2s ease;
@include themed {
background: t($modal-sidebar);
border: 1px solid t($modal-sidebarActive);
border-radius: t($borderRadius);
color: t($color);
&:hover,
&:focus {
border-color: t($color);
}
&::placeholder {
color: t($subColor);
}
}
}
.text-field-textarea {
padding: 16px;
font-size: 16px;
outline: none;
resize: vertical;
min-height: 100px;
transition: 0.2s ease;
@include themed {
background: t($modal-sidebar);
border: 1px solid t($modal-sidebarActive);
border-radius: t($borderRadius);
color: t($color);
&:hover,
&:focus {
border-color: t($color);
}
&::placeholder {
color: t($subColor);
}
}
}
&.customcss .text-field-textarea {
font-family: Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console',
'Lucida Sans Typewriter', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Liberation Mono',
'Nimbus Mono L', Monaco, 'Courier New', Courier, monospace !important;
}
}

View File

@@ -0,0 +1,57 @@
import { memo, useRef, useEffect, useCallback } from 'react';
import './Textarea.scss';
const Textarea = memo(({ value, onChange, placeholder, minRows = 1, maxRows, className, style, readOnly }) => {
const textareaRef = useRef(null);
const adjustHeight = useCallback(() => {
const textarea = textareaRef.current;
if (!textarea) return;
// Reset height to auto to get the correct scrollHeight
textarea.style.height = 'auto';
// Calculate line height
const computedStyle = window.getComputedStyle(textarea);
const lineHeight = parseInt(computedStyle.lineHeight) || 24;
const paddingTop = parseInt(computedStyle.paddingTop) || 0;
const paddingBottom = parseInt(computedStyle.paddingBottom) || 0;
// Calculate min and max heights
const minHeight = (minRows * lineHeight) + paddingTop + paddingBottom;
const maxHeight = maxRows ? (maxRows * lineHeight) + paddingTop + paddingBottom : Infinity;
// Set the height based on content, clamped between min and max
const newHeight = Math.min(Math.max(textarea.scrollHeight, minHeight), maxHeight);
textarea.style.height = `${newHeight}px`;
}, [minRows, maxRows]);
useEffect(() => {
adjustHeight();
}, [value, adjustHeight]);
// Adjust on mount and window resize
useEffect(() => {
adjustHeight();
window.addEventListener('resize', adjustHeight);
return () => window.removeEventListener('resize', adjustHeight);
}, [adjustHeight]);
return (
<textarea
ref={textareaRef}
className={`textarea-autosize${className ? ` ${className}` : ''}`}
value={value}
onChange={onChange}
placeholder={placeholder}
style={style}
readOnly={readOnly}
rows={minRows}
/>
);
});
Textarea.displayName = 'Textarea';
export { Textarea as default, Textarea };

View File

@@ -0,0 +1,34 @@
@use 'scss/variables' as *;
.textarea-autosize {
width: 100%;
padding: 12px 16px;
font-size: 15px;
line-height: 24px;
outline: none;
resize: none;
overflow: hidden;
transition: 0.2s ease;
font-family: inherit;
@include themed {
background: t($modal-sidebar);
border: 1px solid t($modal-sidebarActive);
border-radius: t($borderRadius);
color: t($color);
&:hover,
&:focus {
border-color: t($color);
}
&::placeholder {
color: t($subColor);
}
}
&[readonly] {
opacity: 0.6;
cursor: not-allowed;
}
}

View File

@@ -0,0 +1 @@
export * from './Textarea';

View File

@@ -3,6 +3,8 @@ export * from './ChipSelect';
export * from './Dropdown';
export * from './FileUpload';
export * from './Radio';
export * from './SearchInput';
export * from './Slider';
export * from './Switch';
export * from './Text';
export * from './Textarea';

View File

@@ -10,7 +10,6 @@ import {
Section,
} from 'components/Layout/Settings';
import { Checkbox, Switch, Text } from 'components/Form/Settings';
import { TextareaAutosize } from '@mui/material';
import { Button } from 'components/Elements';
import { toast } from 'react-toastify';
@@ -192,15 +191,15 @@ const GreetingOptions = ({ currentSubSection, onSubSectionChange, sectionName })
<span className="subtitle">
{variables.getMessage(`${GREETING_SECTION}.event_name`)}
</span>
<TextareaAutosize
<input
type="text"
className="text-field-input"
value={event.name}
placeholder={variables.getMessage(`${GREETING_SECTION}.event_name`)}
onChange={(e) => {
const updatedEvent = { ...event, name: e.target.value };
updateEvent(index, updatedEvent);
}}
varient="outlined"
style={{ padding: '0' }}
/>
</div>
</div>

View File

@@ -2,9 +2,9 @@ import variables from 'config/variables';
import { useState } from 'react';
import { MdCancel, MdAdd, MdOutlineTextsms } from 'react-icons/md';
import { toast } from 'react-toastify';
import { TextareaAutosize } from '@mui/material';
import { Header, Row, Content, Action, PreferencesWrapper } from 'components/Layout/Settings';
import { Textarea } from 'components/Form/Settings';
import { Button } from 'components/Elements';
import EventBus from 'utils/eventbus';
@@ -82,14 +82,13 @@ const MessageOptions = () => {
<span className="subtitle">
{variables.getMessage(`${MESSAGE_SECTION}.title`)}
</span>
<TextareaAutosize
<Textarea
value={messages[index]}
placeholder={variables.getMessage(
'modals.main.settings.sections.message.content',
)}
onChange={(e) => message(e, true, index)}
varient="outlined"
style={{ padding: '0' }}
minRows={2}
/>
</div>
</div>

View File

@@ -2,7 +2,6 @@ import variables from 'config/variables';
import { useState, memo } from 'react';
import { Checkbox, Slider } from 'components/Form/Settings';
import { Button } from 'components/Elements';
import { TextField } from '@mui/material';
import { toast } from 'react-toastify';
import EventBus from 'utils/eventbus';
@@ -39,22 +38,26 @@ function ExperimentalOptions() {
element=".other"
/>
<p style={{ textAlign: 'left', width: '100%' }}>Send Event</p>
<TextField
label={'Type'}
value={eventType}
onChange={(e) => setEventType(e.target.value)}
spellCheck={false}
varient="outlined"
InputLabelProps={{ shrink: true }}
/>
<TextField
label={'Name'}
value={eventName}
onChange={(e) => setEventName(e.target.value)}
spellCheck={false}
varient="outlined"
InputLabelProps={{ shrink: true }}
/>
<div className="text-field">
<label className="text-field-label">Type</label>
<input
type="text"
className="text-field-input"
value={eventType || ''}
onChange={(e) => setEventType(e.target.value)}
spellCheck={false}
/>
</div>
<div className="text-field">
<label className="text-field-label">Name</label>
<input
type="text"
className="text-field-input"
value={eventName || ''}
onChange={(e) => setEventName(e.target.value)}
spellCheck={false}
/>
</div>
<Button
type="settings"
onClick={() => EventBus.emit(eventType, eventName)}

View File

@@ -1,10 +1,9 @@
import { useState, useMemo } from 'react';
import { useT, useTranslation } from 'contexts/TranslationContext';
import { MdOutlineOpenInNew, MdSearch, MdComputer } from 'react-icons/md';
import { TextField, InputAdornment } from '@mui/material';
import { MdOutlineOpenInNew, MdComputer } from 'react-icons/md';
import { Radio, Checkbox } from 'components/Form/Settings';
import { Radio, Checkbox, SearchInput } from 'components/Form/Settings';
import languages from '@/i18n/languages.json';
import translationPercentages from '@/i18n/translationPercentages.json';
@@ -123,35 +122,10 @@ const LanguageOptions = () => {
marginBottom: 16,
}}
>
<TextField
<SearchInput
placeholder={t('modals.main.settings.sections.language.search')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
variant="outlined"
size="small"
InputProps={{
startAdornment: (
<InputAdornment position="start">
<MdSearch style={{ color: '#888' }} />
</InputAdornment>
),
}}
sx={{
width: '250px',
'& .MuiOutlinedInput-root': {
borderRadius: '24px',
backgroundColor: 'rgba(255, 255, 255, 0.08)',
'& fieldset': {
border: 'none',
},
'&:hover fieldset': {
border: 'none',
},
'&.Mui-focused fieldset': {
border: 'none',
},
},
}}
/>
{currentLangOption && (
<div style={{ color: '#888', whiteSpace: 'nowrap' }}>

View File

@@ -4,9 +4,9 @@ import { useT } from 'contexts';
import { MdContentCopy, MdAssignment, MdPushPin, MdDownload } from 'react-icons/md';
import { useFloating, shift } from '@floating-ui/react-dom';
import TextareaAutosize from '@mui/material/TextareaAutosize';
import { toast } from 'react-toastify';
import { Tooltip } from 'components/Elements';
import { Textarea } from 'components/Form/Settings';
import { saveFile } from 'utils/saveFile';
import EventBus from 'utils/eventbus';
@@ -112,12 +112,11 @@ const Notes = ({ notesRef, floatRef, position, xPosition, yPosition }) => {
</button>
</Tooltip>
</div>
<TextareaAutosize
<Textarea
placeholder={t('widgets.navbar.notes.placeholder')}
value={notes}
onChange={handleSetNotes}
minRows={5}
maxLength={10000}
/>
</div>
</span>

View File

@@ -9,11 +9,10 @@ import {
MdPlaylistAdd,
MdOutlineDragIndicator,
MdPlaylistRemove,
MdCheck,
} from 'react-icons/md';
import TextareaAutosize from '@mui/material/TextareaAutosize';
import { Tooltip } from 'components/Elements';
import Checkbox from '@mui/material/Checkbox';
import { Textarea } from 'components/Form/Settings';
import { shift, useFloating } from '@floating-ui/react-dom';
import {
DndContext,
@@ -210,15 +209,18 @@ function Todo({ todoRef, floatRef, position, xPosition, yPosition }) {
<SortableItem key={index} id={index}>
{({ attributes, listeners }) => (
<div className={'todoRow' + (todoItem.done ? ' done' : '')}>
<Checkbox
checked={todoItem.done}
<div
className={'todo-checkbox' + (todoItem.done ? ' checked' : '')}
onClick={() => updateTodo('done', index)}
/>
<TextareaAutosize
>
{todoItem.done && <MdCheck />}
</div>
<Textarea
placeholder={t('widgets.navbar.notes.placeholder')}
value={todoItem.value}
onChange={(data) => updateTodo('set', index, data)}
readOnly={todoItem.done}
minRows={1}
/>
<Tooltip
title={t(

View File

@@ -1,7 +1,6 @@
import variables from 'config/variables';
import React, { useState } from 'react';
import { MdCancel, MdAdd, MdSource, MdOutlineFormatQuote } from 'react-icons/md';
import TextareaAutosize from '@mui/material/TextareaAutosize';
import {
Header,
@@ -11,7 +10,7 @@ import {
Section,
PreferencesWrapper,
} from 'components/Layout/Settings';
import { Checkbox, Dropdown } from 'components/Form/Settings';
import { Checkbox, Dropdown, Textarea } from 'components/Form/Settings';
import { Button } from 'components/Elements';
const QuoteOptions = ({ currentSubSection, onSubSectionChange, sectionName }) => {
@@ -166,23 +165,23 @@ const QuoteOptions = ({ currentSubSection, onSubSectionChange, sectionName }) =>
<MdOutlineFormatQuote />
</div>
<div className="messageText">
<TextareaAutosize
<Textarea
value={customQuote[index].quote}
placeholder={variables.getMessage(
'modals.main.settings.sections.quote.title',
)}
onChange={(e) => handleCustomQuote(e, true, index, 'quote')}
varient="outlined"
style={{ fontSize: '22px', fontWeight: 'bold' }}
minRows={1}
/>
<TextareaAutosize
<Textarea
value={customQuote[index].author}
placeholder={variables.getMessage(
'modals.main.settings.sections.quote.author',
)}
className="subtitle"
onChange={(e) => handleCustomQuote(e, true, index, 'author')}
varient="outlined"
minRows={1}
/>
</div>
<div>

View File

@@ -3,7 +3,6 @@ import { MdAutoAwesome } from 'react-icons/md';
import { Header, Row, Content, Action, PreferencesWrapper } from 'components/Layout/Settings';
import { useLocalStorageState } from 'utils/useLocalStorageState';
import { Radio, Dropdown, Checkbox } from 'components/Form/Settings';
import { TextField } from '@mui/material';
import variables from 'config/variables';
const useWeatherSettings = () => {
@@ -82,18 +81,24 @@ const WeatherOptions = () => {
<Row>
<Content title={variables.getMessage(`${WEATHER_SECTION}.location`)} />
<Action>
<TextField
label={variables.getMessage(`${WEATHER_SECTION}.location`)}
value={location}
onChange={changeLocation}
placeholder="London"
variant="outlined"
InputLabelProps={{ shrink: true }}
/>
<span className="link" onClick={getAutoLocation}>
<MdAutoAwesome />
{variables.getMessage(`${WEATHER_SECTION}.auto`)}
</span>
<div className="text-field-container">
<div className="text-field">
<label className="text-field-label">
{variables.getMessage(`${WEATHER_SECTION}.location`)}
</label>
<input
type="text"
className="text-field-input"
value={location}
onChange={changeLocation}
placeholder="London"
/>
</div>
<span className="link" onClick={getAutoLocation}>
<MdAutoAwesome />
{variables.getMessage(`${WEATHER_SECTION}.auto`)}
</span>
</div>
</Action>
</Row>
);

View File

@@ -1,12 +1,11 @@
import { useState, useMemo } from 'react';
import { MdOutlineOpenInNew, MdSearch } from 'react-icons/md';
import { TextField, InputAdornment } from '@mui/material';
import { MdOutlineOpenInNew } from 'react-icons/md';
import languages from '@/i18n/languages.json';
import translationPercentages from '@/i18n/translationPercentages.json';
import { useT, useTranslation } from 'contexts/TranslationContext';
import variables from 'config/variables';
import { Radio } from 'components/Form/Settings';
import { Radio, SearchInput } from 'components/Form/Settings';
import { Header, Content } from '../Layout';
function ChooseLanguage() {
@@ -107,37 +106,14 @@ function ChooseLanguage() {
{t('modals.main.settings.sections.language.use_system')} ({systemLanguage.name})
</button>
)}
<TextField
placeholder={t('modals.main.settings.sections.language.search')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
variant="outlined"
size="small"
fullWidth
InputProps={{
startAdornment: (
<InputAdornment position="start">
<MdSearch style={{ color: '#888' }} />
</InputAdornment>
),
}}
sx={{
marginBottom: 2,
'& .MuiOutlinedInput-root': {
borderRadius: '24px',
backgroundColor: 'rgba(255, 255, 255, 0.08)',
'& fieldset': {
border: 'none',
},
'&:hover fieldset': {
border: 'none',
},
'&.Mui-focused fieldset': {
border: 'none',
},
},
}}
/>
<div style={{ marginBottom: 16 }}>
<SearchInput
placeholder={t('modals.main.settings.sections.language.search')}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
fullWidth
/>
</div>
<div className="languageSettings">
<Radio name="language" options={filteredLanguages} category="welcomeLanguage" />
</div>