Merge PR #474: миграция тулинга с ESLint + Prettier на Biome

Замена ESLint + Prettier (9 dev-зависимостей) на Biome (одна): линт ~10×,
формат-чек ~49× быстрее. Паритет форматирования (single quotes/semi/width
100/trailing/arrowParens/jsxDouble) и Telegram WebView-гвардов (перенесены
GritQL-плагином). Имена npm-скриптов lint/format:check сохранены — CI не тронут.

Ревью-фиксы поверх PR (восстановление точного паритета):
- grit-исключение сужено до якорного src/platform/ (было bare-substring
  "platform", щадившее любой файл с этим словом в имени)
- вернул *.min.js/*.min.css в biome ignore (были в .prettierignore)
- вернул *.css в lint-staged pre-commit
- поправил устаревшую метку CI-шага
This commit is contained in:
Fringg
2026-07-12 01:36:15 +03:00
19 changed files with 373 additions and 1690 deletions

View File

@@ -19,7 +19,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Run ESLint - name: Run linter
run: npm run lint run: npm run lint
- name: Check formatting - name: Check formatting

View File

@@ -1,6 +0,0 @@
dist
node_modules
public
*.min.js
*.min.css
package-lock.json

View File

@@ -1,13 +0,0 @@
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"useTabs": false,
"trailingComma": "all",
"printWidth": 100,
"bracketSpacing": true,
"arrowParens": "always",
"endOfLine": "auto",
"jsxSingleQuote": false,
"plugins": ["prettier-plugin-tailwindcss"]
}

View File

@@ -0,0 +1,35 @@
language js
// Cross-platform guard (was eslint no-restricted-properties): these browser
// APIs misbehave inside the Telegram WebView. Route them through the platform
// abstraction instead. The platform adapters and the clipboard util (the
// canonical implementations) are exempted by the filename conditions below.
file(name=$name, body=$body) where {
// Exempt the canonical implementations only, matching the old eslint override
// scope (`src/platform/**`, `src/utils/clipboard.ts`) — an anchored path
// fragment, not a bare "platform" substring that would also spare any
// unrelated file with "platform" in its name.
$name <: not includes "src/platform/",
$name <: not includes or {
"utils/clipboard.ts",
"utils/clipboard.js"
},
$body <: contains bubble or {
`window.confirm` as $use where {
register_diagnostic(span=$use, message="Use useNativeDialog().confirm — window.confirm is silently ignored in the Telegram WebView.", severity="error")
},
`window.alert` as $use where {
register_diagnostic(span=$use, message="Use useNativeDialog().alert — window.alert is silently ignored in the Telegram WebView.", severity="error")
},
`window.open` as $use where {
register_diagnostic(span=$use, message="Use usePlatform().openLink / openTelegramLink — window.open is intercepted by the Telegram WebView.", severity="error")
},
`window.prompt` as $use where {
register_diagnostic(span=$use, message="Use usePrompt() (PromptDialogHost) — window.prompt is not supported in the Telegram WebView.", severity="error")
},
`navigator.clipboard` as $use where {
register_diagnostic(span=$use, message="Use copyToClipboard from @/utils/clipboard — it falls back to execCommand when the Clipboard API is unavailable (Telegram WebView).", severity="error")
}
}
}

94
biome.json Normal file
View File

@@ -0,0 +1,94 @@
{
"$schema": "https://biomejs.dev/schemas/2.5.3/schema.json",
"files": {
"includes": [
"**",
"!dist",
"!node_modules",
"!public",
"!package-lock.json",
"!**/*.min.js",
"!**/*.min.css"
]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100,
"lineEnding": "auto"
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"jsxQuoteStyle": "double",
"semicolons": "always",
"trailingCommas": "all",
"arrowParentheses": "always",
"bracketSpacing": true
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"a11y": {
"recommended": false
},
"correctness": {
"noUnusedVariables": "warn",
"useExhaustiveDependencies": "warn",
"noInnerDeclarations": "warn",
"noUnsafeOptionalChaining": "warn"
},
"suspicious": {
"noExplicitAny": "warn",
"noArrayIndexKey": "off",
"noImplicitAnyLet": "warn",
"useIterableCallbackReturn": "warn"
},
"security": {
"noGlobalEval": "error",
"noDangerouslySetInnerHtml": "warn"
}
}
},
"assist": {
"enabled": false
},
"plugins": ["./biome-plugins/telegram-webview-guards.grit"],
"css": {
"parser": {
"tailwindDirectives": true
}
},
"overrides": [
{
"includes": ["**/*.css"],
"linter": {
"rules": {
"suspicious": {
"noDuplicateProperties": "off",
"noUnknownAtRules": {
"options": {
"ignore": ["tailwind"]
},
"level": "error"
}
},
"correctness": {
"noUnknownFunction": {
"options": {
"ignore": ["theme", "screen"]
},
"level": "error"
}
},
"complexity": {
"noImportantStyles": "off"
}
}
}
}
]
}

View File

@@ -1,88 +0,0 @@
import js from '@eslint/js';
import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint';
import eslintConfigPrettier from 'eslint-config-prettier';
export default tseslint.config(
{
ignores: ['dist', 'node_modules', 'public'],
},
{
files: ['**/*.{ts,tsx}'],
extends: [js.configs.recommended, ...tseslint.configs.recommended],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-hooks/set-state-in-effect': 'off',
'react-hooks/static-components': 'off',
'react-hooks/immutability': 'off',
'react-hooks/refs': 'off',
'react-hooks/purity': 'off',
'react-hooks/variables': 'off',
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
'@typescript-eslint/no-explicit-any': 'warn',
'@typescript-eslint/no-unused-vars': [
'warn',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_' },
],
'no-empty': ['warn', { allowEmptyCatch: true }],
'no-eval': 'error',
'no-implied-eval': 'error',
'no-new-func': 'error',
'no-script-url': 'error',
// Cross-platform guard: these browser APIs misbehave inside the Telegram WebView.
// Route them through the platform abstraction instead. The platform adapters and
// the clipboard util (the canonical implementations) are exempted below.
'no-restricted-properties': [
'error',
{
object: 'window',
property: 'confirm',
message:
'Use useNativeDialog().confirm — window.confirm is silently ignored in the Telegram WebView.',
},
{
object: 'window',
property: 'alert',
message:
'Use useNativeDialog().alert — window.alert is silently ignored in the Telegram WebView.',
},
{
object: 'window',
property: 'open',
message:
'Use usePlatform().openLink / openTelegramLink — window.open is intercepted by the Telegram WebView.',
},
{
object: 'window',
property: 'prompt',
message:
'Use usePrompt() (PromptDialogHost) — window.prompt is not supported in the Telegram WebView.',
},
{
object: 'navigator',
property: 'clipboard',
message:
'Use copyToClipboard from @/utils/clipboard — it falls back to execCommand when the Clipboard API is unavailable (Telegram WebView).',
},
],
},
},
{
// Canonical implementations that intentionally call the restricted browser APIs.
files: ['src/platform/**/*.{ts,tsx}', 'src/utils/clipboard.ts'],
rules: {
'no-restricted-properties': 'off',
},
},
eslintConfigPrettier,
);

1644
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,10 +8,11 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc && vite build", "build": "tsc && vite build",
"lint": "eslint .", "lint": "biome lint .",
"lint:fix": "eslint . --fix", "lint:fix": "biome lint --write .",
"format": "prettier --write \"src/**/*.{ts,tsx,css,json}\"", "format": "biome format --write .",
"format:check": "prettier --check \"src/**/*.{ts,tsx,css,json}\"", "format:check": "biome format .",
"check": "biome check .",
"type-check": "tsc --noEmit", "type-check": "tsc --noEmit",
"preview": "vite preview", "preview": "vite preview",
"prepare": "husky" "prepare": "husky"
@@ -74,35 +75,26 @@
"zustand": "^5.0.11" "zustand": "^5.0.11"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.39.2", "@biomejs/biome": "2.5.3",
"@types/dompurify": "^3.0.5", "@types/dompurify": "^3.0.5",
"@types/node": "^25.2.1", "@types/node": "^25.2.1",
"@types/react": "^19.2.13", "@types/react": "^19.2.13",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@types/react-twemoji": "^0.4.3", "@types/react-twemoji": "^0.4.3",
"@vitejs/plugin-react": "^5.1.2", "@vitejs/plugin-react": "^5.1.2",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.0",
"globals": "^17.1.0",
"husky": "^9.1.7", "husky": "^9.1.7",
"lint-staged": "^16.2.7", "lint-staged": "^16.2.7",
"postcss": "^8.4.31", "postcss": "^8.4.31",
"prettier": "^3.8.1",
"prettier-plugin-tailwindcss": "^0.7.2",
"tailwindcss": "^3.4.19", "tailwindcss": "^3.4.19",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"typescript-eslint": "^8.54.0",
"vite": "^7.3.1" "vite": "^7.3.1"
}, },
"lint-staged": { "lint-staged": {
"*.{ts,tsx}": [ "*.{ts,tsx,json}": [
"eslint --fix", "biome check --write --no-errors-on-unmatched"
"prettier --write"
], ],
"*.{css,json}": [ "*.css": [
"prettier --write" "biome format --write --no-errors-on-unmatched"
] ]
} }
} }

View File

@@ -284,11 +284,7 @@ export const banSystemApi = {
// Users // Users
getUsers: async ( getUsers: async (
params: { params: { offset?: number; limit?: number; status?: string } = {},
offset?: number;
limit?: number;
status?: string;
} = {},
): Promise<BanUsersListResponse> => { ): Promise<BanUsersListResponse> => {
const response = await apiClient.get('/cabinet/admin/ban-system/users', { params }); const response = await apiClient.get('/cabinet/admin/ban-system/users', { params });
return response.data; return response.data;
@@ -349,11 +345,7 @@ export const banSystemApi = {
// Agents // Agents
getAgents: async ( getAgents: async (
params: { params: { search?: string; health?: string; status?: string } = {},
search?: string;
health?: string;
status?: string;
} = {},
): Promise<BanAgentsListResponse> => { ): Promise<BanAgentsListResponse> => {
const response = await apiClient.get('/cabinet/admin/ban-system/agents', { params }); const response = await apiClient.get('/cabinet/admin/ban-system/agents', { params });
return response.data; return response.data;

View File

@@ -52,7 +52,8 @@ const cardVariants = cva(
); );
export interface CardProps export interface CardProps
extends Omit<HTMLMotionProps<'div'>, 'children'>, VariantProps<typeof cardVariants> { extends Omit<HTMLMotionProps<'div'>, 'children'>,
VariantProps<typeof cardVariants> {
children: ReactNode; children: ReactNode;
asChild?: boolean; asChild?: boolean;
haptic?: boolean; haptic?: boolean;

View File

@@ -7,7 +7,8 @@ import { buttonTap, buttonHover, springTransition } from '../../motion/transitio
import { buttonVariants, type ButtonVariants } from './Button.variants'; import { buttonVariants, type ButtonVariants } from './Button.variants';
export interface ButtonProps export interface ButtonProps
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'disabled'>, ButtonVariants { extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'disabled'>,
ButtonVariants {
children: ReactNode; children: ReactNode;
asChild?: boolean; asChild?: boolean;
disabled?: boolean; disabled?: boolean;

View File

@@ -60,9 +60,8 @@ export const DialogOverlay = forwardRef<HTMLDivElement, DialogOverlayProps>(
DialogOverlay.displayName = 'DialogOverlay'; DialogOverlay.displayName = 'DialogOverlay';
// Content // Content
export interface DialogContentProps extends ComponentPropsWithoutRef< export interface DialogContentProps
typeof DialogPrimitive.Content extends ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
> {
showCloseButton?: boolean; showCloseButton?: boolean;
} }

View File

@@ -16,9 +16,8 @@ export {
} from '@radix-ui/react-dropdown-menu'; } from '@radix-ui/react-dropdown-menu';
// SubTrigger // SubTrigger
export interface DropdownMenuSubTriggerProps extends ComponentPropsWithoutRef< export interface DropdownMenuSubTriggerProps
typeof DropdownMenuPrimitive.SubTrigger extends ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> {
> {
inset?: boolean; inset?: boolean;
} }
@@ -115,9 +114,8 @@ export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuConten
DropdownMenuContent.displayName = 'DropdownMenuContent'; DropdownMenuContent.displayName = 'DropdownMenuContent';
// Item // Item
export interface DropdownMenuItemProps extends ComponentPropsWithoutRef< export interface DropdownMenuItemProps
typeof DropdownMenuPrimitive.Item extends ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> {
> {
inset?: boolean; inset?: boolean;
destructive?: boolean; destructive?: boolean;
} }
@@ -210,9 +208,8 @@ export const DropdownMenuRadioItem = forwardRef<HTMLDivElement, DropdownMenuRadi
DropdownMenuRadioItem.displayName = 'DropdownMenuRadioItem'; DropdownMenuRadioItem.displayName = 'DropdownMenuRadioItem';
// Label // Label
export interface DropdownMenuLabelProps extends ComponentPropsWithoutRef< export interface DropdownMenuLabelProps
typeof DropdownMenuPrimitive.Label extends ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> {
> {
inset?: boolean; inset?: boolean;
} }

View File

@@ -13,9 +13,8 @@ export {
} from '@radix-ui/react-popover'; } from '@radix-ui/react-popover';
// Content // Content
export interface PopoverContentProps extends ComponentPropsWithoutRef< export interface PopoverContentProps
typeof PopoverPrimitive.Content extends ComponentPropsWithoutRef<typeof PopoverPrimitive.Content> {
> {
showCloseButton?: boolean; showCloseButton?: boolean;
} }

View File

@@ -9,9 +9,8 @@ import { dropdown, dropdownTransition } from '../../motion/transitions';
export { Root as Select, Group as SelectGroup } from '@radix-ui/react-select'; export { Root as Select, Group as SelectGroup } from '@radix-ui/react-select';
// Trigger // Trigger
export interface SelectTriggerProps extends ComponentPropsWithoutRef< export interface SelectTriggerProps
typeof SelectPrimitive.Trigger extends ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger> {
> {
placeholder?: string; placeholder?: string;
} }

View File

@@ -1,5 +1,5 @@
import * as DialogPrimitive from '@radix-ui/react-dialog'; import * as DialogPrimitive from '@radix-ui/react-dialog';
import { motion, AnimatePresence, useDragControls, PanInfo } from 'framer-motion'; import { motion, AnimatePresence, useDragControls, type PanInfo } from 'framer-motion';
import { import {
forwardRef, forwardRef,
type ComponentPropsWithoutRef, type ComponentPropsWithoutRef,
@@ -97,9 +97,8 @@ export const SheetOverlay = forwardRef<HTMLDivElement, SheetOverlayProps>(
SheetOverlay.displayName = 'SheetOverlay'; SheetOverlay.displayName = 'SheetOverlay';
// Content // Content
export interface SheetContentProps extends ComponentPropsWithoutRef< export interface SheetContentProps
typeof DialogPrimitive.Content extends ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
> {
showDragHandle?: boolean; showDragHandle?: boolean;
showCloseButton?: boolean; showCloseButton?: boolean;
enableDragToClose?: boolean; enableDragToClose?: boolean;

View File

@@ -3,10 +3,8 @@ import { forwardRef, type ComponentPropsWithoutRef } from 'react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { usePlatform } from '@/platform'; import { usePlatform } from '@/platform';
export interface SwitchProps extends Omit< export interface SwitchProps
ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>, extends Omit<ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>, 'onChange'> {
'onChange'
> {
label?: string; label?: string;
description?: string; description?: string;
onChange?: (checked: boolean) => void; onChange?: (checked: boolean) => void;

View File

@@ -357,7 +357,7 @@ export default function Wheel() {
// Web-only: synchronously pre-open a tab during the user gesture to dodge the // Web-only: synchronously pre-open a tab during the user gesture to dodge the
// popup blocker before the async invoice URL resolves. Not reached in Telegram // popup blocker before the async invoice URL resolves. Not reached in Telegram
// (hasInvoice is true there, so the native invoice flow is used instead). // (hasInvoice is true there, so the native invoice flow is used instead).
// eslint-disable-next-line no-restricted-properties // biome-ignore lint: canonical popup-blocker workaround, see comment above
preOpenedWindowRef.current = window.open('about:blank', '_blank') || null; preOpenedWindowRef.current = window.open('about:blank', '_blank') || null;
} }
starsInvoiceMutation.mutate(); starsInvoiceMutation.mutate();

View File

@@ -8,15 +8,15 @@
of the font stacks fixes every flag everywhere without touching any markup of the font stacks fixes every flag everywhere without touching any markup
and without affecting any other text. Root fix, not a per-component wrap. */ and without affecting any other text. Root fix, not a per-component wrap. */
@font-face { @font-face {
font-family: 'Twemoji Country Flags'; font-family: "Twemoji Country Flags";
unicode-range: U+1F1E6-1F1FF, U+1F3F4, U+E0062-E007F; unicode-range: U+1F1E6-1F1FF, U+1F3F4, U+E0062-E007F;
src: url('/fonts/TwemojiCountryFlags.woff2') format('woff2'); src: url("/fonts/TwemojiCountryFlags.woff2") format("woff2");
font-display: swap; font-display: swap;
} }
/* Animated gradient border — @property enables CSS-only angle animation */ /* Animated gradient border — @property enables CSS-only angle animation */
@property --border-angle { @property --border-angle {
syntax: '<angle>'; syntax: "<angle>";
initial-value: 0deg; initial-value: 0deg;
inherits: false; inherits: false;
} }
@@ -28,13 +28,13 @@
background: background:
linear-gradient(var(--_bg), var(--_bg)) padding-box, linear-gradient(var(--_bg), var(--_bg)) padding-box,
conic-gradient( conic-gradient(
from var(--border-angle), from var(--border-angle),
var(--_accent) 0%, var(--_accent) 0%,
transparent 25%, transparent 25%,
transparent 75%, transparent 75%,
var(--_accent) 100% var(--_accent) 100%
) )
border-box; border-box;
animation: border-rotate var(--border-duration, 3s) linear infinite; animation: border-rotate var(--border-duration, 3s) linear infinite;
} }
@@ -295,7 +295,7 @@
/* Global Noise Texture — no mix-blend-mode to avoid fullscreen GPU compositing */ /* Global Noise Texture — no mix-blend-mode to avoid fullscreen GPU compositing */
body::before { body::before {
content: ''; content: "";
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: -1; z-index: -1;
@@ -450,7 +450,7 @@ img.twemoji {
/* CSS Spotlight Effect via pseudo-element */ /* CSS Spotlight Effect via pseudo-element */
.bento-card-hover::after { .bento-card-hover::after {
content: ''; content: "";
position: absolute; position: absolute;
inset: 0; inset: 0;
background: radial-gradient(circle at top, rgba(255, 255, 255, 0.06), transparent 60%); background: radial-gradient(circle at top, rgba(255, 255, 255, 0.06), transparent 60%);
@@ -625,7 +625,7 @@ img.twemoji {
/* Remove tap highlight and focus outline on mobile */ /* Remove tap highlight and focus outline on mobile */
button, button,
a, a,
[role='button'] { [role="button"] {
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
outline: none; outline: none;
} }
@@ -633,13 +633,13 @@ img.twemoji {
/* Only show focus ring for keyboard navigation */ /* Only show focus ring for keyboard navigation */
button:focus-visible, button:focus-visible,
a:focus-visible, a:focus-visible,
[role='button']:focus-visible { [role="button"]:focus-visible {
@apply ring-2 ring-accent-500/50 ring-offset-2 ring-offset-dark-900; @apply ring-2 ring-accent-500/50 ring-offset-2 ring-offset-dark-900;
} }
.light button:focus-visible, .light button:focus-visible,
.light a:focus-visible, .light a:focus-visible,
.light [role='button']:focus-visible { .light [role="button"]:focus-visible {
@apply ring-offset-champagne-100; @apply ring-offset-champagne-100;
} }
@@ -1181,11 +1181,11 @@ img.twemoji {
/* Glow effects */ /* Glow effects */
.glow-accent { .glow-accent {
box-shadow: 0 0 20px theme('colors.accent.500 / 30%'); box-shadow: 0 0 20px theme("colors.accent.500 / 30%");
} }
.glow-success { .glow-success {
box-shadow: 0 0 20px theme('colors.success.500 / 30%'); box-shadow: 0 0 20px theme("colors.success.500 / 30%");
} }
/* Blur backdrop */ /* Blur backdrop */
@@ -1229,9 +1229,9 @@ img.twemoji {
.shimmer { .shimmer {
background: linear-gradient( background: linear-gradient(
90deg, 90deg,
theme('colors.dark.800') 0%, theme("colors.dark.800") 0%,
theme('colors.dark.700') 50%, theme("colors.dark.700") 50%,
theme('colors.dark.800') 100% theme("colors.dark.800") 100%
); );
background-size: 200% 100%; background-size: 200% 100%;
animation: shimmer 1.5s infinite; animation: shimmer 1.5s infinite;
@@ -1240,9 +1240,9 @@ img.twemoji {
.light .shimmer { .light .shimmer {
background: linear-gradient( background: linear-gradient(
90deg, 90deg,
theme('colors.champagne.200') 0%, theme("colors.champagne.200") 0%,
theme('colors.champagne.300') 50%, theme("colors.champagne.300") 50%,
theme('colors.champagne.200') 100% theme("colors.champagne.200") 100%
); );
} }
@@ -1256,7 +1256,7 @@ img.twemoji {
} }
/* Color picker range input styling */ /* Color picker range input styling */
input[type='range'] { input[type="range"] {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
height: 12px; height: 12px;
@@ -1267,13 +1267,13 @@ input[type='range'] {
/* Desktop: smaller track */ /* Desktop: smaller track */
@media (min-width: 640px) { @media (min-width: 640px) {
input[type='range'] { input[type="range"] {
height: 8px; height: 8px;
border-radius: 4px; border-radius: 4px;
} }
} }
input[type='range']::-webkit-slider-thumb { input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
width: 24px; width: 24px;
@@ -1290,26 +1290,26 @@ input[type='range']::-webkit-slider-thumb {
box-shadow 0.15s ease; box-shadow 0.15s ease;
} }
input[type='range']::-webkit-slider-thumb:hover { input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.1); transform: scale(1.1);
box-shadow: box-shadow:
0 3px 8px rgba(0, 0, 0, 0.4), 0 3px 8px rgba(0, 0, 0, 0.4),
0 0 0 2px rgba(255, 255, 255, 0.2); 0 0 0 2px rgba(255, 255, 255, 0.2);
} }
input[type='range']::-webkit-slider-thumb:active { input[type="range"]::-webkit-slider-thumb:active {
transform: scale(0.95); transform: scale(0.95);
} }
/* Desktop: smaller thumb */ /* Desktop: smaller thumb */
@media (min-width: 640px) { @media (min-width: 640px) {
input[type='range']::-webkit-slider-thumb { input[type="range"]::-webkit-slider-thumb {
width: 18px; width: 18px;
height: 18px; height: 18px;
} }
} }
input[type='range']::-moz-range-thumb { input[type="range"]::-moz-range-thumb {
width: 24px; width: 24px;
height: 24px; height: 24px;
border-radius: 50%; border-radius: 50%;
@@ -1324,7 +1324,7 @@ input[type='range']::-moz-range-thumb {
box-shadow 0.15s ease; box-shadow 0.15s ease;
} }
input[type='range']::-moz-range-thumb:hover { input[type="range"]::-moz-range-thumb:hover {
transform: scale(1.1); transform: scale(1.1);
box-shadow: box-shadow:
0 3px 8px rgba(0, 0, 0, 0.4), 0 3px 8px rgba(0, 0, 0, 0.4),
@@ -1333,25 +1333,25 @@ input[type='range']::-moz-range-thumb:hover {
/* Desktop: smaller thumb */ /* Desktop: smaller thumb */
@media (min-width: 640px) { @media (min-width: 640px) {
input[type='range']::-moz-range-thumb { input[type="range"]::-moz-range-thumb {
width: 18px; width: 18px;
height: 18px; height: 18px;
} }
} }
/* Hide number input spinners for cleaner look */ /* Hide number input spinners for cleaner look */
input[type='number']::-webkit-inner-spin-button, input[type="number"]::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button { input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none; -webkit-appearance: none;
margin: 0; margin: 0;
} }
input[type='number'] { input[type="number"] {
-moz-appearance: textfield; -moz-appearance: textfield;
} }
/* Custom checkbox styling - more visible */ /* Custom checkbox styling - more visible */
input[type='checkbox'] { input[type="checkbox"] {
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
width: 1.25rem; width: 1.25rem;
@@ -1367,13 +1367,13 @@ input[type='checkbox'] {
flex-shrink: 0; flex-shrink: 0;
} }
input[type='checkbox']:checked { input[type="checkbox"]:checked {
background-color: rgb(var(--color-accent-500)); background-color: rgb(var(--color-accent-500));
border-color: rgb(var(--color-accent-500)); border-color: rgb(var(--color-accent-500));
} }
input[type='checkbox']:checked::after { input[type="checkbox"]:checked::after {
content: ''; content: "";
position: absolute; position: absolute;
left: 50%; left: 50%;
top: 45%; top: 45%;
@@ -1384,12 +1384,12 @@ input[type='checkbox']:checked::after {
transform: translate(-50%, -50%) rotate(45deg); transform: translate(-50%, -50%) rotate(45deg);
} }
input[type='checkbox']:focus { input[type="checkbox"]:focus {
outline: none; outline: none;
box-shadow: 0 0 0 2px rgba(var(--color-accent-500), 0.3); box-shadow: 0 0 0 2px rgba(var(--color-accent-500), 0.3);
} }
input[type='checkbox']:hover:not(:checked) { input[type="checkbox"]:hover:not(:checked) {
border-color: rgb(var(--color-dark-500)); border-color: rgb(var(--color-dark-500));
background-color: rgb(var(--color-dark-600)); background-color: rgb(var(--color-dark-600));
} }
@@ -1477,17 +1477,17 @@ input[type='checkbox']:hover:not(:checked) {
background-color: rgb(var(--color-dark-700)); background-color: rgb(var(--color-dark-700));
} }
.light input[type='checkbox'] { .light input[type="checkbox"] {
border-color: rgb(var(--color-champagne-400)); border-color: rgb(var(--color-champagne-400));
background-color: rgb(var(--color-champagne-100)); background-color: rgb(var(--color-champagne-100));
} }
.light input[type='checkbox']:checked { .light input[type="checkbox"]:checked {
background-color: rgb(var(--color-accent-500)); background-color: rgb(var(--color-accent-500));
border-color: rgb(var(--color-accent-500)); border-color: rgb(var(--color-accent-500));
} }
.light input[type='checkbox']:hover:not(:checked) { .light input[type="checkbox"]:hover:not(:checked) {
border-color: rgb(var(--color-champagne-500)); border-color: rgb(var(--color-champagne-500));
background-color: rgb(var(--color-champagne-200)); background-color: rgb(var(--color-champagne-200));
} }
@@ -1523,7 +1523,7 @@ input[type='checkbox']:hover:not(:checked) {
} }
.onboarding-tooltip::before { .onboarding-tooltip::before {
content: ''; content: "";
@apply absolute h-3 w-3 border-l border-t border-accent-500/30 bg-dark-900; @apply absolute h-3 w-3 border-l border-t border-accent-500/30 bg-dark-900;
transform: rotate(45deg); transform: rotate(45deg);
} }