- Clipart store holds an array of root folders instead of a single handle, plus an activeHandleIndex for the in-folder view. Getters: activeHandle, inOverview, hasAnyFolder, canGoBack, folderName. - Actions: addFolder (appends a root), enterFolder, exitFolder, removeFolder. Single-folder installs auto-drill into the sole root and behave exactly like the pre-refactor drawer; adding a 2nd+ folder pops back to the root overview. - IDB persistence migrated from the 'clipartDir' single-handle key to a 'clipartDirs' array. Legacy records are migrated on first load. saveClipartFolders unwraps Pinia proxies (toRaw per element) so IDB's structured clone accepts the payload. - ClipartItem union gained a 'folder' kind so root tiles render as first-class media items. listClipart emits nested directories too, but the drawer keeps drill-down one level deep for now. - ClipartDrawer header: three equally-sized .header-btn chips — back (only when canGoBack), refresh (when viewing a folder), and a new `+` (when any folder is attached). No more big 'Choose folder' button — empty state is a centred 96px dashed + with a wrapped hint. - Folder tiles carry a hover `×` to remove a root from the library. - DocumentSettings media tab lists all configured folders with a per- folder remove action; primary button is now 'Add folder'. - Pinia store now calls acceptHMRUpdate so Vite HMR actually swaps the store definition between reloads instead of leaving the first- load state shape behind. - Tests: store actions (addFolder / enterFolder / exitFolder / remove / IDB round-trip), drawer UI (centred + empty state, overview with folder tiles + click-to-enter, back visibility, single-root hides back, header + button triggers addFolder). Tests isolated with try/finally so a failing store assertion can't leak error state into subsequent drawer tests. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
730 lines
17 KiB
Vue
730 lines
17 KiB
Vue
<script setup lang="ts">
|
|
import { computed, ref, watch } from 'vue'
|
|
import {
|
|
PhFolder,
|
|
PhMoon,
|
|
PhSlidersHorizontal,
|
|
PhSun,
|
|
} from '@phosphor-icons/vue'
|
|
import { useClipartStore } from '@/stores/clipart'
|
|
import { useTheme } from '@/composables/useTheme'
|
|
import type { Theme } from '@/composables/useTheme'
|
|
import { useTooltipPref } from '@/composables/useTooltipPref'
|
|
import { useAssetThumbsPref } from '@/composables/useAssetThumbsPref'
|
|
import { showLocked } from '@/composables/useShowLocked'
|
|
import { Modal } from '@/components/ui'
|
|
import { setLocale, t, useI18n } from '@/i18n'
|
|
import type { Locale } from '@/i18n'
|
|
|
|
const props = defineProps<{ modelValue: boolean }>()
|
|
const emit = defineEmits<{ 'update:modelValue': [value: boolean] }>()
|
|
|
|
const clipart = useClipartStore()
|
|
const { theme, setTheme } = useTheme()
|
|
const { enabled: tooltipsEnabled } = useTooltipPref()
|
|
const { enabled: thumbsEnabled } = useAssetThumbsPref()
|
|
const { locale } = useI18n()
|
|
|
|
// Re-check the stored clipart folder whenever settings opens — covers the
|
|
// case where initial App.vue init raced ahead of IDB or the user opens
|
|
// settings before the drawer ever mounted.
|
|
watch(
|
|
() => props.modelValue,
|
|
(open) => {
|
|
if (open) void clipart.initialize()
|
|
},
|
|
)
|
|
|
|
type TabId = 'clipart' | 'interface'
|
|
|
|
const tabs: { id: TabId; labelKey: string; icon: typeof PhFolder }[] = [
|
|
{ id: 'clipart', labelKey: 'settings.tab.clipart', icon: PhFolder },
|
|
{ id: 'interface', labelKey: 'settings.tab.interface', icon: PhSlidersHorizontal },
|
|
]
|
|
|
|
const activeTab = ref<TabId>('clipart')
|
|
|
|
const selectedLocale = computed({
|
|
get: () => locale.value,
|
|
set: (value: Locale) => setLocale(value),
|
|
})
|
|
|
|
function close() {
|
|
emit('update:modelValue', false)
|
|
}
|
|
|
|
function applyTheme(t: Theme) {
|
|
setTheme(t)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Modal
|
|
:model-value="modelValue"
|
|
:title="t('settings.title')"
|
|
size="lg"
|
|
:dialog-style="{
|
|
width: 'min(920px, calc(100% - 32px))',
|
|
height: 'min(620px, calc(100vh - 48px))',
|
|
}"
|
|
:body-style="{
|
|
display: 'flex',
|
|
overflow: 'hidden',
|
|
minHeight: 0,
|
|
}"
|
|
@update:model-value="emit('update:modelValue', $event)"
|
|
>
|
|
<div class="body">
|
|
<nav class="tabs">
|
|
<button
|
|
v-for="tab in tabs"
|
|
:key="tab.id"
|
|
class="tab"
|
|
:class="{ active: activeTab === tab.id }"
|
|
@click="activeTab = tab.id"
|
|
>
|
|
<component :is="tab.icon" :size="14" />
|
|
{{ t(tab.labelKey) }}
|
|
</button>
|
|
</nav>
|
|
|
|
<div class="content">
|
|
<section v-if="activeTab === 'clipart'">
|
|
<h3>{{ t('settings.tab.clipart') }}</h3>
|
|
<div class="clipart-row">
|
|
<div class="clipart-label">{{ t('clipart.folderLabel') }}</div>
|
|
<ul v-if="clipart.hasAnyFolder" class="clipart-folder-list">
|
|
<li
|
|
v-for="(h, i) in clipart.handles"
|
|
:key="h.name + i"
|
|
class="clipart-value"
|
|
>
|
|
<PhFolder :size="13" />
|
|
<span class="clipart-folder-name">{{ h.name }}</span>
|
|
<button
|
|
class="ghost-btn small"
|
|
v-tooltip="t('clipart.removeFolder')"
|
|
@click="clipart.removeFolder(i)"
|
|
>
|
|
{{ t('clipart.clearFolder') }}
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
<div v-else class="clipart-value">
|
|
<PhFolder :size="13" />
|
|
<span class="clipart-folder-empty">{{ t('clipart.noFolder') }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="clipart-actions">
|
|
<button
|
|
class="primary-btn"
|
|
:disabled="!clipart.supported"
|
|
@click="clipart.addFolder()"
|
|
>
|
|
{{ t('clipart.addFolder') }}
|
|
</button>
|
|
</div>
|
|
<p v-if="!clipart.supported" class="error-line">
|
|
{{ t('clipart.unsupported') }}
|
|
</p>
|
|
<p v-if="clipart.error" class="error-line">{{ clipart.error }}</p>
|
|
<p class="hint">{{ t('clipart.settingsHint') }}</p>
|
|
</section>
|
|
|
|
<section v-else-if="activeTab === 'interface'">
|
|
<h3>{{ t('settings.tab.interface') }}</h3>
|
|
<div class="setting-block">
|
|
<div class="setting-heading">{{ t('settings.languageTitle') }}</div>
|
|
<div class="setting-desc">{{ t('settings.languageDesc') }}</div>
|
|
<label class="setting-select">
|
|
<span class="setting-label">{{ t('settings.languageLabel') }}</span>
|
|
<select v-model="selectedLocale">
|
|
<option value="en">{{ t('settings.languageEnglish') }}</option>
|
|
<option value="de">{{ t('settings.languageGerman') }}</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
|
|
<label class="row-toggle">
|
|
<span class="switch" :class="{ on: tooltipsEnabled }">
|
|
<input type="checkbox" v-model="tooltipsEnabled" />
|
|
</span>
|
|
<div class="row-toggle-text">
|
|
<div class="row-toggle-label">{{ t('settings.tooltipsLabel') }}</div>
|
|
<div class="row-toggle-desc">{{ t('settings.tooltipsDesc') }}</div>
|
|
</div>
|
|
</label>
|
|
|
|
<label class="row-toggle">
|
|
<span class="switch" :class="{ on: showLocked }">
|
|
<input type="checkbox" v-model="showLocked" />
|
|
</span>
|
|
<div class="row-toggle-text">
|
|
<div class="row-toggle-label">{{ t('settings.showLockedLabel') }}</div>
|
|
<div class="row-toggle-desc">{{ t('settings.showLockedDesc') }}</div>
|
|
</div>
|
|
</label>
|
|
|
|
<label class="row-toggle">
|
|
<span class="switch" :class="{ on: thumbsEnabled }">
|
|
<input type="checkbox" v-model="thumbsEnabled" />
|
|
</span>
|
|
<div class="row-toggle-text">
|
|
<div class="row-toggle-label">{{ t('settings.assetThumbsLabel') }}</div>
|
|
<div class="row-toggle-desc">{{ t('settings.assetThumbsDesc') }}</div>
|
|
</div>
|
|
</label>
|
|
|
|
<div class="setting-block">
|
|
<div class="setting-heading">{{ t('settings.appearanceTitle') }}</div>
|
|
<div class="setting-desc">{{ t('settings.themeHint') }}</div>
|
|
<div class="theme-grid">
|
|
<button
|
|
class="theme-card"
|
|
:class="{ active: theme === 'dark' }"
|
|
@click="applyTheme('dark')"
|
|
>
|
|
<div class="theme-preview theme-preview-dark">
|
|
<div class="tp-panel"></div>
|
|
<div class="tp-accent"></div>
|
|
</div>
|
|
<div class="theme-label">
|
|
<PhMoon :size="14" />
|
|
<span>{{ t('settings.themeDark') }}</span>
|
|
</div>
|
|
</button>
|
|
<button
|
|
class="theme-card"
|
|
:class="{ active: theme === 'light' }"
|
|
@click="applyTheme('light')"
|
|
>
|
|
<div class="theme-preview theme-preview-light">
|
|
<div class="tp-panel"></div>
|
|
<div class="tp-accent"></div>
|
|
</div>
|
|
<div class="theme-label">
|
|
<PhSun :size="14" />
|
|
<span>{{ t('settings.themeLight') }}</span>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
<template #footer>
|
|
<button class="primary" @click="close">{{ t('settings.done') }}</button>
|
|
</template>
|
|
</Modal>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.body {
|
|
display: flex;
|
|
flex: 1 1 auto;
|
|
min-height: 0;
|
|
}
|
|
|
|
.tabs {
|
|
width: 150px;
|
|
flex-shrink: 0;
|
|
border-right: 1px solid var(--border);
|
|
padding: 10px 8px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
background: var(--panel-alt);
|
|
min-height: 0;
|
|
}
|
|
|
|
.tab {
|
|
justify-content: flex-start;
|
|
padding: 8px 10px;
|
|
background: transparent;
|
|
border: 1px solid transparent;
|
|
color: var(--text-dim);
|
|
font-size: 12px;
|
|
border-radius: var(--radius-sm);
|
|
}
|
|
|
|
.tab:hover:not(:disabled) {
|
|
background: var(--panel-hover);
|
|
color: var(--text);
|
|
}
|
|
|
|
.tab.active {
|
|
background: var(--accent-soft);
|
|
color: var(--accent);
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
.content {
|
|
flex: 1;
|
|
padding: 16px 20px;
|
|
overflow: auto;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
}
|
|
|
|
section {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
section h3 {
|
|
margin: 0;
|
|
font-size: 13px;
|
|
text-transform: uppercase;
|
|
color: var(--text-dim-strong);
|
|
letter-spacing: 0.08em;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* A second h3 inside the same section (e.g. "Timing" under the
|
|
Document tab) needs breathing room so it reads as its own subgroup
|
|
rather than colliding with the preceding hint paragraph. */
|
|
section h3.sub-heading {
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.row {
|
|
display: flex;
|
|
gap: 10px;
|
|
}
|
|
|
|
label {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
font-size: 10px;
|
|
color: var(--text-dim);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
flex: 1 1 0;
|
|
min-width: 0;
|
|
}
|
|
|
|
label.stacked {
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
input[type='number'] {
|
|
width: 100%;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
color: var(--text);
|
|
padding: 6px 8px;
|
|
border-radius: var(--radius-sm);
|
|
font: inherit;
|
|
font-size: 12px;
|
|
text-transform: none;
|
|
letter-spacing: 0;
|
|
}
|
|
|
|
.presets {
|
|
display: flex;
|
|
gap: 6px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.preset {
|
|
padding: 4px 10px;
|
|
font-size: 11px;
|
|
background: var(--panel-alt);
|
|
border: 1px solid var(--border);
|
|
color: var(--text-dim);
|
|
border-radius: 999px;
|
|
}
|
|
|
|
.preset:hover:not(:disabled) {
|
|
background: var(--panel-hover);
|
|
color: var(--text);
|
|
}
|
|
|
|
.preset.active {
|
|
background: var(--accent-soft);
|
|
border-color: var(--accent);
|
|
color: var(--accent);
|
|
}
|
|
|
|
.color-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 4px 0;
|
|
}
|
|
|
|
.color-swatch {
|
|
width: 36px;
|
|
height: 24px;
|
|
padding: 0;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
background: transparent;
|
|
cursor: pointer;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.color-swatch::-webkit-color-swatch-wrapper {
|
|
padding: 0;
|
|
}
|
|
.color-swatch::-webkit-color-swatch {
|
|
border: none;
|
|
border-radius: 3px;
|
|
}
|
|
.color-swatch::-moz-color-swatch {
|
|
border: none;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.hex {
|
|
font-size: 12px;
|
|
font-variant-numeric: tabular-nums;
|
|
color: var(--text-dim-strong);
|
|
}
|
|
|
|
.hint {
|
|
font-size: 13px;
|
|
line-height: 1.45;
|
|
color: var(--text-dim-strong);
|
|
margin: 0;
|
|
}
|
|
|
|
.setting-block {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
padding: 12px;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
background: var(--panel-alt);
|
|
}
|
|
|
|
.setting-heading {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--text);
|
|
}
|
|
|
|
.setting-desc {
|
|
font-size: 12px;
|
|
line-height: 1.45;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.setting-select {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
flex: 0 0 auto;
|
|
font-size: 11px;
|
|
color: var(--text-dim);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.setting-label {
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.setting-select select {
|
|
width: 100%;
|
|
background: var(--bg);
|
|
border: 1px solid var(--border);
|
|
color: var(--text);
|
|
padding: 8px 10px;
|
|
border-radius: var(--radius-sm);
|
|
font: inherit;
|
|
font-size: 13px;
|
|
text-transform: none;
|
|
letter-spacing: normal;
|
|
}
|
|
|
|
/* Switch + two-line label. Defensive resets defeat the scoped
|
|
blanket `label { flex-direction: column; text-transform: uppercase;
|
|
font-size: 10px; flex: 1 1 0; }` that form-row labels in this file
|
|
depend on — without the reset that rule would stack the switch on
|
|
top of the text and uppercase everything inside. */
|
|
.row-toggle {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 10px 12px;
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
background: var(--panel-alt);
|
|
cursor: pointer;
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
font-size: 13px;
|
|
text-transform: none;
|
|
letter-spacing: normal;
|
|
color: var(--text);
|
|
flex: 0 0 auto;
|
|
}
|
|
|
|
.row-toggle-text {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 3px;
|
|
text-transform: none;
|
|
}
|
|
|
|
.row-toggle-label {
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--text);
|
|
letter-spacing: normal;
|
|
text-transform: none;
|
|
}
|
|
|
|
.row-toggle-desc {
|
|
font-size: 12px;
|
|
color: var(--text-dim);
|
|
line-height: 1.4;
|
|
letter-spacing: normal;
|
|
text-transform: none;
|
|
}
|
|
|
|
.row-toggle .switch {
|
|
position: relative;
|
|
width: 32px;
|
|
height: 18px;
|
|
flex-shrink: 0;
|
|
background: var(--border);
|
|
border-radius: 999px;
|
|
transition: background 140ms var(--ease);
|
|
}
|
|
|
|
.row-toggle .switch.on {
|
|
background: var(--accent);
|
|
}
|
|
|
|
.row-toggle .switch input {
|
|
position: absolute;
|
|
inset: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
margin: 0;
|
|
opacity: 0;
|
|
cursor: inherit;
|
|
}
|
|
|
|
.row-toggle .switch::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 2px;
|
|
left: 2px;
|
|
width: 14px;
|
|
height: 14px;
|
|
background: #fff;
|
|
border-radius: 50%;
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
|
transition: transform 140ms var(--ease);
|
|
}
|
|
|
|
.row-toggle .switch.on::after {
|
|
transform: translateX(14px);
|
|
}
|
|
|
|
.clipart-row {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.clipart-label {
|
|
font-size: 10px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
color: var(--text-dim);
|
|
}
|
|
|
|
.clipart-value {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 8px 10px;
|
|
background: var(--panel-alt);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
color: var(--text);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.clipart-folder-name {
|
|
flex: 1 1 auto;
|
|
font-variant-numeric: tabular-nums;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.clipart-folder-empty {
|
|
color: var(--text-dim);
|
|
font-style: italic;
|
|
}
|
|
|
|
.clipart-folder-list {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.ghost-btn.small {
|
|
padding: 2px 8px;
|
|
font-size: 11px;
|
|
}
|
|
|
|
.clipart-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.primary-btn {
|
|
padding: 6px 12px;
|
|
border-radius: var(--radius-sm);
|
|
background: var(--accent);
|
|
color: var(--accent-text, #fff);
|
|
border: 1px solid transparent;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.primary-btn:hover:not(:disabled) {
|
|
filter: brightness(1.1);
|
|
}
|
|
|
|
.primary-btn:disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.ghost-btn {
|
|
padding: 6px 12px;
|
|
border-radius: var(--radius-sm);
|
|
background: transparent;
|
|
color: var(--text);
|
|
border: 1px solid var(--border);
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.ghost-btn:hover {
|
|
background: var(--panel-alt);
|
|
}
|
|
|
|
.error-line {
|
|
margin: 0 0 8px;
|
|
color: var(--danger);
|
|
font-size: 11px;
|
|
}
|
|
|
|
.theme-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 10px;
|
|
}
|
|
|
|
.theme-card {
|
|
flex-direction: column;
|
|
padding: 12px;
|
|
gap: 10px;
|
|
background: var(--panel-alt);
|
|
border: 2px solid var(--border);
|
|
border-radius: var(--radius);
|
|
align-items: stretch;
|
|
}
|
|
|
|
.theme-card:hover:not(:disabled) {
|
|
background: var(--panel-hover);
|
|
border-color: var(--border-strong);
|
|
}
|
|
|
|
.theme-card.active {
|
|
border-color: var(--accent);
|
|
background: var(--accent-soft);
|
|
}
|
|
|
|
.theme-preview {
|
|
height: 74px;
|
|
border-radius: var(--radius-sm);
|
|
display: flex;
|
|
align-items: stretch;
|
|
gap: 6px;
|
|
padding: 8px;
|
|
}
|
|
|
|
.theme-preview-dark {
|
|
background: #17171b;
|
|
border: 1px solid #2e2e37;
|
|
}
|
|
|
|
.theme-preview-dark .tp-panel {
|
|
background: #272730;
|
|
flex: 1;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.theme-preview-dark .tp-accent {
|
|
background: #6ea8ff;
|
|
width: 22px;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.theme-preview-light {
|
|
background: #f3f3f6;
|
|
border: 1px solid #e2e2e7;
|
|
}
|
|
|
|
.theme-preview-light .tp-panel {
|
|
background: #ffffff;
|
|
flex: 1;
|
|
border-radius: 3px;
|
|
border: 1px solid #e2e2e7;
|
|
}
|
|
|
|
.theme-preview-light .tp-accent {
|
|
background: #2b6af5;
|
|
width: 22px;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.theme-label {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 12px;
|
|
color: var(--text);
|
|
font-weight: 500;
|
|
justify-content: center;
|
|
}
|
|
|
|
.theme-card.active .theme-label {
|
|
color: var(--accent);
|
|
}
|
|
|
|
.primary {
|
|
padding: 6px 14px;
|
|
background: var(--accent-soft);
|
|
border: 1px solid var(--accent);
|
|
color: var(--accent);
|
|
border-radius: var(--radius-sm);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.primary:hover:not(:disabled) {
|
|
background: var(--accent-soft-strong);
|
|
}
|
|
</style>
|