feat: search bar in settings & update changelog

This commit is contained in:
2026-08-26 17:24:55 +09:30
parent de75cb4666
commit 8ccddc1ae0
4 changed files with 77 additions and 6 deletions
+36 -2
View File
@@ -61,6 +61,7 @@
let selectedBackgroundCategory = $state("All"); let selectedBackgroundCategory = $state("All");
let backgroundCategories = $state<string[]>([]); let backgroundCategories = $state<string[]>([]);
let storeSearchTerm = $state(""); let storeSearchTerm = $state("");
let settingsSearch = $state("");
let showDisclaimerModal = $state(false); let showDisclaimerModal = $state(false);
let disclaimerCallbacks = $state<{ onConfirm: () => void; onCancel: () => void } | null>(null); let disclaimerCallbacks = $state<{ onConfirm: () => void; onCancel: () => void } | null>(null);
@@ -142,6 +143,7 @@
); );
const sectionTitle = $derived.by(() => { const sectionTitle = $derived.by(() => {
if (activePage === "settings" && settingsSearch.trim()) return "Search results";
if (activePage === "themes") return "Themes"; if (activePage === "themes") return "Themes";
if (activePage === "backgrounds") return "Backgrounds"; if (activePage === "backgrounds") return "Backgrounds";
return [...userNav, ...appNav].find((item) => item.id === activeSection)?.label ?? "Settings"; return [...userNav, ...appNav].find((item) => item.id === activeSection)?.label ?? "Settings";
@@ -251,6 +253,7 @@
const selectNavItem = (id: string) => { const selectNavItem = (id: string) => {
if (activePage === "settings") { if (activePage === "settings") {
settingsSearch = "";
activeSection = id; activeSection = id;
return; return;
} }
@@ -281,6 +284,7 @@
const applyDestination = (destination: SettingsDestination) => { const applyDestination = (destination: SettingsDestination) => {
activePage = destination.page; activePage = destination.page;
if (destination.page === "settings" && destination.section) { if (destination.page === "settings" && destination.section) {
settingsSearch = "";
activeSection = destination.section; activeSection = destination.section;
} else if (destination.page === "themes" && destination.view) { } else if (destination.page === "themes" && destination.view) {
activeThemeView = destination.view === "store" ? "theme-store" : "theme-settings"; activeThemeView = destination.view === "store" ? "theme-store" : "theme-settings";
@@ -373,6 +377,32 @@
: 'w-[260px] px-4 py-5'}" : 'w-[260px] px-4 py-5'}"
aria-label="Settings categories" aria-label="Settings categories"
> >
{#if activePage === "settings"}
<label class="relative mb-4 block shrink-0">
<span class="sr-only">Search settings</span>
<svg
class="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-zinc-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m21 21-4.35-4.35m1.35-5.65a7 7 0 1 1-14 0 7 7 0 0 1 14 0Z"
/>
</svg>
<input
type="search"
placeholder="Search settings"
bind:value={settingsSearch}
class="h-10 w-full rounded-lg bg-zinc-200/70 pl-9 pr-3 text-sm text-zinc-900 placeholder:text-zinc-400 transition-colors duration-200 focus:bg-white focus:outline-none focus:ring-2 focus:ring-zinc-400 focus:ring-offset-2 dark:bg-zinc-800/80 dark:text-white dark:placeholder:text-zinc-500 dark:focus:bg-zinc-800 dark:focus:ring-offset-zinc-900"
/>
</label>
{/if}
<SidebarNav <SidebarNav
groups={navGroups} groups={navGroups}
selectedId={selectedNavId} selectedId={selectedNavId}
@@ -455,7 +485,7 @@
</div> </div>
<div class="flex-1 min-h-0 px-4 pb-8 overflow-y-auto no-scrollbar"> <div class="flex-1 min-h-0 px-4 pb-8 overflow-y-auto no-scrollbar">
{#if activePage === "settings"} {#if activePage === "settings"}
{#if activeSection === "shortcuts"} {#if activeSection === "shortcuts" && !settingsSearch.trim()}
<Shortcuts /> <Shortcuts />
{:else} {:else}
<Settings <Settings
@@ -463,8 +493,12 @@
showFontPicker={openFontPicker} showFontPicker={openFontPicker}
{showDisclaimer} {showDisclaimer}
showCloudPanel={openCloudPanel} showCloudPanel={openCloudPanel}
{activeSection} activeSection={settingsSearch.trim() ? "all" : activeSection}
searchQuery={settingsSearch}
/> />
{#if settingsSearch.trim()}
<Shortcuts searchQuery={settingsSearch} />
{/if}
{/if} {/if}
{:else if activePage === "themes"} {:else if activePage === "themes"}
<Theme section="themes" /> <Theme section="themes" />
+31 -4
View File
@@ -136,12 +136,13 @@
void loadPluginSettings(); void loadPluginSettings();
}); });
const { showColourPicker, showFontPicker, showDisclaimer, showCloudPanel, activeSection = "general" } = $props<{ const { showColourPicker, showFontPicker, showDisclaimer, showCloudPanel, activeSection = "general", searchQuery = "" } = $props<{
showColourPicker: () => void; showColourPicker: () => void;
showFontPicker: () => void; showFontPicker: () => void;
showDisclaimer: (onConfirm: () => void, onCancel: () => void, title?: string, message?: string) => void; showDisclaimer: (onConfirm: () => void, onCancel: () => void, title?: string, message?: string) => void;
showCloudPanel: () => void; showCloudPanel: () => void;
activeSection?: string; activeSection?: string;
searchQuery?: string;
}>(); }>();
/** Map each plugin into the settings sidebar category that fits its purpose. */ /** Map each plugin into the settings sidebar category that fits its purpose. */
@@ -167,6 +168,13 @@
activeSection === "all" || activeSection === "all" ||
(pluginSectionById[pluginId] ?? "features") === activeSection; (pluginSectionById[pluginId] ?? "features") === activeSection;
const matchesSearch = (...parts: Array<string | undefined>) => {
const q = searchQuery.trim().toLowerCase();
return !q || parts.some((part) => part?.toLowerCase().includes(q));
};
const pluginHit = (plugin: Plugin) =>
matchesSearch(plugin.name, plugin.description, ...Object.values(plugin.settings).flatMap((s) => [s.title, s.description]));
async function exportCloudSettingsJsonToFile() { async function exportCloudSettingsJsonToFile() {
const payload = await getSnapshotForUpload(); const payload = await getSnapshotForUpload();
@@ -183,6 +191,7 @@
</script> </script>
{#snippet Setting({ title, description, Component, props }: SettingsList) } {#snippet Setting({ title, description, Component, props }: SettingsList) }
{#if matchesSearch(title, description)}
<div class="flex justify-between items-center px-5 py-5"> <div class="flex justify-between items-center px-5 py-5">
<div class="pr-5"> <div class="pr-5">
<h2 class="text-xl font-bold">{title}</h2> <h2 class="text-xl font-bold">{title}</h2>
@@ -192,6 +201,7 @@
<Component {...props} /> <Component {...props} />
</div> </div>
</div> </div>
{/if}
{/snippet} {/snippet}
<div class="flex flex-col divide-y divide-zinc-100 dark:divide-zinc-700"> <div class="flex flex-col divide-y divide-zinc-100 dark:divide-zinc-700">
@@ -204,6 +214,7 @@
props: {} props: {}
})} })}
{#if matchesSearch("BetterSEQTA Cloud", "Account & sync")}
<div class="border-none"> <div class="border-none">
<div class="p-1 my-1 from-white to-zinc-100 bg-gradient-to-br rounded-xl border shadow-sm border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40"> <div class="p-1 my-1 from-white to-zinc-100 bg-gradient-to-br rounded-xl border shadow-sm border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40">
<div class="flex justify-between items-center px-5 py-4"> <div class="flex justify-between items-center px-5 py-4">
@@ -223,6 +234,7 @@
</div> </div>
</div> </div>
{/if} {/if}
{/if}
{#if showsSection("general")} {#if showsSection("general")}
{#each [ {#each [
@@ -354,12 +366,13 @@
{@render Setting(option)} {@render Setting(option)}
{/each} {/each}
{#if !isEngage} {#if !isEngage && matchesSearch("Sidebar Style", "Item Size", "Corner Radius", "Active Indicator", "Sidebar Width", "Transparency Effects", "Blur Strength")}
<div class="border-none"> <div class="border-none">
<SidebarAppearance /> <SidebarAppearance />
</div> </div>
{/if} {/if}
{#if matchesSearch("Adaptive Theme Colour", "Soft Gradient", "Smooth colour transition")}
<div class="border-none"> <div class="border-none">
<div class="p-1 my-1 from-white to-zinc-100 bg-gradient-to-br rounded-xl border shadow-sm border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40"> <div class="p-1 my-1 from-white to-zinc-100 bg-gradient-to-br rounded-xl border shadow-sm border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40">
<div class="flex justify-between items-center px-5 py-4"> <div class="flex justify-between items-center px-5 py-4">
@@ -403,8 +416,10 @@
</div> </div>
</div> </div>
{/if} {/if}
{/if}
{#if showsSection("general")} {#if showsSection("general")}
{#if matchesSearch("Home Page Assessments", "Include Past Assessments", "Maximum Subjects", "Maximum Assessments per Subject")}
<div class="border-none"> <div class="border-none">
<div class="p-1 my-1 from-white to-zinc-100 bg-gradient-to-br rounded-xl border shadow-sm border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40"> <div class="p-1 my-1 from-white to-zinc-100 bg-gradient-to-br rounded-xl border shadow-sm border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40">
<div class="flex justify-between items-center px-5 py-4"> <div class="flex justify-between items-center px-5 py-4">
@@ -464,9 +479,10 @@
</div> </div>
</div> </div>
{/if} {/if}
{/if}
{#each pluginSettings as plugin (plugin.pluginId)} {#each pluginSettings as plugin (plugin.pluginId)}
{#if pluginBelongsInSection(plugin.pluginId)} {#if pluginBelongsInSection(plugin.pluginId) && pluginHit(plugin)}
<div class="border-none"> <div class="border-none">
<div class="p-1 my-1 from-white to-zinc-100 bg-gradient-to-br rounded-xl border shadow-sm border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40 {!(plugin as any).disableToggle && Object.keys(plugin.settings).length === 0 ? 'hidden' : ''}"> <div class="p-1 my-1 from-white to-zinc-100 bg-gradient-to-br rounded-xl border shadow-sm border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40 {!(plugin as any).disableToggle && Object.keys(plugin.settings).length === 0 ? 'hidden' : ''}">
<!-- Always show enable toggle if disableToggle is true --> <!-- Always show enable toggle if disableToggle is true -->
@@ -596,7 +612,18 @@
} }
})} })}
{#if $settingsState.devMode} {#if $settingsState.devMode && matchesSearch(
"Developer Mode",
"Verbose logging",
"Delay loading screen",
"Sensitive Hider",
"Mock Notices",
"Show Privacy Notification",
"Show Theme of the Month",
"Export cloud settings JSON",
"API Base URL",
"GitHub latest version override",
)}
<div class="flex-col p-1 my-1 bg-gradient-to-br from-white rounded-xl border shadow-sm to-zinc-100 border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40"> <div class="flex-col p-1 my-1 bg-gradient-to-br from-white rounded-xl border shadow-sm to-zinc-100 border-zinc-200/50 dark:border-zinc-700/40 dark:to-zinc-900/50 dark:from-zinc-900/40">
<div class="flex justify-between items-center px-5 py-4"> <div class="flex justify-between items-center px-5 py-4">
<div class="pr-4"> <div class="pr-4">
@@ -5,6 +5,9 @@
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Shortcuts from "@/seqta/content/links.json" import Shortcuts from "@/seqta/content/links.json"
let { searchQuery = "" } = $props<{ searchQuery?: string }>();
const q = $derived(searchQuery.trim().toLowerCase());
let isLoaded = $state(false); let isLoaded = $state(false);
let fileInput = $state<HTMLInputElement | null>(null); let fileInput = $state<HTMLInputElement | null>(null);
@@ -106,6 +109,7 @@
<div class="flex flex-col pt-4 divide-y divide-zinc-100 dark:divide-zinc-700"> <div class="flex flex-col pt-4 divide-y divide-zinc-100 dark:divide-zinc-700">
{#if isLoaded} {#if isLoaded}
{#if !q}
<div> <div>
<MotionDiv <MotionDiv
initial={{ opacity: 0, height: 0 }} initial={{ opacity: 0, height: 0 }}
@@ -207,9 +211,11 @@
</button> </button>
</MotionDiv> </MotionDiv>
</div> </div>
{/if}
<!-- Custom Shortcuts Section --> <!-- Custom Shortcuts Section -->
{#each ($settingsState.customshortcuts ?? []) as shortcut, index} {#each ($settingsState.customshortcuts ?? []) as shortcut, index}
{#if !q || shortcut.name.toLowerCase().includes(q)}
<div class="flex justify-between items-center px-4 py-3"> <div class="flex justify-between items-center px-4 py-3">
{shortcut.name} {shortcut.name}
<button aria-label="Delete Shortcut" onclick={() => deleteCustomShortcut(index)}> <button aria-label="Delete Shortcut" onclick={() => deleteCustomShortcut(index)}>
@@ -218,9 +224,11 @@
</svg> </svg>
</button> </button>
</div> </div>
{/if}
{/each} {/each}
{#each Object.entries(Shortcuts) as shortcut} {#each Object.entries(Shortcuts) as shortcut}
{#if !q || shortcut[1].DisplayName?.toLowerCase().includes(q) || shortcut[0].toLowerCase().includes(q)}
<div class="flex justify-between items-center px-4 py-3"> <div class="flex justify-between items-center px-4 py-3">
<div class="pr-4"> <div class="pr-4">
<!-- Use DisplayName if it exists, otherwise use the key (shortcut[0]) as a fallback --> <!-- Use DisplayName if it exists, otherwise use the key (shortcut[0]) as a fallback -->
@@ -228,6 +236,7 @@
</div> </div>
<Switch state={$settingsState.shortcuts.find(s => s.name === shortcut[0])?.enabled ?? false} onChange={() => switchChange(shortcut[0])} /> <Switch state={$settingsState.shortcuts.find(s => s.name === shortcut[0])?.enabled ?? false} onChange={() => switchChange(shortcut[0])} />
</div> </div>
{/if}
{/each} {/each}
{:else} {:else}
<div class="p-4 text-center"> <div class="p-4 text-center">
@@ -15,6 +15,7 @@ export const WHATS_NEW_CHANGELOG: WhatsNewRelease[] = [
"Removed white background around SEQTA Learn mobile App icons in settings.", "Removed white background around SEQTA Learn mobile App icons in settings.",
"Re-added the kitten back to the 404 Error pages.", "Re-added the kitten back to the 404 Error pages.",
"Improved the sidebar to be more stable and performant.", "Improved the sidebar to be more stable and performant.",
"Fixed Analytics page not respecting no animations setting. (#474)",
"Fixed dropdown contrast and readability in settings and across SEQTA pages.", "Fixed dropdown contrast and readability in settings and across SEQTA pages.",
"Fixed Analytics sidebar item not hiding when toggled off in Edit Sidebar.", "Fixed Analytics sidebar item not hiding when toggled off in Edit Sidebar.",
"Fixed timetable subject colour picker not reopening after closing (#221).", "Fixed timetable subject colour picker not reopening after closing (#221).",