mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 01:09:56 -05:00
540 lines
21 KiB
Svelte
540 lines
21 KiB
Svelte
<script lang="ts">
|
|
import { Bug, Lightbulb, X, ArrowSquareOut, ClipboardText, Check, CaretDown, CaretRight } from 'phosphor-svelte'
|
|
import { platformService } from '$lib/platform-service'
|
|
import { requestManager } from '$lib/request-manager'
|
|
import { settingsState } from '$lib/state/settings.svelte'
|
|
import type { Settings } from '$lib/types/settings'
|
|
import {
|
|
SETTINGS_GROUPS,
|
|
buildEnvironmentBlock,
|
|
buildSettingsBlock,
|
|
buildIssueUrl,
|
|
type ReportType,
|
|
type BugFields,
|
|
type FeatureFields,
|
|
} from './lib/bugReport'
|
|
|
|
interface Props { onClose: () => void }
|
|
let { onClose }: Props = $props()
|
|
|
|
type Step = 'type' | 'compose'
|
|
|
|
let step: Step = $state('type')
|
|
let reportType: ReportType = $state('bug')
|
|
|
|
let title = $state('')
|
|
let description = $state('')
|
|
let steps = $state('')
|
|
let expected = $state('')
|
|
let actual = $state('')
|
|
let problem = $state('')
|
|
let solution = $state('')
|
|
let alternatives = $state('')
|
|
|
|
let serverVersion = $state<string | undefined>(undefined)
|
|
|
|
$effect(() => {
|
|
requestManager.meta.getAboutServer()
|
|
.then(s => { serverVersion = s.version })
|
|
.catch(() => {})
|
|
})
|
|
|
|
let selectedKeys = $state<Set<keyof Settings>>(new Set())
|
|
let expandedGroups = $state<Set<string>>(new Set(['Library', 'Reader']))
|
|
|
|
function toggleGroup(label: string) {
|
|
const next = new Set(expandedGroups)
|
|
next.has(label) ? next.delete(label) : next.add(label)
|
|
expandedGroups = next
|
|
}
|
|
|
|
function selectGroupAll(keys: (keyof Settings)[]) {
|
|
const next = new Set(selectedKeys)
|
|
const allOn = keys.every(k => next.has(k))
|
|
keys.forEach(k => allOn ? next.delete(k) : next.add(k))
|
|
selectedKeys = next
|
|
}
|
|
|
|
function toggleKey(k: keyof Settings) {
|
|
const next = new Set(selectedKeys)
|
|
next.has(k) ? next.delete(k) : next.add(k)
|
|
selectedKeys = next
|
|
}
|
|
|
|
let copied = $state(false)
|
|
|
|
function buildPreview(): string {
|
|
const env = buildEnvironmentBlock(serverVersion)
|
|
const sets = buildSettingsBlock([...selectedKeys])
|
|
|
|
if (reportType === 'bug') {
|
|
return [
|
|
`**Description**\n${description || '(empty)'}`,
|
|
`**Steps to Reproduce**\n${steps || '(empty)'}`,
|
|
`**Expected**\n${expected || '(empty)'}`,
|
|
`**Actual**\n${actual || '(empty)'}`,
|
|
`**Environment**\n${env}`,
|
|
sets ? `**Relevant Settings**\n\`\`\`yaml\n${sets}\n\`\`\`` : null,
|
|
].filter(Boolean).join('\n\n')
|
|
} else {
|
|
return [
|
|
`**Problem / Motivation**\n${problem || '(empty)'}`,
|
|
`**Proposed Solution**\n${solution || '(empty)'}`,
|
|
alternatives ? `**Alternatives**\n${alternatives}` : null,
|
|
`**Environment**\n${env}`,
|
|
].filter(Boolean).join('\n\n')
|
|
}
|
|
}
|
|
|
|
async function handleCopy() {
|
|
await navigator.clipboard.writeText(buildPreview())
|
|
copied = true
|
|
setTimeout(() => (copied = false), 2000)
|
|
}
|
|
|
|
async function handleOpen() {
|
|
const settingsBlock = buildSettingsBlock([...selectedKeys])
|
|
const fields: BugFields | FeatureFields = reportType === 'bug'
|
|
? { description, steps, expected, actual }
|
|
: { problem, solution, alternatives }
|
|
const url = buildIssueUrl(reportType, settingsBlock, title, fields, serverVersion)
|
|
await platformService.openExternal(url)
|
|
}
|
|
|
|
function onKey(e: KeyboardEvent) { if (e.key === 'Escape') onClose() }
|
|
|
|
function formatKeyVal(key: keyof Settings): string {
|
|
const v = settingsState.settings[key]
|
|
if (v === undefined || v === null) return '~'
|
|
if (typeof v === 'boolean') return v ? 'true' : 'false'
|
|
if (typeof v === 'string') return v === '' ? '""' : v.length > 18 ? v.slice(0, 18) + '…' : v
|
|
if (typeof v === 'number') return String(v)
|
|
return '…'
|
|
}
|
|
|
|
const envBlock = $derived(buildEnvironmentBlock(serverVersion))
|
|
|
|
const canSubmit = $derived(
|
|
reportType === 'bug'
|
|
? title.trim().length > 0 && description.trim().length > 0
|
|
: title.trim().length > 0 && problem.trim().length > 0
|
|
)
|
|
|
|
function autoResize(node: HTMLTextAreaElement) {
|
|
function resize() {
|
|
node.style.height = 'auto'
|
|
node.style.height = node.scrollHeight + 'px'
|
|
}
|
|
resize()
|
|
node.addEventListener('input', resize)
|
|
return { destroy() { node.removeEventListener('input', resize) } }
|
|
}
|
|
</script>
|
|
|
|
<svelte:window onkeydown={onKey} />
|
|
|
|
<div class="br-backdrop"
|
|
role="button" tabindex="-1" aria-label="Close"
|
|
onclick={(e) => { if (e.target === e.currentTarget) onClose() }}
|
|
onkeydown={(e) => e.key === 'Escape' && onClose()}>
|
|
|
|
<div class="br-shell" role="dialog" aria-label="Report an issue"
|
|
onclick={(e) => e.stopPropagation()}
|
|
onkeydown={(e) => e.stopPropagation()}>
|
|
|
|
<header class="br-header">
|
|
<div class="br-header-left">
|
|
{#if reportType === 'bug'}
|
|
<Bug size={15} weight="duotone" class="br-header-icon bug" />
|
|
{:else}
|
|
<Lightbulb size={15} weight="duotone" class="br-header-icon feature" />
|
|
{/if}
|
|
<span class="br-title">
|
|
{step === 'type' ? 'Report an Issue' : reportType === 'bug' ? 'Bug Report' : 'Feature Request'}
|
|
</span>
|
|
</div>
|
|
<button class="br-close" onclick={onClose} aria-label="Close"><X size={14} weight="bold" /></button>
|
|
</header>
|
|
|
|
{#if step === 'type'}
|
|
<div class="br-type-step">
|
|
<p class="br-type-hint">What would you like to submit?</p>
|
|
<div class="br-type-cards">
|
|
<button class="br-type-card" class:selected={reportType === 'bug'}
|
|
onclick={() => { reportType = 'bug'; step = 'compose' }}>
|
|
<Bug size={28} weight="duotone" />
|
|
<span class="br-type-name">Bug Report</span>
|
|
<span class="br-type-desc">Something isn't working as expected</span>
|
|
</button>
|
|
<button class="br-type-card" class:selected={reportType === 'feature'}
|
|
onclick={() => { reportType = 'feature'; step = 'compose' }}>
|
|
<Lightbulb size={28} weight="duotone" />
|
|
<span class="br-type-name">Feature Request</span>
|
|
<span class="br-type-desc">Suggest an improvement or new idea</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{:else}
|
|
<div class="br-compose">
|
|
|
|
<div class="br-form">
|
|
<div class="br-form-scroll">
|
|
|
|
<div class="br-field">
|
|
<label class="br-label" for="br-title">Title <span class="br-required">*</span></label>
|
|
<input id="br-title" class="br-input" bind:value={title}
|
|
placeholder={reportType === 'bug' ? 'Short summary of the bug' : 'Short summary of your idea'}
|
|
maxlength={120} spellcheck />
|
|
</div>
|
|
|
|
{#if reportType === 'bug'}
|
|
<div class="br-field">
|
|
<label class="br-label" for="br-desc">Description <span class="br-required">*</span></label>
|
|
<textarea id="br-desc" class="br-textarea" bind:value={description}
|
|
use:autoResize
|
|
placeholder="What's broken? A clear, concise summary." rows={3} spellcheck></textarea>
|
|
</div>
|
|
<div class="br-field">
|
|
<label class="br-label" for="br-steps">Steps to Reproduce</label>
|
|
<textarea id="br-steps" class="br-textarea" bind:value={steps}
|
|
use:autoResize
|
|
placeholder={"1. Open Settings → Library\n2. Enable \"Always show card stats\"\n3. Return to Library\n4. Unread counts are not visible"}
|
|
rows={4} spellcheck></textarea>
|
|
</div>
|
|
<div class="br-field-row">
|
|
<div class="br-field">
|
|
<label class="br-label" for="br-expected">Expected</label>
|
|
<textarea id="br-expected" class="br-textarea" bind:value={expected}
|
|
use:autoResize
|
|
placeholder="What should have happened?" rows={2} spellcheck></textarea>
|
|
</div>
|
|
<div class="br-field">
|
|
<label class="br-label" for="br-actual">Actual</label>
|
|
<textarea id="br-actual" class="br-textarea" bind:value={actual}
|
|
use:autoResize
|
|
placeholder="What actually happened?" rows={2} spellcheck></textarea>
|
|
</div>
|
|
</div>
|
|
{:else}
|
|
<div class="br-field">
|
|
<label class="br-label" for="br-problem">Problem / Motivation <span class="br-required">*</span></label>
|
|
<textarea id="br-problem" class="br-textarea" bind:value={problem}
|
|
use:autoResize
|
|
placeholder="What gap or frustration does this address?" rows={3} spellcheck></textarea>
|
|
</div>
|
|
<div class="br-field">
|
|
<label class="br-label" for="br-solution">Proposed Solution <span class="br-required">*</span></label>
|
|
<textarea id="br-solution" class="br-textarea" bind:value={solution}
|
|
use:autoResize
|
|
placeholder="What would you like to see?" rows={3} spellcheck></textarea>
|
|
</div>
|
|
<div class="br-field">
|
|
<label class="br-label" for="br-alt">Alternatives Considered</label>
|
|
<textarea id="br-alt" class="br-textarea" bind:value={alternatives}
|
|
use:autoResize
|
|
placeholder="Any workarounds or other approaches?" rows={2} spellcheck></textarea>
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="br-field">
|
|
<label class="br-label">Environment <span class="br-auto">auto-filled</span></label>
|
|
<pre class="br-env-block">{envBlock}</pre>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
{#if reportType === 'bug'}
|
|
<div class="br-sidebar">
|
|
<div class="br-sidebar-header">
|
|
<span class="br-sidebar-title">Include Settings</span>
|
|
<span class="br-sidebar-hint">Select groups relevant to the bug</span>
|
|
</div>
|
|
<div class="br-sidebar-scroll">
|
|
{#each SETTINGS_GROUPS as group}
|
|
{@const selectedInGroup = group.keys.filter(k => selectedKeys.has(k)).length}
|
|
{@const allSelected = selectedInGroup === group.keys.length}
|
|
{@const expanded = expandedGroups.has(group.label)}
|
|
|
|
<div class="br-group">
|
|
<div class="br-group-row">
|
|
<button class="br-group-toggle" onclick={() => toggleGroup(group.label)}>
|
|
{#if expanded}
|
|
<CaretDown size={9} weight="bold" />
|
|
{:else}
|
|
<CaretRight size={9} weight="bold" />
|
|
{/if}
|
|
<span class="br-group-label">{group.label}</span>
|
|
{#if selectedInGroup > 0}
|
|
<span class="br-group-count">{selectedInGroup}</span>
|
|
{/if}
|
|
</button>
|
|
<button class="br-group-all"
|
|
class:active={allSelected}
|
|
onclick={() => selectGroupAll(group.keys)}
|
|
title={allSelected ? 'Deselect all' : 'Select all'}>
|
|
{allSelected ? 'none' : 'all'}
|
|
</button>
|
|
</div>
|
|
|
|
{#if expanded}
|
|
<div class="br-key-list">
|
|
{#each group.keys as key}
|
|
<label class="br-key-row">
|
|
<input type="checkbox"
|
|
checked={selectedKeys.has(key)}
|
|
onchange={() => toggleKey(key)}
|
|
class="br-checkbox" />
|
|
<span class="br-key-name">{key}</span>
|
|
<span class="br-key-val">{formatKeyVal(key)}</span>
|
|
</label>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
</div>
|
|
|
|
<footer class="br-footer">
|
|
<button class="br-btn ghost" onclick={() => (step = 'type')}>Back</button>
|
|
<div class="br-footer-right">
|
|
<button class="br-btn secondary" onclick={handleCopy} title="Copy report to clipboard">
|
|
{#if copied}
|
|
<Check size={13} /><span>Copied!</span>
|
|
{:else}
|
|
<ClipboardText size={13} /><span>Copy</span>
|
|
{/if}
|
|
</button>
|
|
<button class="br-btn primary" disabled={!canSubmit} onclick={handleOpen}>
|
|
<ArrowSquareOut size={13} /><span>Open on GitHub</span>
|
|
</button>
|
|
</div>
|
|
</footer>
|
|
{/if}
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.br-backdrop {
|
|
position: fixed; inset: 0;
|
|
z-index: calc(var(--z-settings) + 1);
|
|
display: flex; align-items: center; justify-content: center;
|
|
background: rgba(0,0,0,0.55);
|
|
backdrop-filter: blur(4px);
|
|
animation: br-fade 0.14s ease both;
|
|
}
|
|
|
|
@keyframes br-fade { from { opacity: 0 } to { opacity: 1 } }
|
|
@keyframes br-scale { from { transform: scale(0.96); opacity: 0 } to { transform: scale(1); opacity: 1 } }
|
|
|
|
.br-shell {
|
|
width: min(820px, calc(100vw - 32px));
|
|
height: min(600px, calc(100vh - 64px));
|
|
display: flex; flex-direction: column;
|
|
background: var(--bg-surface);
|
|
border: 1px solid var(--border-base);
|
|
border-radius: var(--radius-2xl);
|
|
box-shadow: 0 0 0 1px var(--border-dim), 0 32px 80px rgba(0,0,0,0.7);
|
|
overflow: hidden;
|
|
animation: br-scale 0.2s cubic-bezier(0.16,1,0.3,1) both;
|
|
}
|
|
|
|
.br-header {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 14px var(--sp-5);
|
|
border-bottom: 1px solid var(--border-dim);
|
|
flex-shrink: 0;
|
|
}
|
|
.br-header-left { display: flex; align-items: center; gap: var(--sp-2); }
|
|
.br-title { font-size: var(--text-sm); font-weight: var(--weight-medium); color: var(--text-primary); }
|
|
:global(.br-header-icon.bug) { color: var(--color-error); }
|
|
:global(.br-header-icon.feature) { color: var(--accent-fg); }
|
|
.br-close {
|
|
display: flex; align-items: center; justify-content: center;
|
|
width: 28px; height: 28px;
|
|
border-radius: var(--radius-md); border: none; background: none;
|
|
color: var(--text-faint); cursor: pointer;
|
|
transition: color var(--t-base), background var(--t-base);
|
|
}
|
|
.br-close:hover { color: var(--text-muted); background: var(--bg-raised); }
|
|
|
|
.br-type-step {
|
|
flex: 1; display: flex; flex-direction: column;
|
|
align-items: center; justify-content: center;
|
|
gap: var(--sp-5); padding: var(--sp-6);
|
|
}
|
|
.br-type-hint {
|
|
font-family: var(--font-ui); font-size: var(--text-xs);
|
|
letter-spacing: var(--tracking-wide);
|
|
color: var(--text-faint); text-transform: uppercase;
|
|
}
|
|
.br-type-cards { display: flex; gap: var(--sp-4); }
|
|
.br-type-card {
|
|
display: flex; flex-direction: column; align-items: center;
|
|
gap: var(--sp-2); padding: var(--sp-6) var(--sp-8);
|
|
border-radius: var(--radius-xl);
|
|
border: 1px solid var(--border-base);
|
|
background: var(--bg-raised);
|
|
cursor: pointer; color: var(--text-secondary);
|
|
transition: border-color var(--t-base), background var(--t-base), color var(--t-base);
|
|
min-width: 180px;
|
|
}
|
|
.br-type-card:hover { border-color: var(--border-strong); background: var(--bg-overlay); color: var(--text-primary); }
|
|
.br-type-card.selected { border-color: var(--accent); background: var(--accent-muted); color: var(--accent-fg); }
|
|
.br-type-name { font-size: var(--text-sm); font-weight: var(--weight-medium); }
|
|
.br-type-desc {
|
|
font-family: var(--font-ui); font-size: var(--text-xs);
|
|
letter-spacing: var(--tracking-wide); color: var(--text-faint);
|
|
text-align: center;
|
|
}
|
|
|
|
.br-compose { flex: 1; display: flex; overflow: hidden; min-height: 0; }
|
|
|
|
.br-form { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-width: 0; border-right: 1px solid var(--border-dim); }
|
|
.br-form-scroll { flex: 1; overflow-y: auto; padding: var(--sp-4) var(--sp-5); display: flex; flex-direction: column; gap: var(--sp-3); }
|
|
|
|
.br-field { display: flex; flex-direction: column; gap: 5px; }
|
|
.br-field-row { display: flex; gap: var(--sp-3); }
|
|
.br-field-row .br-field { flex: 1; }
|
|
|
|
.br-label {
|
|
font-family: var(--font-ui); font-size: var(--text-xs);
|
|
letter-spacing: var(--tracking-wide);
|
|
color: var(--text-faint); display: flex; align-items: center; gap: var(--sp-1);
|
|
}
|
|
.br-required { color: var(--color-error); }
|
|
.br-auto {
|
|
font-size: var(--text-2xs); padding: 1px 6px;
|
|
border-radius: var(--radius-full);
|
|
background: var(--accent-muted); color: var(--accent-fg);
|
|
border: 1px solid var(--accent-dim);
|
|
}
|
|
|
|
.br-input, .br-textarea {
|
|
background: var(--bg-raised); border: 1px solid var(--border-dim);
|
|
border-radius: var(--radius-md);
|
|
font-size: var(--text-sm); color: var(--text-primary);
|
|
padding: 8px var(--sp-3); outline: none; resize: none;
|
|
font-family: inherit; line-height: var(--leading-snug);
|
|
transition: border-color var(--t-base);
|
|
overflow: hidden;
|
|
}
|
|
.br-input:focus, .br-textarea:focus { border-color: var(--border-focus); }
|
|
.br-input::placeholder, .br-textarea::placeholder { color: var(--text-faint); }
|
|
|
|
.br-env-block {
|
|
font-family: monospace; font-size: 11px;
|
|
color: var(--text-faint); line-height: var(--leading-base);
|
|
background: var(--bg-raised); border: 1px solid var(--border-dim);
|
|
border-radius: var(--radius-md); padding: var(--sp-3);
|
|
white-space: pre-wrap; margin: 0;
|
|
}
|
|
|
|
.br-sidebar {
|
|
width: 220px; flex-shrink: 0;
|
|
display: flex; flex-direction: column;
|
|
background: var(--bg-base);
|
|
}
|
|
.br-sidebar-header {
|
|
padding: var(--sp-3) var(--sp-4) var(--sp-2);
|
|
border-bottom: 1px solid var(--border-dim);
|
|
display: flex; flex-direction: column; gap: 2px; flex-shrink: 0;
|
|
}
|
|
.br-sidebar-title {
|
|
font-family: var(--font-ui); font-size: var(--text-xs);
|
|
letter-spacing: var(--tracking-wide); color: var(--text-secondary);
|
|
font-weight: var(--weight-medium);
|
|
}
|
|
.br-sidebar-hint {
|
|
font-family: var(--font-ui); font-size: var(--text-2xs);
|
|
letter-spacing: var(--tracking-wide); color: var(--text-faint);
|
|
}
|
|
.br-sidebar-scroll { flex: 1; overflow-y: auto; padding: var(--sp-1) 0 var(--sp-2); }
|
|
|
|
.br-group { display: flex; flex-direction: column; }
|
|
|
|
.br-group-row {
|
|
display: flex; align-items: center;
|
|
padding: 3px var(--sp-3) 3px var(--sp-3);
|
|
gap: var(--sp-1);
|
|
}
|
|
.br-group-toggle {
|
|
flex: 1; display: flex; align-items: center; gap: 5px;
|
|
background: none; border: none; cursor: pointer;
|
|
color: var(--text-faint); padding: 2px 0;
|
|
transition: color var(--t-fast);
|
|
min-width: 0;
|
|
}
|
|
.br-group-toggle:hover { color: var(--text-secondary); }
|
|
.br-group-label {
|
|
font-family: var(--font-ui); font-size: var(--text-2xs);
|
|
letter-spacing: var(--tracking-wider); text-transform: uppercase;
|
|
color: inherit;
|
|
}
|
|
.br-group-count {
|
|
font-family: var(--font-ui); font-size: var(--text-2xs);
|
|
padding: 0 5px; border-radius: var(--radius-full);
|
|
background: var(--accent-muted); color: var(--accent-fg);
|
|
border: 1px solid var(--accent-dim);
|
|
line-height: 1.6;
|
|
}
|
|
.br-group-all {
|
|
font-family: var(--font-ui); font-size: var(--text-2xs);
|
|
letter-spacing: var(--tracking-wide);
|
|
background: none; border: none; cursor: pointer;
|
|
color: var(--text-faint); padding: 2px 6px;
|
|
border-radius: var(--radius-sm);
|
|
transition: color var(--t-fast), background var(--t-fast);
|
|
flex-shrink: 0;
|
|
}
|
|
.br-group-all:hover, .br-group-all.active { color: var(--accent-fg); background: var(--accent-muted); }
|
|
|
|
.br-key-list { display: flex; flex-direction: column; padding-bottom: var(--sp-1); }
|
|
.br-key-row {
|
|
display: flex; align-items: center; gap: var(--sp-2);
|
|
padding: 3px var(--sp-3) 3px 24px; cursor: pointer;
|
|
transition: background var(--t-fast);
|
|
}
|
|
.br-key-row:hover { background: var(--bg-raised); }
|
|
.br-checkbox { accent-color: var(--accent); flex-shrink: 0; cursor: pointer; }
|
|
.br-key-name {
|
|
flex: 1; font-family: monospace; font-size: 10px;
|
|
color: var(--text-secondary);
|
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
}
|
|
.br-key-val {
|
|
font-family: monospace; font-size: 10px;
|
|
color: var(--text-faint); flex-shrink: 0;
|
|
max-width: 52px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
|
}
|
|
|
|
.br-footer {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: var(--sp-3) var(--sp-5);
|
|
border-top: 1px solid var(--border-dim);
|
|
flex-shrink: 0;
|
|
}
|
|
.br-footer-right { display: flex; gap: var(--sp-2); }
|
|
|
|
.br-btn {
|
|
display: flex; align-items: center; gap: var(--sp-1);
|
|
font-family: var(--font-ui); font-size: var(--text-xs);
|
|
letter-spacing: var(--tracking-wide);
|
|
padding: 6px var(--sp-4); border-radius: var(--radius-md);
|
|
cursor: pointer; border: 1px solid transparent;
|
|
transition: background var(--t-base), color var(--t-base), border-color var(--t-base), opacity var(--t-base);
|
|
}
|
|
.br-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
.br-btn.ghost { background: none; color: var(--text-faint); border-color: transparent; }
|
|
.br-btn.ghost:hover { color: var(--text-secondary); background: var(--bg-raised); }
|
|
.br-btn.secondary { background: none; color: var(--text-muted); border-color: var(--border-dim); }
|
|
.br-btn.secondary:hover { border-color: var(--border-strong); color: var(--text-secondary); }
|
|
.br-btn.primary { background: var(--accent-muted); color: var(--accent-fg); border-color: var(--accent-dim); }
|
|
.br-btn.primary:not(:disabled):hover { filter: brightness(1.12); }
|
|
</style> |