1
0

WIP: custom dropdown + One Dark Pro theme

This commit is contained in:
Maksim Totmin 2026-06-22 09:59:20 +07:00
parent e8b4eafc2e
commit 1d10191d90
3 changed files with 386 additions and 95 deletions

View File

@ -47,6 +47,40 @@ document.addEventListener('alpine:init', () => {
keyboard: 'Keyboard shortcut', keyboard: 'Keyboard shortcut',
}, },
// ── Dropdown option sets (declarative — add a new dropdown = add a list) ──
actionTypeOptions: [
{ value: 'none', label: 'None' },
{ value: 'command', label: 'Command' },
{ value: 'builtin', label: 'Builtin' },
{ value: 'script', label: 'Script' },
{ value: 'page', label: 'Switch page' },
{ value: 'keyboard', label: 'Keyboard shortcut' },
],
builtinOptions: [
{ value: '', label: '-- select --' },
{ value: 'volume_up', label: 'Volume Up' },
{ value: 'volume_down', label: 'Volume Down' },
{ value: 'volume_mute', label: 'Volume Mute' },
{ value: 'brightness_up', label: 'Brightness Up' },
{ value: 'brightness_down', label: 'Brightness Down' },
{ value: 'media_play_pause', label: 'Play/Pause' },
{ value: 'media_next', label: 'Next Track' },
{ value: 'media_prev', label: 'Previous Track' },
{ value: 'media_stop', label: 'Stop' },
],
fontOptions: [
{ value: 'medium', label: 'Medium' },
{ value: 'regular', label: 'Regular' },
],
// Pages are dynamic — exposed as a getter so dropdowns stay reactive.
get pageOptions() {
return [
{ value: '', label: '-- select --' },
...this.pages.map(p => ({ value: p.name, label: p.name })),
]
},
// ── Init ── // ── Init ──
async init() { async init() {
await this.loadConfig() await this.loadConfig()
@ -720,4 +754,74 @@ document.addEventListener('alpine:init', () => {
return m[1].replace('T', ' ') return m[1].replace('T', ' ')
}, },
})) }))
// ── Reusable dropdown component ──
// Usage:
// x-data="dropdown({ get: () => model, set: v => model = v, options: [...] })"
// `options` may be an array or a function returning one (for reactive lists).
Alpine.data('dropdown', (config = {}) => ({
open: false,
activeIndex: -1,
_get: config.get || (() => ''),
_set: config.set || (() => {}),
_options: config.options || [],
placeholder: config.placeholder || '-- select --',
get optionList() {
return typeof this._options === 'function' ? this._options() : this._options
},
get selectedValue() {
return this._get()
},
get selectedLabel() {
const opt = this.optionList.find(o => o.value === this.selectedValue)
return opt ? opt.label : this.placeholder
},
get isPlaceholder() {
const v = this.selectedValue
const opt = this.optionList.find(o => o.value === v)
return v === '' || v == null || !opt
},
isSelected(value) {
return value === this.selectedValue
},
toggle() {
this.open = !this.open
if (this.open) {
this.activeIndex = this.optionList.findIndex(o => o.value === this.selectedValue)
}
},
close() {
this.open = false
this.activeIndex = -1
},
select(value) {
this._set(value)
this.close()
},
onTriggerKey(e) {
if (!this.open) {
if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
e.preventDefault()
this.toggle()
}
return
}
const list = this.optionList
if (e.key === 'ArrowDown') {
e.preventDefault()
this.activeIndex = (this.activeIndex + 1) % list.length
} else if (e.key === 'ArrowUp') {
e.preventDefault()
this.activeIndex = (this.activeIndex - 1 + list.length) % list.length
} else if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault()
if (this.activeIndex >= 0) this.select(list[this.activeIndex].value)
} else if (e.key === 'Escape') {
e.preventDefault()
e.stopPropagation() // keep Escape from also closing the parent modal
this.close()
}
},
}))
}) })

View File

@ -117,10 +117,20 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label>Default Font</label> <label>Default Font</label>
<select x-model="settingsForm.font"> <div class="dropdown" x-data="dropdown({ get: () => settingsForm.font, set: v => settingsForm.font = v, options: fontOptions })" @click.outside="close()">
<option value="medium">Medium</option> <button type="button" class="dropdown-trigger" :class="{ 'is-placeholder': isPlaceholder }" @click="toggle()" @keydown="onTriggerKey($event)">
<option value="regular">Regular</option> <span class="dropdown-text" x-text="selectedLabel"></span>
</select> <i class="fas fa-chevron-down dropdown-arrow"></i>
</button>
<div class="dropdown-menu" x-show="open" x-transition x-cloak>
<template x-for="(opt, oi) in optionList" :key="opt.value">
<button type="button" class="dropdown-option" :class="{ selected: isSelected(opt.value), active: activeIndex === oi }" @click="select(opt.value)" @mouseenter="activeIndex = oi">
<span x-text="opt.label"></span>
<i class="fas fa-check dropdown-check"></i>
</button>
</template>
</div>
</div>
</div> </div>
<div class="checkbox-row"> <div class="checkbox-row">
<input type="checkbox" id="show-label-bg" x-model="settingsForm.show_label_background"> <input type="checkbox" id="show-label-bg" x-model="settingsForm.show_label_background">
@ -185,12 +195,20 @@
<div class="form-row"> <div class="form-row">
<div class="form-group" style="margin-bottom:0;"> <div class="form-group" style="margin-bottom:0;">
<label>Page</label> <label>Page</label>
<select x-model="rule.page"> <div class="dropdown" x-data="dropdown({ get: () => rule.page, set: v => rule.page = v, options: () => pageOptions })" @click.outside="close()">
<option value="">--</option> <button type="button" class="dropdown-trigger" :class="{ 'is-placeholder': isPlaceholder }" @click="toggle()" @keydown="onTriggerKey($event)">
<template x-for="p in pages" :key="p.name"> <span class="dropdown-text" x-text="selectedLabel"></span>
<option :value="p.name" x-text="p.name"></option> <i class="fas fa-chevron-down dropdown-arrow"></i>
</template> </button>
</select> <div class="dropdown-menu" x-show="open" x-transition x-cloak>
<template x-for="(opt, oi) in optionList" :key="opt.value">
<button type="button" class="dropdown-option" :class="{ selected: isSelected(opt.value), active: activeIndex === oi }" @click="select(opt.value)" @mouseenter="activeIndex = oi">
<span x-text="opt.label"></span>
<i class="fas fa-check dropdown-check"></i>
</button>
</template>
</div>
</div>
</div> </div>
<div class="checkbox-row" style="margin-bottom:0;padding-top:16px;"> <div class="checkbox-row" style="margin-bottom:0;padding-top:16px;">
<input type="checkbox" x-model="rule.stay"> <input type="checkbox" x-model="rule.stay">
@ -301,14 +319,20 @@
<template x-for="t in ['tap','long_press','double_tap']" :key="t"> <template x-for="t in ['tap','long_press','double_tap']" :key="t">
<div x-show="editForm.activeTrigger === t"> <div x-show="editForm.activeTrigger === t">
<div class="form-group"> <div class="form-group">
<select x-model="editForm.actions[t].type"> <div class="dropdown" x-data="dropdown({ get: () => editForm.actions[t].type, set: v => editForm.actions[t].type = v, options: actionTypeOptions })" @click.outside="close()">
<option value="none">None</option> <button type="button" class="dropdown-trigger" :class="{ 'is-placeholder': isPlaceholder }" @click="toggle()" @keydown="onTriggerKey($event)">
<option value="command">Command</option> <span class="dropdown-text" x-text="selectedLabel"></span>
<option value="builtin">Builtin</option> <i class="fas fa-chevron-down dropdown-arrow"></i>
<option value="script">Script</option> </button>
<option value="page">Switch page</option> <div class="dropdown-menu" x-show="open" x-transition x-cloak>
<option value="keyboard">Keyboard shortcut</option> <template x-for="(opt, oi) in optionList" :key="opt.value">
</select> <button type="button" class="dropdown-option" :class="{ selected: isSelected(opt.value), active: activeIndex === oi }" @click="select(opt.value)" @mouseenter="activeIndex = oi">
<span x-text="opt.label"></span>
<i class="fas fa-check dropdown-check"></i>
</button>
</template>
</div>
</div>
</div> </div>
<template x-if="editForm.actions[t].type === 'command'"> <template x-if="editForm.actions[t].type === 'command'">
@ -325,18 +349,20 @@
<template x-if="editForm.actions[t].type === 'builtin'"> <template x-if="editForm.actions[t].type === 'builtin'">
<div class="form-group"> <div class="form-group">
<select x-model="editForm.actions[t].builtin"> <div class="dropdown" x-data="dropdown({ get: () => editForm.actions[t].builtin, set: v => editForm.actions[t].builtin = v, options: builtinOptions })" @click.outside="close()">
<option value="">-- select --</option> <button type="button" class="dropdown-trigger" :class="{ 'is-placeholder': isPlaceholder }" @click="toggle()" @keydown="onTriggerKey($event)">
<option value="volume_up">Volume Up</option> <span class="dropdown-text" x-text="selectedLabel"></span>
<option value="volume_down">Volume Down</option> <i class="fas fa-chevron-down dropdown-arrow"></i>
<option value="volume_mute">Volume Mute</option> </button>
<option value="brightness_up">Brightness Up</option> <div class="dropdown-menu" x-show="open" x-transition x-cloak>
<option value="brightness_down">Brightness Down</option> <template x-for="(opt, oi) in optionList" :key="opt.value">
<option value="media_play_pause">Play/Pause</option> <button type="button" class="dropdown-option" :class="{ selected: isSelected(opt.value), active: activeIndex === oi }" @click="select(opt.value)" @mouseenter="activeIndex = oi">
<option value="media_next">Next Track</option> <span x-text="opt.label"></span>
<option value="media_prev">Previous Track</option> <i class="fas fa-check dropdown-check"></i>
<option value="media_stop">Stop</option> </button>
</select> </template>
</div>
</div>
</div> </div>
</template> </template>
@ -347,12 +373,20 @@
</template> </template>
<div class="form-group" x-show="editForm.actions[t].type === 'page'"> <div class="form-group" x-show="editForm.actions[t].type === 'page'">
<select x-model="editForm.actions[t].page"> <div class="dropdown" x-data="dropdown({ get: () => editForm.actions[t].page, set: v => editForm.actions[t].page = v, options: () => pageOptions })" @click.outside="close()">
<option value="">-- select --</option> <button type="button" class="dropdown-trigger" :class="{ 'is-placeholder': isPlaceholder }" @click="toggle()" @keydown="onTriggerKey($event)">
<template x-for="p in pages" :key="p.name"> <span class="dropdown-text" x-text="selectedLabel"></span>
<option :value="p.name" x-text="p.name"></option> <i class="fas fa-chevron-down dropdown-arrow"></i>
</template> </button>
</select> <div class="dropdown-menu" x-show="open" x-transition x-cloak>
<template x-for="(opt, oi) in optionList" :key="opt.value">
<button type="button" class="dropdown-option" :class="{ selected: isSelected(opt.value), active: activeIndex === oi }" @click="select(opt.value)" @mouseenter="activeIndex = oi">
<span x-text="opt.label"></span>
<i class="fas fa-check dropdown-check"></i>
</button>
</template>
</div>
</div>
</div> </div>
<template x-if="editForm.actions[t].type === 'keyboard'"> <template x-if="editForm.actions[t].type === 'keyboard'">
@ -398,14 +432,20 @@
<template x-for="phase in ['start','end']" :key="phase"> <template x-for="phase in ['start','end']" :key="phase">
<div x-show="editForm.holdPhase === phase"> <div x-show="editForm.holdPhase === phase">
<div class="form-group"> <div class="form-group">
<select x-model="editForm.actions.hold[phase].type"> <div class="dropdown" x-data="dropdown({ get: () => editForm.actions.hold[phase].type, set: v => editForm.actions.hold[phase].type = v, options: actionTypeOptions })" @click.outside="close()">
<option value="none">None</option> <button type="button" class="dropdown-trigger" :class="{ 'is-placeholder': isPlaceholder }" @click="toggle()" @keydown="onTriggerKey($event)">
<option value="command">Command</option> <span class="dropdown-text" x-text="selectedLabel"></span>
<option value="builtin">Builtin</option> <i class="fas fa-chevron-down dropdown-arrow"></i>
<option value="script">Script</option> </button>
<option value="page">Switch page</option> <div class="dropdown-menu" x-show="open" x-transition x-cloak>
<option value="keyboard">Keyboard shortcut</option> <template x-for="(opt, oi) in optionList" :key="opt.value">
</select> <button type="button" class="dropdown-option" :class="{ selected: isSelected(opt.value), active: activeIndex === oi }" @click="select(opt.value)" @mouseenter="activeIndex = oi">
<span x-text="opt.label"></span>
<i class="fas fa-check dropdown-check"></i>
</button>
</template>
</div>
</div>
</div> </div>
<template x-if="editForm.actions.hold[phase].type === 'command'"> <template x-if="editForm.actions.hold[phase].type === 'command'">
@ -422,18 +462,20 @@
<template x-if="editForm.actions.hold[phase].type === 'builtin'"> <template x-if="editForm.actions.hold[phase].type === 'builtin'">
<div class="form-group"> <div class="form-group">
<select x-model="editForm.actions.hold[phase].builtin"> <div class="dropdown" x-data="dropdown({ get: () => editForm.actions.hold[phase].builtin, set: v => editForm.actions.hold[phase].builtin = v, options: builtinOptions })" @click.outside="close()">
<option value="">-- select --</option> <button type="button" class="dropdown-trigger" :class="{ 'is-placeholder': isPlaceholder }" @click="toggle()" @keydown="onTriggerKey($event)">
<option value="volume_up">Volume Up</option> <span class="dropdown-text" x-text="selectedLabel"></span>
<option value="volume_down">Volume Down</option> <i class="fas fa-chevron-down dropdown-arrow"></i>
<option value="volume_mute">Volume Mute</option> </button>
<option value="brightness_up">Brightness Up</option> <div class="dropdown-menu" x-show="open" x-transition x-cloak>
<option value="brightness_down">Brightness Down</option> <template x-for="(opt, oi) in optionList" :key="opt.value">
<option value="media_play_pause">Play/Pause</option> <button type="button" class="dropdown-option" :class="{ selected: isSelected(opt.value), active: activeIndex === oi }" @click="select(opt.value)" @mouseenter="activeIndex = oi">
<option value="media_next">Next Track</option> <span x-text="opt.label"></span>
<option value="media_prev">Previous Track</option> <i class="fas fa-check dropdown-check"></i>
<option value="media_stop">Stop</option> </button>
</select> </template>
</div>
</div>
</div> </div>
</template> </template>
@ -444,12 +486,20 @@
</template> </template>
<div class="form-group" x-show="editForm.actions.hold[phase].type === 'page'"> <div class="form-group" x-show="editForm.actions.hold[phase].type === 'page'">
<select x-model="editForm.actions.hold[phase].page"> <div class="dropdown" x-data="dropdown({ get: () => editForm.actions.hold[phase].page, set: v => editForm.actions.hold[phase].page = v, options: () => pageOptions })" @click.outside="close()">
<option value="">-- select --</option> <button type="button" class="dropdown-trigger" :class="{ 'is-placeholder': isPlaceholder }" @click="toggle()" @keydown="onTriggerKey($event)">
<template x-for="p in pages" :key="p.name"> <span class="dropdown-text" x-text="selectedLabel"></span>
<option :value="p.name" x-text="p.name"></option> <i class="fas fa-chevron-down dropdown-arrow"></i>
</template> </button>
</select> <div class="dropdown-menu" x-show="open" x-transition x-cloak>
<template x-for="(opt, oi) in optionList" :key="opt.value">
<button type="button" class="dropdown-option" :class="{ selected: isSelected(opt.value), active: activeIndex === oi }" @click="select(opt.value)" @mouseenter="activeIndex = oi">
<span x-text="opt.label"></span>
<i class="fas fa-check dropdown-check"></i>
</button>
</template>
</div>
</div>
</div> </div>
<template x-if="editForm.actions.hold[phase].type === 'keyboard'"> <template x-if="editForm.actions.hold[phase].type === 'keyboard'">

View File

@ -1,24 +1,52 @@
:root { :root {
--bg: #0a0a10; /* ── One Dark Pro palette ── */
--surface: #141420; /* Surfaces: layered dark greys built around One Dark's #282c34 base */
--surface-hover: #1a1a2a; --bg: #282c34;
--border: #252540; --surface: #2f343f;
--border-light: #333358; --surface-hover: #3a3f4b;
--text: #e4e4f0; --surface-sunken: #21252b;
--text-muted: #7a7a90; --border: #3e4451;
--accent: #6366f1; --border-light: #4b5263;
--accent-glow: rgba(99, 102, 241, 0.3);
--danger: #ef4444; /* Text */
--success: #22c55e; --text: #abb2bf;
--text-muted: #5c6370;
/* Accent — purple (One Dark Pro signature) */
--accent: #c678dd;
--accent-hover: #d292e8;
--accent-active: #b561d0;
--accent-glow: rgba(198, 120, 221, 0.28);
--accent-soft: rgba(198, 120, 221, 0.12);
--on-accent: #21252b; /* dark text on the bright accent — WCAG AA (5.3:1) */
/* Semantic */
--danger: #e06c75;
--danger-soft: rgba(224, 108, 117, 0.12);
--success: #98c379;
--warning: #e5c07b;
/* Radii */
--radius: 12px; --radius: 12px;
--radius-sm: 8px; --radius-sm: 8px;
--radius-xs: 6px; --radius-xs: 6px;
--shadow: 0 4px 24px rgba(0, 0, 0, 0.4);
/* Elevation */
--shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
--shadow-sm: 0 2px 10px rgba(0, 0, 0, 0.35);
/* Motion */
--transition: 150ms cubic-bezier(0.4, 0, 0.2, 1); --transition: 150ms cubic-bezier(0.4, 0, 0.2, 1);
--transition-spring: 220ms cubic-bezier(0.16, 1, 0.3, 1);
/* Typography */
--font-mono: 'SF Mono', 'Fira Code', 'JetBrains Mono', ui-monospace, monospace;
} }
* { margin: 0; padding: 0; box-sizing: border-box; } * { margin: 0; padding: 0; box-sizing: border-box; }
[x-cloak] { display: none !important; }
html, body { html, body {
height: 100%; height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Inter', sans-serif;
@ -112,6 +140,7 @@ main {
background: var(--surface); background: var(--surface);
border-radius: var(--radius); border-radius: var(--radius);
border: 1px solid var(--border); border: 1px solid var(--border);
box-shadow: var(--shadow-sm);
width: 100%; width: 100%;
max-width: 525px; max-width: 525px;
contain: layout style; contain: layout style;
@ -140,7 +169,7 @@ main {
.deck-btn:hover { color: var(--text); border-color: var(--border-light); } .deck-btn:hover { color: var(--text); border-color: var(--border-light); }
.deck-btn.active { .deck-btn.active {
background: var(--accent); background: var(--accent);
color: #fff; color: var(--on-accent);
border-color: var(--accent); border-color: var(--accent);
} }
@ -148,7 +177,7 @@ main {
aspect-ratio: 1; aspect-ratio: 1;
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
background: #0d0d18; background: var(--surface-sunken);
cursor: pointer; cursor: pointer;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
@ -164,8 +193,9 @@ main {
.key-btn:hover { .key-btn:hover {
border-color: var(--accent); border-color: var(--accent);
box-shadow: 0 0 12px var(--accent-glow); box-shadow: 0 0 0 1px var(--accent), 0 4px 16px var(--accent-glow);
transform: scale(1.03); transform: scale(1.04);
z-index: 1;
} }
.key-btn:active { transform: scale(0.97); } .key-btn:active { transform: scale(0.97); }
@ -210,8 +240,9 @@ main {
.modal-overlay { .modal-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
background: rgba(0, 0, 0, 0.6); background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(4px); backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
z-index: 100; z-index: 100;
display: flex; display: flex;
align-items: center; align-items: center;
@ -257,7 +288,7 @@ main {
overflow: hidden; overflow: hidden;
border: 1px solid var(--border); border: 1px solid var(--border);
position: relative; position: relative;
background: #0d0d18; background: var(--surface-sunken);
cursor: pointer; cursor: pointer;
} }
@ -303,38 +334,139 @@ main {
.form-group input, .form-group input,
.form-group select { .form-group select {
width: 100%; width: 100%;
padding: 8px 10px; padding: 9px 11px;
background: var(--bg); background: var(--surface-sunken);
border: 1px solid var(--border); border: 1px solid var(--border);
border-radius: var(--radius-xs); border-radius: var(--radius-xs);
color: var(--text); color: var(--text);
font-size: 13px; font-size: 13px;
font-family: inherit; font-family: inherit;
outline: none; outline: none;
transition: border-color var(--transition); transition: border-color var(--transition), box-shadow var(--transition), background var(--transition);
}
.form-group input:hover,
.form-group select:hover {
border-color: var(--border-light);
} }
.form-group input:focus, .form-group input:focus,
.form-group select:focus { .form-group select:focus {
border-color: var(--accent); border-color: var(--accent);
box-shadow: 0 0 0 2px var(--accent-glow); box-shadow: 0 0 0 3px var(--accent-soft);
} }
.form-group input::placeholder { .form-group input::placeholder {
color: var(--text-muted); color: var(--text-muted);
opacity: 0.5; opacity: 0.7;
} }
.form-group select { .form-group select {
cursor: pointer; cursor: pointer;
-webkit-appearance: none; -webkit-appearance: none;
appearance: none; appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 5l3 3 3-3' fill='none' stroke='%237a7a90' stroke-width='1.5'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M3 5l3 3 3-3' fill='none' stroke='%235c6370' stroke-width='1.5'/%3E%3C/svg%3E");
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: right 10px center; background-position: right 10px center;
padding-right: 28px; padding-right: 28px;
} }
/* Custom dropdown
Inline-expand pattern: the menu lives in normal document flow, so it
can never be clipped by an ancestor with overflow:auto (e.g. the modal).
Reusable across every <select> replacement. */
.dropdown {
position: relative;
width: 100%;
}
.dropdown-trigger {
width: 100%;
padding: 9px 11px;
background: var(--surface-sunken);
border: 1px solid var(--border);
border-radius: var(--radius-xs);
color: var(--text);
font-size: 13px;
font-family: inherit;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
text-align: left;
transition: border-color var(--transition), box-shadow var(--transition), background var(--transition);
}
.dropdown-trigger:hover { border-color: var(--border-light); }
.dropdown.open > .dropdown-trigger,
.dropdown-trigger:focus-visible {
border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft);
}
.dropdown-trigger .dropdown-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dropdown-trigger.is-placeholder .dropdown-text { color: var(--text-muted); }
.dropdown-arrow {
flex-shrink: 0;
font-size: 10px;
color: var(--text-muted);
transition: transform var(--transition), color var(--transition);
}
.dropdown.open > .dropdown-trigger .dropdown-arrow {
transform: rotate(180deg);
color: var(--accent);
}
.dropdown-menu {
margin-top: 4px;
background: var(--surface);
border: 1px solid var(--border-light);
border-radius: var(--radius-xs);
box-shadow: var(--shadow-sm);
overflow-y: auto;
max-height: 260px;
padding: 4px;
}
.dropdown-option {
width: 100%;
padding: 8px 10px;
background: none;
border: none;
border-radius: var(--radius-xs);
color: var(--text-muted);
font-size: 13px;
font-family: inherit;
text-align: left;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
transition: background var(--transition), color var(--transition);
}
.dropdown-option:hover,
.dropdown-option.active {
background: var(--surface-hover);
color: var(--text);
}
.dropdown-option.selected {
color: var(--accent);
background: var(--accent-soft);
}
.dropdown-option .dropdown-check {
font-size: 10px;
opacity: 0;
flex-shrink: 0;
}
.dropdown-option.selected .dropdown-check { opacity: 1; }
.form-row { .form-row {
display: flex; display: flex;
gap: 8px; gap: 8px;
@ -401,18 +533,23 @@ main {
transition: all var(--transition); transition: all var(--transition);
} }
.btn:active { transform: translateY(1px); }
.btn-primary { .btn-primary {
background: var(--accent); background: var(--accent);
color: #fff; color: var(--on-accent);
border-color: var(--accent); border-color: var(--accent);
flex: 1; flex: 1;
} }
.btn-primary:hover { .btn-primary:hover {
background: #4f46e5; background: var(--accent-hover);
box-shadow: 0 0 16px var(--accent-glow); border-color: var(--accent-hover);
box-shadow: 0 4px 16px var(--accent-glow);
} }
.btn-primary:active { background: var(--accent-active); }
.btn-outline { .btn-outline {
background: transparent; background: transparent;
border-color: var(--border); border-color: var(--border);
@ -655,7 +792,7 @@ main {
.tab-btn:hover { color: var(--text); } .tab-btn:hover { color: var(--text); }
.tab-btn.active { .tab-btn.active {
background: var(--accent); background: var(--accent);
color: #fff; color: var(--on-accent);
} }
/* ── Modifier buttons ── */ /* ── Modifier buttons ── */
@ -681,7 +818,7 @@ main {
.mod-btn:hover { color: var(--text); border-color: var(--border-light); } .mod-btn:hover { color: var(--text); border-color: var(--border-light); }
.mod-btn.active { .mod-btn.active {
background: var(--accent); background: var(--accent);
color: #fff; color: var(--on-accent);
border-color: var(--accent); border-color: var(--accent);
} }
@ -771,7 +908,7 @@ main {
.action-tab:hover { color: var(--text); border-color: var(--border-light); } .action-tab:hover { color: var(--text); border-color: var(--border-light); }
.action-tab.active { .action-tab.active {
background: var(--accent); background: var(--accent);
color: #fff; color: var(--on-accent);
border-color: var(--accent); border-color: var(--accent);
} }
.action-tab.configured { .action-tab.configured {
@ -780,7 +917,7 @@ main {
} }
.action-tab.active.configured { .action-tab.active.configured {
background: var(--accent); background: var(--accent);
color: #fff; color: var(--on-accent);
border-color: var(--accent); border-color: var(--accent);
} }
.tab-check { font-size: 8px; } .tab-check { font-size: 8px; }
@ -808,6 +945,6 @@ main {
.hold-stab:hover { color: var(--text); border-color: var(--border-light); } .hold-stab:hover { color: var(--text); border-color: var(--border-light); }
.hold-stab.active { .hold-stab.active {
background: var(--accent); background: var(--accent);
color: #fff; color: var(--on-accent);
border-color: var(--accent); border-color: var(--accent);
} }