275 lines
7.9 KiB
Svelte
275 lines
7.9 KiB
Svelte
<script lang="ts">
|
|
import {
|
|
addConfigSetting,
|
|
changeConfigValue,
|
|
configLines,
|
|
configWarnings
|
|
} from '$lib/config-workbench.js';
|
|
import Button from './primitives/Button.svelte';
|
|
let {
|
|
text = $bindable(''),
|
|
overlay = false,
|
|
disabled = false
|
|
}: { text?: string; overlay?: boolean; disabled?: boolean } = $props();
|
|
let mode = $state<'raw' | 'settings'>('raw');
|
|
let search = $state('');
|
|
let newKey = $state('');
|
|
let newValue = $state('');
|
|
let addError = $state('');
|
|
function addSetting() {
|
|
try {
|
|
text = addConfigSetting(text, newKey, newValue);
|
|
search = '';
|
|
newKey = '';
|
|
newValue = '';
|
|
addError = '';
|
|
} catch (error) {
|
|
addError = (error as Error).message;
|
|
}
|
|
}
|
|
function addOnEnter(event: KeyboardEvent) {
|
|
if (event.key === 'Enter') {
|
|
event.preventDefault();
|
|
if (!disabled && newKey.trim() && newValue.trim()) addSetting();
|
|
}
|
|
}
|
|
const entries = $derived(configLines(text));
|
|
const visible = $derived(
|
|
entries.filter((e) => `${e.key} ${e.value}`.toLowerCase().includes(search.toLowerCase()))
|
|
);
|
|
const warnings = $derived(configWarnings(text, overlay));
|
|
</script>
|
|
|
|
<div class="editor">
|
|
<div class="toolbar">
|
|
<div class="switch" aria-label="Editor view">
|
|
<button
|
|
type="button"
|
|
class:active={mode === 'raw'}
|
|
aria-pressed={mode === 'raw'}
|
|
onclick={() => (mode = 'raw')}>Raw .cfg</button
|
|
>
|
|
<button
|
|
type="button"
|
|
class:active={mode === 'settings'}
|
|
aria-pressed={mode === 'settings'}
|
|
onclick={() => (mode = 'settings')}>Settings <span>{entries.length}</span></button
|
|
>
|
|
</div>
|
|
<span class="hint">{text.split(/\r?\n/).length} lines</span>
|
|
</div>
|
|
{#if mode === 'raw'}
|
|
<textarea
|
|
aria-label="Config contents"
|
|
bind:value={text}
|
|
{disabled}
|
|
spellcheck="false"
|
|
rows="12"
|
|
placeholder={'// Add your settings\nset g_inactivity "0"'}
|
|
></textarea>
|
|
{:else}
|
|
<div class="settings">
|
|
<input
|
|
class="search"
|
|
aria-label="Find a setting"
|
|
bind:value={search}
|
|
placeholder="Find a setting…"
|
|
/>
|
|
<p class="hint">
|
|
Edit values and inline comments. Other commands and formatting stay in the raw file.
|
|
</p>
|
|
{#each visible as entry (entry.index)}
|
|
<label class="setting"
|
|
><span><code>{entry.key}</code><small>Line {entry.index + 1}</small></span>
|
|
<input
|
|
value={entry.value}
|
|
{disabled}
|
|
aria-label={`${entry.key}, line ${entry.index + 1}`}
|
|
oninput={(e) =>
|
|
(text = changeConfigValue(text, entry.index, e.currentTarget.value))}
|
|
/>
|
|
</label>
|
|
{:else}<p class="hint">
|
|
{entries.length ? 'No matching settings.' : 'Add your first setting below.'}
|
|
</p>{/each}
|
|
<div class="add-setting">
|
|
<label
|
|
>Setting name<input
|
|
bind:value={newKey}
|
|
onkeydown={addOnEnter}
|
|
placeholder="g_inactivity"
|
|
{disabled}
|
|
/></label
|
|
><label
|
|
>Value<input
|
|
bind:value={newValue}
|
|
onkeydown={addOnEnter}
|
|
placeholder="0"
|
|
{disabled}
|
|
/></label
|
|
><Button
|
|
size="sm"
|
|
onclick={addSetting}
|
|
disabled={disabled || !newKey.trim() || !newValue.trim()}>Add setting</Button
|
|
>
|
|
</div>
|
|
{#if addError}<p class="warnings" role="alert">{addError}</p>{/if}
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
{#if warnings.length}
|
|
<details
|
|
class="warnings"
|
|
open={warnings.some((w) => w.includes('ignored') || w.includes('no value'))}
|
|
>
|
|
<summary
|
|
>{warnings.length} configuration {warnings.length === 1 ? 'note' : 'notes'} — review before
|
|
saving</summary
|
|
>
|
|
<ul>
|
|
{#each warnings as warning}<li>{warning}</li>{/each}
|
|
</ul>
|
|
</details>
|
|
{/if}
|
|
|
|
<style>
|
|
.editor {
|
|
border: 1px solid var(--border-strong);
|
|
border-radius: var(--radius-md);
|
|
overflow: hidden;
|
|
background: var(--surface-sunken);
|
|
}
|
|
.toolbar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 8px 12px;
|
|
gap: 12px;
|
|
border-bottom: 1px solid var(--border-subtle);
|
|
}
|
|
.switch {
|
|
display: flex;
|
|
gap: 4px;
|
|
}
|
|
button {
|
|
font: inherit;
|
|
font-size: var(--text-sm);
|
|
color: var(--text-secondary);
|
|
border: 0;
|
|
border-radius: var(--radius-sm);
|
|
padding: 8px 12px;
|
|
background: transparent;
|
|
cursor: pointer;
|
|
}
|
|
button.active {
|
|
color: var(--text-primary);
|
|
background: var(--surface-raised);
|
|
}
|
|
button span {
|
|
margin-left: 8px;
|
|
color: var(--text-secondary);
|
|
}
|
|
button:focus-visible,
|
|
input:focus-visible,
|
|
textarea:focus-visible {
|
|
outline: 2px solid var(--accent-primary);
|
|
outline-offset: -2px;
|
|
}
|
|
textarea {
|
|
display: block;
|
|
width: 100%;
|
|
height: 360px;
|
|
min-height: 240px;
|
|
box-sizing: border-box;
|
|
resize: vertical;
|
|
border: 0;
|
|
padding: var(--space-5);
|
|
background: transparent;
|
|
color: var(--text-primary);
|
|
font: var(--text-sm)/1.8 var(--font-mono);
|
|
tab-size: 4;
|
|
}
|
|
textarea:disabled,
|
|
input:disabled {
|
|
opacity: 0.65;
|
|
}
|
|
.settings {
|
|
padding: var(--space-4);
|
|
max-height: 520px;
|
|
overflow: auto;
|
|
}
|
|
input {
|
|
box-sizing: border-box;
|
|
background: var(--surface);
|
|
color: var(--text-primary);
|
|
border: 1px solid var(--border-strong);
|
|
border-radius: var(--radius-sm);
|
|
padding: 9px 12px;
|
|
font: var(--text-sm) var(--font-mono);
|
|
width: 100%;
|
|
}
|
|
.setting {
|
|
display: grid;
|
|
grid-template-columns: minmax(140px, 1fr) minmax(0, 1.4fr);
|
|
align-items: center;
|
|
gap: 16px;
|
|
padding: 12px 0;
|
|
border-top: 1px solid var(--border-subtle);
|
|
}
|
|
.setting span {
|
|
min-width: 0;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
.add-setting {
|
|
display: flex;
|
|
align-items: end;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
border-top: 1px solid var(--border-strong);
|
|
padding-top: 16px;
|
|
}
|
|
.add-setting label {
|
|
flex: 1 1 140px;
|
|
display: grid;
|
|
gap: 6px;
|
|
font-size: var(--text-sm);
|
|
}
|
|
code {
|
|
font: var(--text-sm) var(--font-mono);
|
|
}
|
|
small {
|
|
display: block;
|
|
color: var(--text-secondary);
|
|
font-size: var(--text-xs);
|
|
margin-top: 4px;
|
|
}
|
|
.hint {
|
|
color: var(--text-secondary);
|
|
font-size: var(--text-xs);
|
|
}
|
|
.warnings {
|
|
color: var(--state-warning);
|
|
font-size: var(--text-sm);
|
|
padding: 12px 0 0;
|
|
}
|
|
summary {
|
|
cursor: pointer;
|
|
}
|
|
li {
|
|
margin: 6px 0;
|
|
}
|
|
@media (max-width: 600px) {
|
|
.setting {
|
|
grid-template-columns: 1fr;
|
|
gap: 8px;
|
|
}
|
|
.toolbar > .hint {
|
|
display: none;
|
|
}
|
|
}
|
|
input:not([type='checkbox']):not([type='file']),
|
|
textarea {
|
|
border-color: var(--border-control);
|
|
}
|
|
</style>
|