Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -1,74 +1,274 @@
|
||||
<script lang="ts">
|
||||
import { addConfigSetting, changeConfigValue, configLines, configWarnings } from '$lib/config-workbench.js';
|
||||
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 {
|
||||
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; }
|
||||
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();
|
||||
}
|
||||
}
|
||||
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 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>
|
||||
<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>
|
||||
<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>
|
||||
<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 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>
|
||||
{: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
|
||||
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: .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); }
|
||||
.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>
|
||||
|
||||
Reference in New Issue
Block a user