Skip to content

Commit 9d02e26

Browse files
authored
refactor(settings): declarative api (#107)
1 parent 0d1af91 commit 9d02e26

26 files changed

+795
-627
lines changed

src/lib/components/User.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
});
3535
3636
function getMentionStyle() {
37-
switch (settings.state.chat.mentionStyle) {
37+
switch (settings.state.chat.usernames.mentionStyle) {
3838
case "none":
3939
return null;
4040
case "colored":

src/lib/components/message/Highlight.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
<script lang="ts">
22
import type { Snippet } from "svelte";
3-
import type { HighlightType, HighlightTypeSettings } from "$lib/settings";
3+
import type { HighlightConfig, HighlightType } from "$lib/settings";
44
55
interface Props {
66
children: Snippet;
77
type: HighlightType | "custom";
8-
highlight: HighlightTypeSettings;
8+
highlight: HighlightConfig;
99
info?: string;
1010
}
1111

src/lib/components/message/UserMessage.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
const highlights = $derived(settings.state.highlights);
1818
1919
const customMatched = $derived(
20-
highlights.custom.find((hl) => {
20+
highlights.keywords.find((hl) => {
2121
if (!hl.pattern.trim()) return false;
2222
2323
let pattern = hl.regex ? hl.pattern : RegExp.escape(hl.pattern);
@@ -67,7 +67,7 @@
6767
}
6868
6969
function getMentionStyle(viewer?: Viewer) {
70-
switch (settings.state.chat.mentionStyle) {
70+
switch (settings.state.chat.usernames.mentionStyle) {
7171
case "none":
7272
return null;
7373
case "colored":
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<script lang="ts">
2+
import Field from "./Field.svelte";
3+
import type { SettingsCategory } from "./";
4+
5+
const { category }: { category: SettingsCategory } = $props();
6+
</script>
7+
8+
<div class="space-y-6">
9+
<h1>{category.label}</h1>
10+
11+
<div class="divide-border divide-y *:py-6 *:first:pt-0 *:last:pb-0">
12+
{#each category.fields as field}
13+
<Field {field} />
14+
{/each}
15+
</div>
16+
</div>
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<script lang="ts">
2+
import { RadioGroup, Slider, Switch } from "bits-ui";
3+
import Input from "../ui/Input.svelte";
4+
import Label from "../ui/Label.svelte";
5+
import Field from "./Field.svelte";
6+
import type { SettingsField } from "./types";
7+
8+
interface Props {
9+
field: SettingsField;
10+
depth?: number;
11+
}
12+
13+
const { field, depth = 2 }: Props = $props();
14+
15+
const heading = `h${depth}`;
16+
</script>
17+
18+
{#if field.type === "group"}
19+
<div>
20+
<svelte:element
21+
this={heading}
22+
class="mb-4 inline-block font-semibold [h2]:text-xl [h3]:text-lg"
23+
>
24+
{field.label}
25+
</svelte:element>
26+
27+
<div class="space-y-6">
28+
{#each field.fields as subField}
29+
<Field field={subField} depth={depth + 1} />
30+
{/each}
31+
</div>
32+
</div>
33+
{:else if field.type === "custom"}
34+
<div class="flex flex-col gap-2">
35+
<span class="text-lg font-semibold">{field.label}</span>
36+
37+
{@render description(field.description)}
38+
39+
<field.component />
40+
</div>
41+
{:else if field.type === "input"}
42+
<div class="flex flex-col gap-2">
43+
<Label for={field.id}>{field.label}</Label>
44+
45+
<Input
46+
id={field.id}
47+
class="max-w-1/2"
48+
type="text"
49+
autocapitalize="off"
50+
autocomplete="off"
51+
disabled={field.disabled?.()}
52+
bind:value={field.binding.get, field.binding.set}
53+
/>
54+
55+
{@render description(field.description)}
56+
</div>
57+
{:else if field.type === "radio"}
58+
<div class="flex flex-col gap-2">
59+
<Label for={field.id}>{field.label}</Label>
60+
61+
{@render description(field.description)}
62+
63+
<RadioGroup.Root
64+
id={field.id}
65+
class="group space-y-1 data-disabled:cursor-not-allowed data-disabled:opacity-50"
66+
disabled={field.disabled?.()}
67+
bind:value={field.binding.get, field.binding.set}
68+
>
69+
{#each field.options as option (option.value)}
70+
<Label
71+
class="hover:bg-muted has-data-[state=checked]:bg-muted flex items-center gap-3 rounded-md px-3 py-2 text-sm transition-colors duration-100 hover:cursor-pointer aria-disabled:cursor-not-allowed"
72+
aria-disabled={field.disabled?.()}
73+
>
74+
<RadioGroup.Item
75+
class="data-[state=checked]:border-twitch data-[state=checked]:bg-foreground size-4 rounded-full border data-[state=checked]:border-5"
76+
value={option.value}
77+
/>
78+
79+
{option.label}
80+
</Label>
81+
{/each}
82+
</RadioGroup.Root>
83+
</div>
84+
{:else if field.type === "slider"}
85+
<div>
86+
<div class="mb-4">
87+
<Label class="mb-2" for={field.id}>{field.label}</Label>
88+
89+
{@render description(field.description)}
90+
</div>
91+
92+
<Slider.Root
93+
id={field.id}
94+
class="relative flex items-center"
95+
type="single"
96+
min={field.min}
97+
max={field.max}
98+
step={field.step}
99+
disabled={field.disabled?.()}
100+
bind:value={field.binding.get, field.binding.set}
101+
>
102+
<div class="bg-input relative h-1.5 w-full rounded-full hover:cursor-pointer">
103+
<Slider.Range class="bg-twitch absolute h-full rounded-full" />
104+
</div>
105+
106+
<Slider.Thumb
107+
class="flex size-5 justify-center rounded-full bg-white hover:cursor-grab active:scale-110 active:cursor-grabbing"
108+
index={0}
109+
>
110+
<div class="mt-7 text-center text-xs font-medium">
111+
{field.binding.get()}
112+
</div>
113+
</Slider.Thumb>
114+
</Slider.Root>
115+
</div>
116+
{:else if field.type === "toggle"}
117+
<div class="space-y-2">
118+
<Label class="flex items-center justify-between hover:cursor-pointer" for={field.id}>
119+
<span class="font-medium">{field.label}</span>
120+
121+
<Switch.Root
122+
id={field.id}
123+
class="data-[state=checked]:bg-twitch data-[state=unchecked]:bg-input h-6 w-11 items-center rounded-full border-2 border-transparent transition-colors"
124+
bind:checked={field.binding.get, field.binding.set}
125+
>
126+
<Switch.Thumb
127+
class="pointer-events-none block size-4.5 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0.5"
128+
/>
129+
</Switch.Root>
130+
</Label>
131+
132+
{@render description(field.description)}
133+
</div>
134+
{/if}
135+
136+
{#snippet description(description?: string)}
137+
{#if description}
138+
<p class="text-muted-foreground text-sm">
139+
{@html description}
140+
</p>
141+
{/if}
142+
{/snippet}

src/lib/components/settings/Group.svelte

Lines changed: 0 additions & 37 deletions
This file was deleted.

src/lib/components/settings/Settings.svelte

Lines changed: 13 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,13 @@
1212
import { log } from "$lib/log";
1313
import { settings } from "$lib/settings";
1414
import TitleBar from "../TitleBar.svelte";
15-
import Appearance from "./appearance/Appearance.svelte";
16-
import Chat from "./chat/Chat.svelte";
17-
import Highlights from "./highlights/Highlights.svelte";
15+
import Category from "./Category.svelte";
16+
import { categories } from "./";
1817
1918
let { open = $bindable(false), detached = false } = $props();
2019
2120
let copied = $state(false);
2221
23-
const categories = [
24-
{
25-
name: "Appearance",
26-
icon: "lucide--monitor-cog",
27-
component: Appearance,
28-
},
29-
{
30-
name: "Chat",
31-
icon: "lucide--message-square",
32-
component: Chat,
33-
},
34-
{
35-
name: "Highlights",
36-
icon: "lucide--highlighter",
37-
component: Highlights,
38-
},
39-
];
40-
4122
$effect(() => {
4223
if (!open) {
4324
settings.saveNow().then(() => {
@@ -100,16 +81,20 @@
10081
{/snippet}
10182
</TitleBar>
10283

103-
<Tabs.Root class="relative flex h-full" orientation="vertical" value="Appearance">
84+
<Tabs.Root
85+
class="relative flex h-full"
86+
orientation="vertical"
87+
value={categories[0].label}
88+
>
10489
<nav class="h-full min-w-44 p-2 pt-0">
10590
<Tabs.List class="space-y-1">
106-
{#each categories as category (category.name)}
91+
{#each categories as category (category.label)}
10792
<Tabs.Trigger
10893
class="settings-btn text-muted-foreground data-[state=active]:bg-muted data-[state=active]:text-foreground"
109-
value={category.name}
94+
value={category.label}
11095
>
11196
<span class="iconify size-4 {category.icon}"></span>
112-
<span class="text-sm">{category.name}</span>
97+
<span class="text-sm">{category.label}</span>
11398
</Tabs.Trigger>
11499
{/each}
115100
</Tabs.List>
@@ -181,9 +166,9 @@
181166
</Dialog.Close>
182167
{/if}
183168

184-
{#each categories as category (category.name)}
185-
<Tabs.Content value={category.name}>
186-
<category.component />
169+
{#each categories as category (category.label)}
170+
<Tabs.Content value={category.label}>
171+
<Category {category} />
187172
</Tabs.Content>
188173
{/each}
189174
</div>

src/lib/components/settings/appearance/Appearance.svelte

Lines changed: 0 additions & 13 deletions
This file was deleted.

src/lib/components/settings/appearance/Theme.svelte

Lines changed: 0 additions & 40 deletions
This file was deleted.

0 commit comments

Comments
 (0)