c122e088bece | atagen | 2026-10-25 02:37 UTC | 1 | <script lang="ts"> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 2 | import { onDestroy } from 'svelte'; |
445bf199a9cf | atagen | 2026-10-25 02:56 UTC | 3 | import EmojiText from './EmojiText.svelte'; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 4 | import { get } from 'svelte/store'; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 5 | import { THEMES, accent, customThemes, theme, themePreview, type ThemeId } from './appearance'; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 6 | import { |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 7 | MALICIOUS_PALETTE, THEME_COLOURS, captureTheme, isThemeColour, readThemeFile, |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 8 | themeDocument, themeStyle, type CustomTheme, type ThemeColour, type ThemeDocument |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 9 | } from './custom-themes'; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 10 | |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 11 | let draft: ThemeDocument | null = null; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 12 | let editingId: CustomTheme['id'] | null = null; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 13 | let selectedColour: ThemeColour = 'bg'; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 14 | let hexInput = ''; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 15 | let previewApp = false; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 16 | let error = ''; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 17 | let notice = ''; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 18 | let fileInput: HTMLInputElement; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 19 | |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 20 | $: selected = $customThemes.find((item) => item.id === $theme); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 21 | $: selectedName = selected?.name ?? THEMES.find((item) => item.id === $theme)?.label ?? 'Theme'; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 22 | $: if (draft) hexInput = draft.colours[selectedColour]; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 23 | $: themePreview.set(previewApp ? draft : null); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 24 | |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 25 | onDestroy(() => themePreview.set(null)); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 26 | |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 27 | function chooseTheme(id: ThemeId): void { |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 28 | draft = null; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 29 | previewApp = false; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 30 | themePreview.set(null); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 31 | accent.set(null); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 32 | theme.set(id); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 33 | error = ''; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 34 | notice = ''; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 35 | } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 36 | |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 37 | function editTheme(asNew: boolean): void { |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 38 | themePreview.set(null); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 39 | previewApp = false; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 40 | editingId = !asNew && selected ? selected.id : null; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 41 | draft = selected && !asNew ? themeDocument(selected) : captureTheme( |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 42 | `${selectedName} custom`, $theme === 'malicious' ? MALICIOUS_PALETTE : selected?.palette |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 43 | ); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 44 | selectedColour = 'bg'; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 45 | error = ''; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 46 | notice = ''; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 47 | } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 48 | |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 49 | function persistThemes(items: CustomTheme[]): void { |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 50 | localStorage.setItem('malice.customThemes', JSON.stringify(items)); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 51 | customThemes.set(items); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 52 | } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 53 | |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 54 | function saveTheme(doc: ThemeDocument, id: CustomTheme['id'] | null = null): void { |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 55 | try { |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 56 | const saved: CustomTheme = { ...themeDocument(doc), id: id ?? `custom:${crypto.randomUUID()}` }; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 57 | const items = get(customThemes); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 58 | persistThemes(id ? items.map((item) => item.id === id ? saved : item) : [...items, saved]); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 59 | chooseTheme(saved.id); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 60 | notice = `${saved.name} saved on this device.`; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 61 | } catch { |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 62 | error = 'Could not save the theme. Check that browser storage is available and has space.'; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 63 | } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 64 | } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 65 | |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 66 | function deleteTheme(): void { |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 67 | if (!selected) return; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 68 | try { |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 69 | persistThemes($customThemes.filter((item) => item.id !== selected?.id)); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 70 | chooseTheme('dark'); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 71 | notice = 'Custom theme deleted.'; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 72 | } catch { |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 73 | error = 'Could not delete the theme from browser storage.'; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 74 | } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 75 | } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 76 | |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 77 | function exportTheme(doc: ThemeDocument): void { |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 78 | const blob = new Blob([JSON.stringify(themeDocument(doc), null, 2) + '\n'], { type: 'application/json' }); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 79 | const url = URL.createObjectURL(blob); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 80 | const anchor = document.createElement('a'); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 81 | anchor.href = url; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 82 | anchor.download = `${doc.name.replace(/[^\p{L}\p{N}_-]+/gu, '-').replace(/^-|-$/g, '') || 'theme'}.malice-theme.json`; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 83 | anchor.click(); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 84 | setTimeout(() => URL.revokeObjectURL(url), 1000); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 85 | } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 86 | |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 87 | async function importTheme(event: Event): Promise<void> { |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 88 | const input = event.currentTarget as HTMLInputElement; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 89 | const file = input.files?.[0]; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 90 | input.value = ''; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 91 | if (!file) return; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 92 | error = ''; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 93 | notice = ''; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 94 | try { |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 95 | if (file.size > 65536) throw new Error('Theme files must be smaller than 64 KB.'); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 96 | const imported = readThemeFile(await file.text()); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 97 | draft = imported; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 98 | editingId = null; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 99 | previewApp = false; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 100 | selectedColour = 'bg'; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 101 | notice = 'Theme loaded. Preview it and save to add it to your themes.'; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 102 | } catch (cause) { |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 103 | error = cause instanceof Error ? cause.message : 'Could not read the theme file.'; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 104 | } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 105 | } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 106 | |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 107 | function updateColour(value: string): void { |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 108 | if (!draft || !isThemeColour(value)) return; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 109 | draft = { ...draft, colours: { ...draft.colours, [selectedColour]: value.toLowerCase() } }; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 110 | error = ''; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 111 | } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 112 | |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 113 | function updateHex(): void { |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 114 | if (!isThemeColour(hexInput)) { |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 115 | error = 'Enter a hex colour with six digits, or eight digits including opacity.'; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 116 | return; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 117 | } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 118 | updateColour(hexInput); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 119 | } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 120 | |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 121 | function updateOpacity(value: string): void { |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 122 | if (!draft) return; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 123 | const alpha = Math.round(Number(value) * 255 / 100); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 124 | updateColour(draft.colours[selectedColour].slice(0, 7) + (alpha === 255 ? '' : alpha.toString(16).padStart(2, '0'))); |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 125 | } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 126 | |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 127 | function addToPalette(): void { |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 128 | if (!draft || draft.palette.length >= 64) return; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 129 | const colour = draft.colours[selectedColour]; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 130 | if (!draft.palette.includes(colour)) draft = { ...draft, palette: [...draft.palette, colour] }; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 131 | } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 132 | |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 133 | function removeFromPalette(): void { |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 134 | if (draft) draft = { ...draft, palette: draft.palette.filter((colour) => colour !== draft?.colours[selectedColour]) }; |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 135 | } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 136 | </script> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 137 | |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 138 | <section aria-label="Theme"> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 139 | <h3 class="sec-h">Theme</h3> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 140 | <div class="themes" role="radiogroup" aria-label="Theme"> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 141 | {#each THEMES as item (item.id)} |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 142 | <button type="button" role="radio" aria-checked={$theme === item.id} class="theme-opt" class:active={$theme === item.id} |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 143 | data-swatch={item.id} on:click={() => chooseTheme(item.id)}> |
445bf199a9cf | atagen | 2026-10-25 02:56 UTC | 144 | <span class="swatch" aria-hidden="true"></span><EmojiText text={item.label} /> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 145 | </button> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 146 | {/each} |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 147 | {#each $customThemes as item (item.id)} |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 148 | <button type="button" role="radio" aria-checked={$theme === item.id} class="theme-opt" class:active={$theme === item.id} |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 149 | on:click={() => chooseTheme(item.id)}> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 150 | <span class="swatch" style:background={item.colours.bg} style:border-color={item.colours.accent} aria-hidden="true"></span> |
445bf199a9cf | atagen | 2026-10-25 02:56 UTC | 151 | <span class="theme-name"><EmojiText text={item.name} /></span> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 152 | </button> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 153 | {/each} |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 154 | </div> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 155 | <div class="toolbar"> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 156 | <button type="button" class="ghost-btn" on:click={() => editTheme(true)}>Create theme</button> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 157 | {#if selected}<button type="button" class="ghost-btn" on:click={() => editTheme(false)}>Edit theme</button>{/if} |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 158 | <button type="button" class="ghost-btn" on:click={() => fileInput.click()}>Import theme</button> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 159 | <button type="button" class="ghost-btn" disabled={draft !== null} on:click={() => exportTheme(captureTheme(selectedName, selected?.palette ?? ($theme === 'malicious' ? MALICIOUS_PALETTE : undefined)))}>Export theme</button> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 160 | {#if selected}<button type="button" class="ghost-btn danger" on:click={deleteTheme}>Delete theme</button>{/if} |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 161 | <input class="file-input" type="file" accept=".json,application/json" aria-label="Import theme file" bind:this={fileInput} on:change={importTheme} /> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 162 | </div> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 163 | {#if !draft} |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 164 | <div class="accent-row"> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 165 | <label for="accent-colour">Accent colour</label> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 166 | <input id="accent-colour" type="color" value={$accent ?? captureTheme(selectedName).colours.accent.slice(0, 7)} on:input={(event) => accent.set(event.currentTarget.value)} /> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 167 | <button type="button" class="ghost-btn" disabled={$accent === null} on:click={() => accent.set(null)}>Use theme colour</button> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 168 | </div> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 169 | {/if} |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 170 | |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 171 | {#if draft} |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 172 | <div class="editor"> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 173 | <label class="field">Theme name<input type="text" maxlength="80" bind:value={draft.name} /></label> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 174 | <label class="field">Colour scheme |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 175 | <select bind:value={draft.scheme}><option value="dark">Dark</option><option value="light">Light</option></select> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 176 | </label> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 177 | <div class="colour-roles" role="radiogroup" aria-label="Colour to edit"> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 178 | {#each THEME_COLOURS as [key, label] (key)} |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 179 | <button type="button" role="radio" aria-checked={selectedColour === key} class="role-opt" class:active={selectedColour === key} |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 180 | on:click={() => selectedColour = key}> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 181 | <span class="swatch" style:background={draft.colours[key]} aria-hidden="true"></span>{label} |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 182 | </button> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 183 | {/each} |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 184 | </div> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 185 | <div class="colour-edit"> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 186 | <input type="color" aria-label="Pick colour" value={draft.colours[selectedColour].slice(0, 7)} |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 187 | on:input={(event) => updateColour(event.currentTarget.value + draft!.colours[selectedColour].slice(7))} /> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 188 | <label class="hex-field">Hex colour<input type="text" maxlength="9" spellcheck="false" bind:value={hexInput} on:change={updateHex} aria-invalid={!!error && !isThemeColour(hexInput)} /></label> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 189 | <label class="opacity-field">Opacity |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 190 | <input type="range" min="0" max="100" step="1" |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 191 | value={Math.round(parseInt(draft.colours[selectedColour].slice(7) || 'ff', 16) / 255 * 100)} |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 192 | on:input={(event) => updateOpacity(event.currentTarget.value)} /> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 193 | </label> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 194 | </div> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 195 | <h4 class="sec-h">Palette</h4> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 196 | <div class="palette" aria-label="Theme palette"> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 197 | {#each draft.palette as colour, index (`${index}-${colour}`)} |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 198 | <button type="button" class="palette-colour" style:background={colour} title={colour} aria-label={`Use ${colour}`} |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 199 | on:click={() => updateColour(colour)}></button> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 200 | {/each} |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 201 | </div> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 202 | <div class="toolbar"> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 203 | <button type="button" class="ghost-btn" disabled={draft.palette.length >= 64 || draft.palette.includes(draft.colours[selectedColour])} on:click={addToPalette}>Add colour</button> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 204 | <button type="button" class="ghost-btn" disabled={!draft.palette.includes(draft.colours[selectedColour])} on:click={removeFromPalette}>Remove colour</button> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 205 | </div> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 206 | <label class="field">Sender name lightness |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 207 | <input type="range" min="0" max="100" step="1" bind:value={draft.nameLightness} /> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 208 | </label> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 209 | <div class="theme-preview" style={themeStyle(draft)} aria-label="Theme preview"> |
445bf199a9cf | atagen | 2026-10-25 02:56 UTC | 210 | <div class="preview-heading"><span><EmojiText text={draft.name || 'Your theme'} /></span><span class="preview-muted">just now</span></div> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 211 | <p>A little room to make your own. <span class="preview-link">A shared link</span></p> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 212 | <div class="preview-status"><span class="preview-ok">Connected</span><span class="preview-warn">Warning</span><span class="preview-error">Error</span></div> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 213 | <div class="preview-composer"><span class="preview-muted">Write a message…</span><span class="primary-btn" aria-hidden="true">Send</span></div> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 214 | </div> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 215 | <label class="preview-toggle"><input type="checkbox" bind:checked={previewApp} />Preview across the app</label> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 216 | <div class="toolbar"> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 217 | <button type="button" class="primary-btn" disabled={!draft.name.trim() || !isThemeColour(hexInput)} on:click={() => draft && saveTheme(draft, editingId)}>Save theme</button> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 218 | {#if editingId}<button type="button" class="ghost-btn" disabled={!draft.name.trim() || !isThemeColour(hexInput)} on:click={() => draft && saveTheme(draft)}>Save as new</button>{/if} |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 219 | <button type="button" class="ghost-btn" on:click={() => draft && exportTheme(draft)}>Export draft</button> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 220 | <button type="button" class="ghost-btn" on:click={() => { draft = null; previewApp = false; error = ''; notice = ''; }}>Cancel</button> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 221 | </div> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 222 | </div> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 223 | {/if} |
445bf199a9cf | atagen | 2026-10-25 02:56 UTC | 224 | {#if error}<p class="theme-error" role="alert"><EmojiText text={error} /></p>{/if} |
445bf199a9cf | atagen | 2026-10-25 02:56 UTC | 225 | {#if notice}<p class="theme-notice" role="status"><EmojiText text={notice} /></p>{/if} |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 226 | </section> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 227 | |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 228 | <style> |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 229 | .sec-h { font-size: 12px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); margin: 0 0 6px; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 230 | .themes { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(130px, 100%), 1fr)); gap: 6px; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 231 | .theme-opt, .role-opt { display: flex; align-items: center; gap: 8px; min-width: 0; padding: 7px 8px; background: var(--bg-soft); border: 1px solid var(--border); border-radius: var(--r-md); color: var(--text); font-size: 13px; cursor: pointer; text-align: left; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 232 | .theme-opt.active, .role-opt.active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 233 | .theme-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 234 | .swatch { flex: none; width: 18px; height: 18px; border-radius: 50%; border: 1px solid var(--border); } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 235 | [data-swatch='system'] .swatch { background: linear-gradient(135deg, #111218 50%, #f7f7fa 50%); } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 236 | [data-swatch='dark'] .swatch { background: #111218; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 237 | [data-swatch='light'] .swatch { background: #f7f7fa; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 238 | [data-swatch='black'] .swatch { background: #000; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 239 | [data-swatch='nord'] .swatch { background: #2e3440; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 240 | [data-swatch='malicious'] .swatch { background: #1b2021; border-color: #5abfc9; box-shadow: inset -6px 0 #ffb852; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 241 | .toolbar { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 242 | .file-input { display: none; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 243 | .accent-row { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 10px; font-size: 13px; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 244 | .accent-row label { flex: 1; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 245 | input[type='color'] { width: 40px; height: 34px; padding: 2px; border: 1px solid var(--border); border-radius: var(--r-sm); background: var(--panel); cursor: pointer; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 246 | .editor { margin-top: 14px; padding: 12px; border: 1px solid var(--border); border-radius: var(--r-md); } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 247 | .field { display: flex; flex-direction: column; gap: 6px; font-size: 13px; color: var(--muted); margin-bottom: 12px; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 248 | input[type='text'], select { min-width: 0; width: 100%; min-height: var(--ctl-h); padding: 6px 8px; background: var(--bg-soft); border: 1px solid var(--border); border-radius: var(--r-sm); color: var(--text); font: inherit; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 249 | .colour-roles { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(140px, 100%), 1fr)); gap: 6px; max-height: 210px; overflow-y: auto; padding: 2px; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 250 | .colour-edit { display: flex; align-items: end; flex-wrap: wrap; gap: 10px; margin: 14px 0; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 251 | .hex-field, .opacity-field { display: flex; flex-direction: column; gap: 4px; font-size: 12px; color: var(--muted); } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 252 | .hex-field { flex: 1; min-width: 100px; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 253 | .opacity-field { flex: 1; min-width: 100px; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 254 | input[type='range'] { width: 100%; accent-color: var(--accent); } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 255 | .palette { display: flex; flex-wrap: wrap; gap: 6px; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 256 | .palette-colour { width: 28px; height: 28px; border: 1px solid var(--border); border-radius: var(--r-sm); cursor: pointer; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 257 | .toolbar + .field { margin-top: 14px; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 258 | .theme-preview { margin-top: 14px; padding: 12px; border: 1px solid var(--border); border-radius: var(--r-md); background: var(--bg); color: var(--text); font-size: 13px; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 259 | .preview-heading { display: flex; gap: 10px; flex-wrap: wrap; font-weight: 600; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 260 | .preview-muted { color: var(--muted); font-size: 12px; font-weight: 400; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 261 | .preview-link { color: var(--accent); } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 262 | .preview-status { display: flex; gap: 10px; flex-wrap: wrap; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 263 | .preview-ok { color: var(--ok-text); } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 264 | .preview-warn { color: var(--warn-text); background: var(--warn-bg); } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 265 | .preview-error { color: var(--danger-text); background: var(--danger-bg); } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 266 | .preview-composer { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 8px; margin-top: 12px; background: var(--panel); border: 1px solid var(--border); border-radius: var(--r-sm); } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 267 | .preview-toggle { display: flex; align-items: center; gap: 6px; font-size: 13px; margin-top: 12px; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 268 | .theme-error { color: var(--danger-text); background: var(--danger-bg); padding: 8px; border-radius: var(--r-sm); } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 269 | .theme-notice { color: var(--muted); font-size: 13px; } |
c122e088bece | atagen | 2026-10-25 02:37 UTC | 270 | </style> |