mirror of
https://github.com/Toinane/colorpicker.git
synced 2026-08-24 10:05:36 -05:00
enhance code
This commit is contained in:
+14
-2
@@ -1,4 +1,13 @@
|
||||
{
|
||||
// `suspicious`/`pedantic`/`style` were evaluated and left off: they're
|
||||
// dominated by false positives for this codebase's conventions —
|
||||
// react/react-in-jsx-scope (React 19's automatic JSX runtime needs no
|
||||
// import), import/no-unassigned-import (CSS side-effect imports),
|
||||
// import/no-named-export (named exports are this project's convention for
|
||||
// stores/hooks), eslint/max-lines-per-function, import/max-dependencies.
|
||||
// `perf` is enabled below since it was mostly signal; `oxc/no-map-spread`
|
||||
// is its one false positive (it'd push stores toward in-place mutation,
|
||||
// which breaks Zustand's shallow-equality change detection).
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": [
|
||||
"typescript",
|
||||
@@ -8,9 +17,12 @@
|
||||
"import"
|
||||
],
|
||||
"categories": {
|
||||
"correctness": "error"
|
||||
"correctness": "error",
|
||||
"perf": "error"
|
||||
},
|
||||
"rules": {
|
||||
"oxc/no-map-spread": "off"
|
||||
},
|
||||
"rules": {},
|
||||
"env": {
|
||||
"builtin": true
|
||||
}
|
||||
|
||||
@@ -15,6 +15,12 @@
|
||||
"terms": {
|
||||
"website": "Website"
|
||||
},
|
||||
"color": {
|
||||
"red": "Red",
|
||||
"green": "Green",
|
||||
"blue": "Blue",
|
||||
"hex": "Hex color"
|
||||
},
|
||||
"tray": {
|
||||
"open": "Open Colorpicker",
|
||||
"settings": "Settings",
|
||||
|
||||
@@ -11,15 +11,18 @@
|
||||
"title": "Colorpicker",
|
||||
"openAtLogin": {
|
||||
"label": "Open Colorpicker at login",
|
||||
"description": "Launch Colorpicker automatically when you log in"
|
||||
"description": "Launch Colorpicker automatically when you log in",
|
||||
"errorToast": "Failed to update \"Open Colorpicker at login\""
|
||||
},
|
||||
"keepOnTop": {
|
||||
"label": "Keep Colorpicker on top",
|
||||
"description": "Always display Colorpicker above other windows"
|
||||
"description": "Always display Colorpicker above other windows",
|
||||
"errorToast": "Failed to update \"Keep Colorpicker on top\""
|
||||
},
|
||||
"closeToTray": {
|
||||
"label": "Close to tray",
|
||||
"description": "Keep Colorpicker running in the system tray instead of quitting when the window is closed"
|
||||
"description": "Keep Colorpicker running in the system tray instead of quitting when the window is closed",
|
||||
"errorToast": "Failed to update \"Close to tray\""
|
||||
},
|
||||
"theme": {
|
||||
"label": "Theme",
|
||||
@@ -159,7 +162,8 @@
|
||||
"title": "Picker",
|
||||
"pickerHotkey": {
|
||||
"label": "Launch Picker Hotkey",
|
||||
"description": "Global keyboard shortcut to launch the color picker from anywhere, even when Colorpicker is closed"
|
||||
"description": "Global keyboard shortcut to launch the color picker from anywhere, even when Colorpicker is closed",
|
||||
"errorToast": "Failed to register the picker hotkey"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -15,6 +15,12 @@
|
||||
"terms": {
|
||||
"website": "Site web"
|
||||
},
|
||||
"color": {
|
||||
"red": "Rouge",
|
||||
"green": "Vert",
|
||||
"blue": "Bleu",
|
||||
"hex": "Couleur hexadécimale"
|
||||
},
|
||||
"tray": {
|
||||
"open": "Ouvrir Colorpicker",
|
||||
"settings": "Paramètres",
|
||||
|
||||
@@ -11,15 +11,18 @@
|
||||
"title": "Colorpicker",
|
||||
"openAtLogin": {
|
||||
"label": "Ouvrir Colorpicker à la connexion",
|
||||
"description": "Ouvre Colorpicker automatiquement au démarrage de votre session"
|
||||
"description": "Ouvre Colorpicker automatiquement au démarrage de votre session",
|
||||
"errorToast": "Échec de la mise à jour de « Ouvrir Colorpicker à la connexion »"
|
||||
},
|
||||
"keepOnTop": {
|
||||
"label": "Garder Colorpicker au premier plan",
|
||||
"description": "Affiche toujours Colorpicker au-dessus des autres fenêtres"
|
||||
"description": "Affiche toujours Colorpicker au-dessus des autres fenêtres",
|
||||
"errorToast": "Échec de la mise à jour de « Garder Colorpicker au premier plan »"
|
||||
},
|
||||
"closeToTray": {
|
||||
"label": "Fermer dans la barre système",
|
||||
"description": "Garde Colorpicker actif dans la barre système au lieu de quitter à la fermeture de la fenêtre"
|
||||
"description": "Garde Colorpicker actif dans la barre système au lieu de quitter à la fermeture de la fenêtre",
|
||||
"errorToast": "Échec de la mise à jour de « Fermer dans la barre système »"
|
||||
},
|
||||
"theme": {
|
||||
"label": "Thème",
|
||||
@@ -159,7 +162,8 @@
|
||||
"title": "Pipette",
|
||||
"pickerHotkey": {
|
||||
"label": "Raccourci de lancement",
|
||||
"description": "Raccourci clavier global pour lancer le sélecteur de couleur depuis n'importe où, même lorsque Colorpicker est fermé"
|
||||
"description": "Raccourci clavier global pour lancer le sélecteur de couleur depuis n'importe où, même lorsque Colorpicker est fermé",
|
||||
"errorToast": "Échec de l'enregistrement du raccourci de la pipette"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
+2
-25
@@ -85,18 +85,7 @@ getWindowLabel().catch(() => {
|
||||
// Ignore errors, will use default
|
||||
})
|
||||
|
||||
/**
|
||||
* Logger interface - the single source of truth for all application logging.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* import { logger } from '@/common/logger'
|
||||
*
|
||||
* logger.debug('Starting color pick', { gridSize: 11 })
|
||||
* logger.info('Color picked successfully', { color: '#8E44AD' })
|
||||
* logger.error('Pick failed', { error: err.message })
|
||||
* ```
|
||||
*/
|
||||
/** The single source of truth for all application logging. */
|
||||
export const logger = {
|
||||
/**
|
||||
* Trace - Most granular logging, typically for tracking program flow.
|
||||
@@ -139,19 +128,7 @@ export const logger = {
|
||||
},
|
||||
} as const
|
||||
|
||||
/**
|
||||
* Create a scoped logger with automatic scope tagging.
|
||||
* The scope name appears as a tag in the log output.
|
||||
*
|
||||
* @param scopeName - Name of the scope (e.g., "SettingsStore", "ColorPicker")
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const log = createScopedLogger('SettingsStore')
|
||||
* log.info('Settings saved')
|
||||
* // Output: [23:22:25] DEBUG [window:main][SettingsStore] Settings saved
|
||||
* ```
|
||||
*/
|
||||
/** Logger whose messages are tagged with `scopeName`, e.g. `[SettingsStore]`. */
|
||||
export function createScopedLogger(scopeName: string) {
|
||||
return {
|
||||
trace: (message: string, context?: LogContext) => {
|
||||
|
||||
@@ -26,9 +26,7 @@ export const persistToStore = async (
|
||||
entries: Record<string, unknown>,
|
||||
): Promise<void> => {
|
||||
if (!store) return
|
||||
for (const [key, value] of Object.entries(entries)) {
|
||||
await store.set(key, value)
|
||||
}
|
||||
await Promise.all(Object.entries(entries).map(([key, value]) => store.set(key, value)))
|
||||
await store.save()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { FunctionComponent, JSX, useCallback, useState, useEffect } from 'react'
|
||||
import { FunctionComponent, JSX, useCallback } from 'react'
|
||||
import Color from 'colorjs.io'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
// import { writeText } from '@tauri-apps/plugin-clipboard-manager'
|
||||
// import { useTranslation } from 'react-i18next'
|
||||
|
||||
// import Icon, { IconEnum } from '@components/icons'
|
||||
import { useColorStore } from '@stores/colorStore'
|
||||
@@ -9,18 +9,15 @@ import { useColorStore } from '@stores/colorStore'
|
||||
import { useColorHistoryStore } from '@stores/colorHistoryStore'
|
||||
// import { showToast } from '@stores/toastStore'
|
||||
import { isValidHex /*, serializeColor*/, toHex } from '@common/color'
|
||||
import { useControlledValue } from '@hooks/index'
|
||||
|
||||
import './hexInput.css'
|
||||
import style from './hexInput.module.css'
|
||||
|
||||
const HexInput: FunctionComponent = (): JSX.Element => {
|
||||
// const CommonT = useTranslation('common')
|
||||
const CommonT = useTranslation('common')
|
||||
const { color, setColor /*, isDarkColor */ } = useColorStore((state) => state)
|
||||
// const { defaultFormat, hexPrefix } = useSettingsStore()
|
||||
const [inputValue, setInputValue] = useState(toHex(color))
|
||||
|
||||
useEffect(() => {
|
||||
setInputValue(toHex(color))
|
||||
}, [color])
|
||||
const [inputValue, setInputValue] = useControlledValue(toHex(color))
|
||||
|
||||
const onInput = useCallback(
|
||||
(event: React.FormEvent<HTMLInputElement>) => {
|
||||
@@ -34,7 +31,7 @@ const HexInput: FunctionComponent = (): JSX.Element => {
|
||||
useColorHistoryStore.getState().commitColor(toHex(newColor))
|
||||
}
|
||||
},
|
||||
[setColor],
|
||||
[setColor, setInputValue],
|
||||
)
|
||||
|
||||
const onKeyboard = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
@@ -72,12 +69,13 @@ const HexInput: FunctionComponent = (): JSX.Element => {
|
||||
// }
|
||||
|
||||
return (
|
||||
<div className="hexInputGroup">
|
||||
<div className={style.hexInputGroup}>
|
||||
<input
|
||||
className="hexInput"
|
||||
className={style.hexInput}
|
||||
type="text"
|
||||
maxLength={7}
|
||||
value={inputValue}
|
||||
aria-label={CommonT.t('color.hex')}
|
||||
onInput={onInput}
|
||||
onKeyDown={onKeyboard}
|
||||
onFocus={(e) => e.target.select()}
|
||||
@@ -85,7 +83,7 @@ const HexInput: FunctionComponent = (): JSX.Element => {
|
||||
/>
|
||||
{/* <button
|
||||
type="button"
|
||||
className="hexInputCopyButton"
|
||||
className={style.hexInputCopyButton}
|
||||
onClick={onCopy}
|
||||
title={CommonT.t("action.copy")}
|
||||
aria-label={CommonT.t("action.copy")}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { FunctionComponent, JSX, useState, useEffect } from 'react'
|
||||
import { FunctionComponent, JSX } from 'react'
|
||||
|
||||
import './numberInput.css'
|
||||
import { useControlledValue } from '@hooks/index'
|
||||
|
||||
import style from './numberInput.module.css'
|
||||
|
||||
type NumberInputProps = {
|
||||
min: number
|
||||
@@ -9,6 +11,8 @@ type NumberInputProps = {
|
||||
step?: number
|
||||
value: number
|
||||
onChange?: (value: number) => void
|
||||
/** Accessible name (e.g. "Red") — this input has no visible text label. */
|
||||
label: string
|
||||
}
|
||||
|
||||
const NumberInput: FunctionComponent<NumberInputProps> = ({
|
||||
@@ -18,12 +22,9 @@ const NumberInput: FunctionComponent<NumberInputProps> = ({
|
||||
step = 1,
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
}): JSX.Element => {
|
||||
const [number, setNumber] = useState(Number.isNaN(value) ? 0 : value)
|
||||
|
||||
useEffect(() => {
|
||||
setNumber(Number.isNaN(value) ? 0 : value)
|
||||
}, [value])
|
||||
const [number, setNumber] = useControlledValue(Number.isNaN(value) ? 0 : value)
|
||||
|
||||
const verifyNumber = (currentNumber: number): number => {
|
||||
if (currentNumber < min) return min
|
||||
@@ -60,13 +61,14 @@ const NumberInput: FunctionComponent<NumberInputProps> = ({
|
||||
|
||||
return (
|
||||
<input
|
||||
className="numberInput"
|
||||
className={style.numberInput}
|
||||
type="input"
|
||||
min={min}
|
||||
max={max}
|
||||
maxLength={maxLength}
|
||||
step={step}
|
||||
value={number}
|
||||
aria-label={label}
|
||||
onFocus={(e) => e.target.select()}
|
||||
onInput={onInput}
|
||||
onKeyDown={onKeyboard}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FunctionComponent, JSX, useMemo, useCallback } from 'react'
|
||||
import Color from 'colorjs.io'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { useColorStore } from '@stores/colorStore'
|
||||
import { useColorHistoryStore } from '@stores/colorHistoryStore'
|
||||
@@ -8,9 +9,10 @@ import { toHex } from '@common/color'
|
||||
import Slider from '@components/colorpicker/sliders/slider'
|
||||
import NumberInput from '@components/colorpicker/inputs/numberInput/numberInput'
|
||||
|
||||
import './RGBSlider.css'
|
||||
import style from './RGBSlider.module.css'
|
||||
|
||||
const RGBSlider: FunctionComponent = (): JSX.Element => {
|
||||
const CommonT = useTranslation('common')
|
||||
const color = useColorStore((state) => state.color)
|
||||
// NOTE: use getAll('srgb') rather than the color.srgb accessor - colorjs.io's
|
||||
// package.json sideEffects list omits src/space-accessors.js, so Vite's esbuild
|
||||
@@ -34,8 +36,8 @@ const RGBSlider: FunctionComponent = (): JSX.Element => {
|
||||
)
|
||||
|
||||
return (
|
||||
<section className={'RGBSlider'}>
|
||||
<section className="slider">
|
||||
<section className={style.RGBSlider}>
|
||||
<section className={style.slider}>
|
||||
<Slider
|
||||
type="redGradient"
|
||||
min={0}
|
||||
@@ -48,9 +50,10 @@ const RGBSlider: FunctionComponent = (): JSX.Element => {
|
||||
max={255}
|
||||
value={Math.round((r ?? 0) * 255)}
|
||||
onChange={(value) => handleChange('r', value)}
|
||||
label={CommonT.t('color.red')}
|
||||
/>
|
||||
</section>
|
||||
<section className="slider">
|
||||
<section className={style.slider}>
|
||||
<Slider
|
||||
type="greenGradient"
|
||||
min={0}
|
||||
@@ -63,9 +66,10 @@ const RGBSlider: FunctionComponent = (): JSX.Element => {
|
||||
max={255}
|
||||
value={Math.round((g ?? 0) * 255)}
|
||||
onChange={(value) => handleChange('g', value)}
|
||||
label={CommonT.t('color.green')}
|
||||
/>
|
||||
</section>
|
||||
<section className="slider">
|
||||
<section className={style.slider}>
|
||||
<Slider
|
||||
type="blueGradient"
|
||||
min={0}
|
||||
@@ -78,6 +82,7 @@ const RGBSlider: FunctionComponent = (): JSX.Element => {
|
||||
max={255}
|
||||
value={Math.round((b ?? 0) * 255)}
|
||||
onChange={(value) => handleChange('b', value)}
|
||||
label={CommonT.t('color.blue')}
|
||||
/>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { FunctionComponent, JSX, useState, useEffect } from 'react'
|
||||
import { FunctionComponent, JSX } from 'react'
|
||||
import classNames from 'clsx'
|
||||
|
||||
import { useColorStore } from '@stores/colorStore'
|
||||
import { useControlledValue } from '@hooks/index'
|
||||
|
||||
import style from './slider.module.css'
|
||||
|
||||
@@ -19,14 +21,9 @@ const Slider: FunctionComponent<SliderProps> = ({
|
||||
value,
|
||||
onChange,
|
||||
}): JSX.Element => {
|
||||
const [color, setColor] = useState(Number.isNaN(value) ? 0 : value)
|
||||
const [color, setColor] = useControlledValue(Number.isNaN(value) ? 0 : value)
|
||||
const isDarkColor = useColorStore((state) => state.isDarkColor)
|
||||
|
||||
// Sync local state when prop changes
|
||||
useEffect(() => {
|
||||
setColor(Number.isNaN(value) ? 0 : value)
|
||||
}, [value])
|
||||
|
||||
const changeValue = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
const newColor = event.target instanceof HTMLInputElement ? Number(event.target.value) : 0
|
||||
setColor(newColor)
|
||||
@@ -44,7 +41,7 @@ const Slider: FunctionComponent<SliderProps> = ({
|
||||
onInput={changeValue}
|
||||
/>
|
||||
<progress
|
||||
className={`${style.progress} ${style[type]} ${isDarkColor ? style.dark : style.light}`}
|
||||
className={classNames(style.progress, style[type], isDarkColor ? style.dark : style.light)}
|
||||
max={max}
|
||||
value={color}
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import classNames from 'clsx'
|
||||
|
||||
import { Text } from '@components/ui'
|
||||
|
||||
import style from './navigationItem.module.css'
|
||||
@@ -12,7 +14,7 @@ const NavigationItem = ({
|
||||
isActive?: boolean
|
||||
}) => {
|
||||
return (
|
||||
<button className={`${style.navigationItem} ${isActive ? style.active : ''}`}>
|
||||
<button className={classNames(style.navigationItem, isActive && style.active)}>
|
||||
<div className={style.colorSwatch} style={{ backgroundColor: color }}></div>
|
||||
<Text color="primary" className={style.label}>
|
||||
{label}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import classNames from 'clsx'
|
||||
|
||||
import { Text, Button } from '@components/ui'
|
||||
import Icon, { IconEnum } from '@components/icons'
|
||||
@@ -47,7 +48,7 @@ const NavigationSection = ({
|
||||
}}
|
||||
>
|
||||
<div className={style.navigationSectionToggle}>
|
||||
<span className={`${style.chevron} ${isOpen ? style.chevronOpen : ''}`}>
|
||||
<span className={classNames(style.chevron, isOpen && style.chevronOpen)}>
|
||||
<Icon type={IconEnum.ARROW} />
|
||||
</span>
|
||||
<Text
|
||||
@@ -68,7 +69,10 @@ const NavigationSection = ({
|
||||
>
|
||||
<Button
|
||||
variant="transparent"
|
||||
className={`${style.navigationSectionMenuButton} ${isMenuOpen ? style.navigationSectionMenuButtonActive : ''}`}
|
||||
className={classNames(
|
||||
style.navigationSectionMenuButton,
|
||||
isMenuOpen && style.navigationSectionMenuButtonActive,
|
||||
)}
|
||||
onClick={() => setIsMenuOpen((open) => !open)}
|
||||
aria-label="Section options"
|
||||
>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import classNames from 'clsx'
|
||||
|
||||
import { Heading, Text } from '@components/ui'
|
||||
|
||||
import style from './settingsItem.module.css'
|
||||
@@ -11,7 +13,7 @@ export interface SettingsItemProps {
|
||||
|
||||
const SettingsItem = ({ label, description, disabled = false, children }: SettingsItemProps) => {
|
||||
return (
|
||||
<section className={`${style.settingsItem} ${disabled ? style.settingsItemDisabled : ''}`}>
|
||||
<section className={classNames(style.settingsItem, disabled && style.settingsItemDisabled)}>
|
||||
<div className={style.settingsItemInfo}>
|
||||
<Heading level={3} className={style.settingsItemLabel}>
|
||||
{label}
|
||||
|
||||
@@ -13,17 +13,17 @@ export interface SettingsItemListProps {
|
||||
const SettingsItemList = ({ items }: SettingsItemListProps) => {
|
||||
return (
|
||||
<section className={style.settingsItemList}>
|
||||
{items.map((item, index) => {
|
||||
{items.map((item) => {
|
||||
if (typeof item === 'string') {
|
||||
return (
|
||||
<React.Fragment key={`string-${index}`}>
|
||||
<React.Fragment key={item}>
|
||||
<Text className={style.settingsItemListValue}>{item}</Text>
|
||||
<span className={style.settingsItemListEmpty}></span>
|
||||
</React.Fragment>
|
||||
)
|
||||
} else {
|
||||
return Object.entries(item).map(([key, value]) => (
|
||||
<React.Fragment key={`${index}-${key}`}>
|
||||
<React.Fragment key={key}>
|
||||
<Text className={style.settingsItemListLabel}>{key}</Text>
|
||||
<Text className={style.settingsItemListValue}>{value}</Text>
|
||||
</React.Fragment>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import classNames from 'clsx'
|
||||
|
||||
import style from './Heading.module.css'
|
||||
|
||||
export type HeadingLevel = 1 | 2 | 3
|
||||
@@ -31,7 +33,13 @@ const Heading = ({
|
||||
return (
|
||||
<Tag
|
||||
id={id}
|
||||
className={`${style.heading} ${color && style[color]} ${style[size]} ${style[weight]} ${className ?? ''}`}
|
||||
className={classNames(
|
||||
style.heading,
|
||||
color && style[color],
|
||||
style[size],
|
||||
style[weight],
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</Tag>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import classNames from 'clsx'
|
||||
|
||||
import { Text, KeyCombo } from '@components/ui'
|
||||
import Icon, { IconEnum } from '@components/icons'
|
||||
@@ -135,7 +136,7 @@ const HotkeyInput = ({
|
||||
<div className={style.hotkeyInputWrapper}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${style.hotkeyInput} ${isRecording ? style.hotkeyInputRecording : ''}`}
|
||||
className={classNames(style.hotkeyInput, isRecording && style.hotkeyInputRecording)}
|
||||
onClick={() => !disabled && !isRecording && setIsRecording(true)}
|
||||
onBlur={stopRecording}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -27,7 +27,7 @@ const KeyCombo = ({ keys, type = 'joined', variant = 'filled' }: KeyComboProps)
|
||||
return (
|
||||
<span className={style.keyComboGroup}>
|
||||
{keys.map((part, i) => (
|
||||
<Fragment key={`${part}-${i}`}>
|
||||
<Fragment key={part}>
|
||||
{type === 'separated' && i > 0 && (
|
||||
<Text color="accent" size="small">
|
||||
+
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import classNames from 'clsx'
|
||||
import style from './Select.module.css'
|
||||
import Icon, { IconEnum } from '@components/icons'
|
||||
import { Text } from '@components/ui'
|
||||
@@ -79,7 +80,7 @@ const Select = ({ value, onChange, options, disabled = false }: SelectProps) =>
|
||||
return (
|
||||
<div className={style.selectWrapper} ref={selectRef}>
|
||||
<div
|
||||
className={`${style.select} ${disabled ? style.selectDisabled : ''} ${isOpen ? style.selectOpen : ''}`}
|
||||
className={classNames(style.select, disabled && style.selectDisabled, isOpen && style.selectOpen)}
|
||||
onClick={handleToggle}
|
||||
onKeyDown={handleKeyDown}
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
@@ -97,7 +98,7 @@ const Select = ({ value, onChange, options, disabled = false }: SelectProps) =>
|
||||
{options.map((option) => (
|
||||
<div
|
||||
key={option.value}
|
||||
className={`${style.selectOption} ${option.value === value ? style.selectOptionSelected : ''}`}
|
||||
className={classNames(style.selectOption, option.value === value && style.selectOptionSelected)}
|
||||
onClick={() => handleOptionClick(option.value)}
|
||||
onKeyDown={(e) => handleOptionKeyDown(e, option.value)}
|
||||
tabIndex={0}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import classNames from 'clsx'
|
||||
|
||||
import style from './Text.module.css'
|
||||
|
||||
export type TextSize = 'small' | 'medium' | 'large'
|
||||
@@ -15,7 +17,7 @@ export interface TextProps {
|
||||
const Text = ({ size = 'medium', weight = 'normal', children, className, color }: TextProps) => {
|
||||
return (
|
||||
<p
|
||||
className={`${style.text} ${color && style[color]} ${style[size]} ${style[weight]} ${className ?? ''}`}
|
||||
className={classNames(style.text, color && style[color], style[size], style[weight], className)}
|
||||
>
|
||||
{children}
|
||||
</p>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import classNames from 'clsx'
|
||||
import { Text } from '@components/ui'
|
||||
import style from './Toggle.module.css'
|
||||
|
||||
@@ -144,7 +145,7 @@ const Toggle = ({ checked, onChange, disabled = false }: ToggleProps) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${style.toggleWrapper} ${disabled ? style.toggleDisabled : ''}`}
|
||||
className={classNames(style.toggleWrapper, disabled && style.toggleDisabled)}
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleClick}
|
||||
@@ -153,7 +154,7 @@ const Toggle = ({ checked, onChange, disabled = false }: ToggleProps) => {
|
||||
{checked ? 'On' : 'Off'}
|
||||
</Text>
|
||||
<button
|
||||
className={`${style.toggle} ${checked ? style.toggleChecked : ''} ${disabled ? style.toggleDisabled : ''}`}
|
||||
className={classNames(style.toggle, checked && style.toggleChecked, disabled && style.toggleDisabled)}
|
||||
disabled={disabled}
|
||||
role="switch"
|
||||
tabIndex={-1}
|
||||
@@ -164,7 +165,7 @@ const Toggle = ({ checked, onChange, disabled = false }: ToggleProps) => {
|
||||
onPointerCancel={handlePointerCancel}
|
||||
>
|
||||
<span
|
||||
className={`${style.toggleSlider} ${instant ? style.toggleSliderDragging : ''}`}
|
||||
className={classNames(style.toggleSlider, instant && style.toggleSliderDragging)}
|
||||
style={pinnedLeft !== null ? { left: `${pinnedLeft}px` } : undefined}
|
||||
></span>
|
||||
</button>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { FunctionComponent, JSX, useEffect, useState, useRef } from 'react'
|
||||
import classNames from 'clsx'
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||
|
||||
import style from './windowControls.module.css'
|
||||
@@ -49,6 +50,8 @@ const WindowControls: FunctionComponent = (): JSX.Element => {
|
||||
|
||||
return (
|
||||
<section className={style.windowControls}>
|
||||
{/* tabIndex={-1} on all three: window-chrome actions (minimize/maximize/close)
|
||||
are conventionally excluded from the app's own tab order, matching native title bars. */}
|
||||
<button
|
||||
className={style.controlButton}
|
||||
onClick={handleMinimize}
|
||||
@@ -101,7 +104,7 @@ const WindowControls: FunctionComponent = (): JSX.Element => {
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className={`${style.controlButton} ${style.closeButton}`}
|
||||
className={classNames(style.controlButton, style.closeButton)}
|
||||
onClick={handleClose}
|
||||
aria-label="Close"
|
||||
title="Close"
|
||||
|
||||
@@ -54,6 +54,11 @@
|
||||
color: var(--win-icon-active);
|
||||
}
|
||||
|
||||
.controlButton:focus-visible {
|
||||
outline: 2px solid var(--focus-outline);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.closeButton:hover {
|
||||
background-color: var(--close-bg-hover);
|
||||
color: var(--close-icon-hover);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* React custom hooks
|
||||
*/
|
||||
export { useTheme } from './useTheme'
|
||||
export { useControlledValue } from './useControlledValue'
|
||||
export { useLogger } from './useLogger'
|
||||
export { useHotkeyConflict } from './useHotkeyConflict'
|
||||
export { useInitializeHistory } from './useHistory'
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { useEffect, useState, type Dispatch, type SetStateAction } from 'react'
|
||||
|
||||
/**
|
||||
* Local state that mirrors an external `value`, so an input can show live
|
||||
* keystrokes before the parent's re-render round-trips back down, while
|
||||
* staying in sync if `value` changes for a reason other than the input
|
||||
* itself (e.g. a reset, or an edit made from another window).
|
||||
*/
|
||||
export function useControlledValue<T>(value: T): [T, Dispatch<SetStateAction<T>>] {
|
||||
const [local, setLocal] = useState(value)
|
||||
|
||||
useEffect(() => {
|
||||
setLocal(value)
|
||||
}, [value])
|
||||
|
||||
return [local, setLocal]
|
||||
}
|
||||
+1
-27
@@ -1,33 +1,7 @@
|
||||
import { useMemo } from 'react'
|
||||
import { createScopedLogger } from '@common/logger'
|
||||
|
||||
/**
|
||||
* React hook that provides a logger with automatic component scope tagging.
|
||||
*
|
||||
* This hook creates a scoped logger that automatically tags all log messages
|
||||
* with the component name, making it easier to trace logs back to their source.
|
||||
*
|
||||
* @param componentName - Name of the component or module using the logger
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* function ColorPicker() {
|
||||
* const logger = useLogger('ColorPicker')
|
||||
*
|
||||
* const handlePick = async () => {
|
||||
* logger.debug('Starting color pick', { gridSize: 11 })
|
||||
* // Output: [23:22:25] DEBUG [window:main][ColorPicker] Starting color pick gridSize=11
|
||||
*
|
||||
* try {
|
||||
* const color = await pickColor()
|
||||
* logger.info('Color picked', { color })
|
||||
* } catch (err) {
|
||||
* logger.error('Pick failed', { error: err })
|
||||
* }
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
/** Scoped logger tagged with `componentName`, memoized across re-renders. */
|
||||
export function useLogger(componentName: string) {
|
||||
return useMemo(() => createScopedLogger(componentName), [componentName])
|
||||
}
|
||||
|
||||
Vendored
+1
-15
@@ -1,18 +1,4 @@
|
||||
export type ColorpickerTool = 'picker' | 'swatch' | 'tint' | 'contrast'
|
||||
|
||||
export interface IWindowSchema {
|
||||
width: number
|
||||
height: number
|
||||
x?: number
|
||||
y?: number
|
||||
}
|
||||
|
||||
export interface ISettingsSchema extends IWindowSchema {
|
||||
currentColor: string
|
||||
history: Array<string>
|
||||
sendCrashReport: boolean
|
||||
tools: Array<ColorpickerTool>
|
||||
}
|
||||
// Convention: `interface` for object/store shapes, `type` for unions and primitives.
|
||||
|
||||
export type ThemeOption = 'light' | 'dark' | 'system'
|
||||
export type LanguageOption = 'en_US' | 'fr_FR'
|
||||
|
||||
@@ -20,7 +20,7 @@ import { showToast } from '@stores/toastStore'
|
||||
import { rgbToColor, serializeColor, toHex } from '@common/color'
|
||||
import { onColorPicked, onPaletteColorApplied } from '@common/ipc'
|
||||
|
||||
import './colorpicker.css'
|
||||
import style from './colorpicker.module.css'
|
||||
|
||||
const notifyColorPicked = async (title: string, text: string): Promise<void> => {
|
||||
let granted = await isPermissionGranted()
|
||||
@@ -97,9 +97,9 @@ const Colorpicker = () => {
|
||||
}, [setColor])
|
||||
|
||||
return (
|
||||
<section className="colorpicker">
|
||||
<section className={style.colorpicker}>
|
||||
<WindowBar />
|
||||
<section className="sliders">
|
||||
<section className={style.sliders}>
|
||||
<RGBSlider />
|
||||
<HexInput />
|
||||
</section>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { SettingsSection, SettingsItem } from '@components/settings'
|
||||
import { Toggle, Select } from '@components/ui'
|
||||
import { useOpenAtLogin, useKeepOnTop, useCloseToTray, useThemeSetting } from '@hooks/index'
|
||||
import { showToast } from '@stores/toastStore'
|
||||
import type { ThemeOption } from '@interfaces/settings'
|
||||
|
||||
const ColorpickerSettings = () => {
|
||||
@@ -26,10 +27,10 @@ const ColorpickerSettings = () => {
|
||||
await setOpenAtLogin(checked)
|
||||
} catch (error) {
|
||||
console.error('Failed to update open at login:', error)
|
||||
// TODO: Show error notification to user
|
||||
showToast(SettingsT.t('openAtLogin.errorToast'))
|
||||
}
|
||||
},
|
||||
[setOpenAtLogin],
|
||||
[setOpenAtLogin, SettingsT],
|
||||
)
|
||||
|
||||
const handleKeepOnTopChange = useCallback(
|
||||
@@ -38,10 +39,10 @@ const ColorpickerSettings = () => {
|
||||
await setKeepOnTop(checked)
|
||||
} catch (error) {
|
||||
console.error('Failed to update keep on top:', error)
|
||||
// TODO: Show error notification to user
|
||||
showToast(SettingsT.t('keepOnTop.errorToast'))
|
||||
}
|
||||
},
|
||||
[setKeepOnTop],
|
||||
[setKeepOnTop, SettingsT],
|
||||
)
|
||||
|
||||
const handleCloseToTrayChange = useCallback(
|
||||
@@ -50,10 +51,10 @@ const ColorpickerSettings = () => {
|
||||
await setCloseToTray(checked)
|
||||
} catch (error) {
|
||||
console.error('Failed to update close to tray:', error)
|
||||
// TODO: Show error notification to user
|
||||
showToast(SettingsT.t('closeToTray.errorToast'))
|
||||
}
|
||||
},
|
||||
[setCloseToTray],
|
||||
[setCloseToTray, SettingsT],
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useSettingsStore, DEFAULT_SETTINGS } from '@stores/settingsStore'
|
||||
import { useHotkeyConflict } from '@hooks/index'
|
||||
import { setPickerHotkey } from '@common/ipc'
|
||||
import { HotkeyInput } from '@components/ui'
|
||||
import { showToast } from '@stores/toastStore'
|
||||
|
||||
const PickerShortcutsSettings = () => {
|
||||
const SettingsT = useTranslation('settings', { keyPrefix: 'shortcuts.picker' })
|
||||
@@ -21,10 +22,10 @@ const PickerShortcutsSettings = () => {
|
||||
await updateSetting('pickerHotkey', hotkey)
|
||||
} catch (error) {
|
||||
console.error('Failed to register picker hotkey:', error)
|
||||
// TODO: Show error notification to user
|
||||
showToast(SettingsT.t('pickerHotkey.errorToast'))
|
||||
}
|
||||
},
|
||||
[updateSetting],
|
||||
[updateSetting, SettingsT],
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user