feat: sidebar customisaitons and settings fixes

This commit is contained in:
2026-07-26 11:32:04 +09:30
parent 88926aaee7
commit c5ab69ba82
19 changed files with 1479 additions and 222 deletions
+53
View File
@@ -0,0 +1,53 @@
import type { Plugin, ViteDevServer } from "vite";
/**
* CRXJS + Vite 6 often corrupt content-script ESM bindings after HMR /
* `[crx] runtime reload` — modules load but named/`default` exports are missing
* until the dev server is restarted.
*
* Prefer invalidating the module graph + a full page reload over partial HMR.
*/
export default function stabilizeCrxDevHmr(): Plugin {
let reloadTimer: ReturnType<typeof setTimeout> | null = null;
const scheduleFullReload = (s: ViteDevServer) => {
if (reloadTimer) clearTimeout(reloadTimer);
// Debounce cascading invalidations (e.g. many files in one save).
reloadTimer = setTimeout(() => {
reloadTimer = null;
s.ws.send({ type: "full-reload", path: "*" });
}, 50);
};
return {
name: "stabilize-crx-dev-hmr",
apply: "serve",
enforce: "pre",
configureServer(s) {
s.ws.on("bsplus:reset-module-graph", () => {
s.moduleGraph.invalidateAll();
scheduleFullReload(s);
});
},
handleHotUpdate({ file, modules, server: viteServer }) {
if (!file.replace(/\\/g, "/").includes("/src/")) return;
if (file.includes("node_modules")) return;
const seen = new Set(modules);
const queue = [...modules];
while (queue.length) {
const mod = queue.pop()!;
viteServer.moduleGraph.invalidateModule(mod);
for (const importer of mod.importers) {
if (seen.has(importer)) continue;
seen.add(importer);
queue.push(importer);
}
}
scheduleFullReload(viteServer);
// Skip Vite's partial HMR for these modules — it is what leaves exports empty.
return [];
},
};
}
+7 -47
View File
@@ -1,55 +1,15 @@
import fs from "fs";
import type { Plugin } from "vite";
/**
* Creates a Vite plugin designed to improve the reliability of Hot Module Replacement (HMR)
* for global CSS files.
* Previously touched CSS mtimes on JS HMR to force style refresh.
* That raced with CRXJS runtime reload and corrupted Vite's module graph
* (missing named/`default` exports until `npm run dev` was restarted).
*
* When a JavaScript/TypeScript module that imports a CSS file is updated, Vite's HMR
* might not always reliably update the styles injected by that global CSS. This plugin
* attempts to mitigate this by listening for hot updates. If an updated module
* has direct importers that are CSS files (e.g., a JS file imports a global CSS file),
* this plugin will "touch" those CSS files by updating their access and modification
* timestamps using `fs.utimesSync`. This action can help signal to Vite or the browser
* that the CSS file has changed, potentially triggering a more reliable style reload.
*
* @returns {import('vite').Plugin} A Vite plugin object configured with `name` and `handleHotUpdate` hooks.
* Style updates are now covered by `stabilizeCrxDevHmr` full reloads.
*/
export default function touchGlobalCSSPlugin() {
export default function touchGlobalCSSPlugin(): Plugin {
return {
/**
* The unique name of this Vite plugin.
* This name is used by Vite for identification purposes and will appear in logs.
* @type {string}
*/
name: "touch-global-css",
/**
* A Vite hook that is called when a module is hot-updated.
* This function inspects the importers of the updated module. If any of these
* importers are CSS files, their filesystem timestamps are updated ("touched").
*
* @param {object} context The context object provided by Vite's `handleHotUpdate` hook.
* @param {Array<import('vite').ModuleNode>} context.modules An array of `ModuleNode` instances that have been updated.
* This plugin specifically accesses `modules[0]._clientModule.importers`
* to find CSS files that import the updated module.
*/
handleHotUpdate({ modules }) {
// It's assumed `modules[0]` is the primary updated module of interest.
// `_clientModule` and `importers` might be internal or less stable Vite APIs.
const importers = modules[0]?._clientModule?.importers;
if (importers) {
importers.forEach((importer) => {
// Check if the importer is a CSS file
if (importer.file && importer.file.includes(".css")) {
console.log("[touch-global-css] touching", importer.file);
try {
// Update the access and modification times of the CSS file to the current time
fs.utimesSync(importer.file, new Date(), new Date());
} catch (err) {
console.error(`[touch-global-css] Error touching file ${importer.file}:`, err);
}
}
});
}
},
apply: "serve",
};
}
+34
View File
@@ -53,6 +53,12 @@ if (document.childNodes[1]) {
init();
}
if (import.meta.env.DEV) {
window.addEventListener("unhandledrejection", (event) => {
recoverFromStaleDevModuleGraph(event.reason);
});
}
async function init() {
if (
hasSEQTAText &&
@@ -118,11 +124,16 @@ async function init() {
initializeHideSensitiveToggle();
}
if (import.meta.env.DEV) {
sessionStorage.removeItem("bsplus-dev-export-recovery");
}
verboseInfo(
"[BetterSEQTA+] Successfully initialised BetterSEQTA+, starting to load assets.",
);
} catch (error) {
console.error(error);
recoverFromStaleDevModuleGraph(error);
}
}
}
@@ -136,3 +147,26 @@ function replaceIcons() {
}
});
}
/** Vite/CRX HMR can leave modules with missing named exports until the graph is cleared. */
function recoverFromStaleDevModuleGraph(error: unknown) {
if (!import.meta.env.DEV) return;
const message = error instanceof Error ? error.message : String(error);
if (!/does not provide an export named/.test(message)) return;
const key = "bsplus-dev-export-recovery";
if (!sessionStorage.getItem(key)) {
sessionStorage.setItem(key, "1");
import.meta.hot?.send("bsplus:reset-module-graph");
setTimeout(() => {
location.reload();
}, 80);
return;
}
sessionStorage.removeItem(key);
console.error(
"[BetterSEQTA+] Dev module graph is still stale after recovery. Restart `npm run dev`, then reload this page.",
);
}
+42 -13
View File
@@ -13,6 +13,7 @@
}
@include meta.load-css("injected/sidebar-animation.scss");
@include meta.load-css("injected/sidebar-styles.scss");
@include meta.load-css("injected/theme.scss");
@include meta.load-css("injected/transparency.scss");
@@ -565,21 +566,26 @@ ul.magicDelete > li.deleting {
visibility: visible !important;
}
/* Edit rows should match normal custom-sidebar item size (not shrunk). */
#menu.bsplus-sidebar-edit-mode > #bsplus-sidebar-root > li.item.draggable,
#menu.bsplus-sidebar-edit-mode .item.draggable {
display: flex !important;
align-items: center;
gap: 0.5rem;
width: calc(100% - 12px) !important;
align-items: center !important;
width: auto !important;
max-width: none !important;
box-sizing: border-box !important;
padding: 0 !important;
margin: 0 6px 8px !important;
}
#menu.bsplus-sidebar-edit-mode .item.draggable > label:not(.toggle) {
flex: 1 1 0 !important;
width: 0 !important;
flex: 1 1 auto !important;
width: auto !important;
min-width: 0 !important;
max-width: none !important;
padding: 12px !important;
padding-left: 4px !important;
box-sizing: border-box !important;
}
#menu.bsplus-sidebar-edit-mode .item.draggable .label {
@@ -587,6 +593,14 @@ ul.magicDelete > li.deleting {
overflow: hidden !important;
text-overflow: ellipsis !important;
overflow-wrap: normal !important;
font-size: inherit !important;
line-height: 1.2 !important;
}
#menu.bsplus-sidebar-edit-mode .item.draggable > label:not(.toggle) > svg {
width: 24px !important;
height: 24px !important;
flex-shrink: 0 !important;
}
#menu.bsplus-sidebar-edit-mode .item.draggable > .toggle,
@@ -594,7 +608,7 @@ ul.magicDelete > li.deleting {
pointer-events: auto !important;
flex: 0 0 auto !important;
width: auto !important;
margin: 0 10px 0 0 !important;
margin: 0 12px 0 0 !important;
position: relative;
z-index: 2;
}
@@ -631,7 +645,7 @@ ul.magicDelete > li.deleting {
}
#menu {
width: 270px;
width: var(--bsplus-sidebar-width, 270px);
z-index: 19;
background: var(--better-main) !important;
color: var(--text-color);
@@ -907,7 +921,9 @@ body.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) {
margin-bottom: 8px !important;
width: 85% !important;
}
.item.draggable {
/* Keep width scoped to #menu — fallback drag clones append to body, and
width:100% there stretches across the viewport. */
#menu .item.draggable {
width: 100% !important;
cursor: grab;
@@ -921,6 +937,19 @@ body.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) {
}
}
/* Sortable forceFallback clone (on body) — match source row size. */
body > .bsplus-sortable-drag,
body > .sortable-fallback.bsplus-sortable-drag,
.bsplus-sortable-drag.sortable-fallback {
width: var(--bsplus-drag-width, 240px) !important;
max-width: var(--bsplus-drag-width, 240px) !important;
min-width: 0 !important;
box-sizing: border-box !important;
margin: 0 !important;
pointer-events: none !important;
list-style: none !important;
}
#menu {
li.active > .sub > ul > .item:not(.hasChildren) {
position: relative;
@@ -1340,7 +1369,7 @@ html.transparencyEffects
#content {
transition: left 0.4s cubic-bezier(0.4, 0, 0.2, 1), transform 0.4s ease;
left: 270px;
left: var(--bsplus-sidebar-width, 270px);
background: unset;
}
@@ -1368,8 +1397,8 @@ html.transparencyEffects
display: none;
}
#menu {
-webkit-transform: translatex(-270px);
transform: translatex(-270px);
-webkit-transform: translatex(calc(-1 * var(--bsplus-sidebar-width, 270px)));
transform: translatex(calc(-1 * var(--bsplus-sidebar-width, 270px)));
}
.menuShown #menu {
-webkit-transform: translatex(0);
@@ -1379,8 +1408,8 @@ html.transparencyEffects
left: 0;
}
.menuShown #content {
-webkit-transform: translatex(270px);
transform: translatex(270px);
-webkit-transform: translatex(var(--bsplus-sidebar-width, 270px));
transform: translatex(var(--bsplus-sidebar-width, 270px));
}
body.icon-only-sidebar:not(:has(#menu li.hasChildren.active)) {
+220
View File
@@ -0,0 +1,220 @@
/* Custom sidebar style presets + Look (density / radius / indicator). */
$sidebar-motion: 0.4s cubic-bezier(0.4, 0, 0.2, 1);
@mixin accent-bar($shadow: 0.55) {
content: "" !important;
position: absolute !important;
left: 0 !important;
top: 10px !important;
bottom: 10px !important;
width: 4px !important;
border-radius: 2px !important;
background: #fff !important;
box-shadow: 0 0 10px rgba(255, 255, 255, $shadow) !important;
pointer-events: none !important;
z-index: 3 !important;
opacity: 1 !important;
display: block !important;
}
#menu.bsplus-custom-sidebar {
$item: "> #bsplus-sidebar-root > li.item:not(.bsplus-sidebar-edit-header):not(.bsplus-sidebar-edit-actions)";
$label: "> #bsplus-sidebar-root > li.item > label:not(.toggle)";
$active: "> #bsplus-sidebar-root > li.item.active:not(.hasChildren)";
/* Look: base radius (pill / sharp / strip override).
Density/radius animate with the same easing as sidebar width. */
#{$item} {
border-radius: var(--bsplus-sidebar-radius, 12px) !important;
transition:
padding $sidebar-motion,
margin $sidebar-motion,
border-radius $sidebar-motion,
background-color 0.2s ease,
box-shadow 0.2s ease,
opacity 0.2s ease;
}
#{$label} {
transition:
font-size $sidebar-motion,
line-height $sidebar-motion,
padding $sidebar-motion,
margin $sidebar-motion;
> svg {
transition:
width $sidebar-motion,
height $sidebar-motion,
margin $sidebar-motion;
}
}
/* —— Style presets —— */
&.bsplus-sidebar-style-soft {
#{$item} {
margin: 0 8px 10px !important;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.12);
}
#{$active} {
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.22);
background: rgba(0, 0, 0, 0.38) !important;
}
}
&.bsplus-sidebar-style-pill {
#{$item} {
margin: 0 10px 8px !important;
border-radius: 999px !important;
}
#{$active} {
background: rgba(0, 0, 0, 0.4) !important;
}
}
&.bsplus-sidebar-style-glass {
#{$item} {
margin: 0 8px 8px !important;
background: rgba(255, 255, 255, 0.1) !important;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14);
}
#{$active} {
background: rgba(255, 255, 255, 0.2) !important;
box-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.28),
0 8px 20px rgba(0, 0, 0, 0.2);
}
}
&.bsplus-sidebar-style-sharp {
#{$item},
#{$active} {
border-radius: 4px !important;
}
#{$item} {
margin: 0 4px 4px !important;
}
#{$active} {
background: rgba(0, 0, 0, 0.42) !important;
}
}
&.bsplus-sidebar-style-strip {
#{$item} {
margin: 0 0 2px !important;
border-radius: 0 !important;
background: transparent !important;
box-shadow: none !important;
}
#{$active} {
position: relative !important;
background: rgba(255, 255, 255, 0.12) !important;
box-shadow: none !important;
&::after {
@include accent-bar(0.55);
}
}
}
&.bsplus-sidebar-style-neon #{$active} {
background: rgba(255, 255, 255, 0.1) !important;
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.55),
0 0 14px rgba(255, 255, 255, 0.4),
0 0 28px rgba(255, 255, 255, 0.22) !important;
}
&.bsplus-sidebar-style-neon #{$item} {
margin: 0 8px 8px !important;
}
/* —— Density (comfortable = no class / no overrides) —— */
&.bsplus-sidebar-density-compact {
#{$item},
#{$active} {
padding: 6px 8px !important;
min-height: 0 !important;
}
#{$item} {
margin: 0 6px 4px !important;
height: auto !important;
}
#{$label} {
padding: 0 !important;
margin: 0 !important;
font-size: 13px !important;
line-height: 1.2 !important;
min-height: 0 !important;
height: auto !important;
white-space: nowrap !important;
> svg {
width: 20px !important;
height: 20px !important;
margin: 0 8px 0 2px !important;
flex-shrink: 0 !important;
}
}
}
&.bsplus-sidebar-density-large {
#{$item} {
margin: 0 8px 10px !important;
padding: 14px 12px !important;
}
#{$label} {
padding: 0 !important;
font-size: 17px !important;
line-height: 1.25 !important;
> svg {
width: 30px !important;
height: 30px !important;
margin: 0 10px 0 4px !important;
}
}
}
/* —— Active indicator (fill = default, no class) —— */
&.bsplus-sidebar-indicator-bar:not(.bsplus-sidebar-style-strip) #{$active} {
position: relative !important;
background: rgba(255, 255, 255, 0.1) !important;
box-shadow: none !important;
&::after {
@include accent-bar(0.45);
}
}
&.bsplus-sidebar-indicator-outline #{$active} {
background: transparent !important;
box-shadow:
inset 0 0 0 2px rgba(255, 255, 255, 0.55),
0 0 0 1px rgba(255, 255, 255, 0.12) !important;
}
&.bsplus-sidebar-indicator-underline:not(.bsplus-sidebar-style-strip) #{$active} {
position: relative !important;
background: transparent !important;
box-shadow: none !important;
&::after {
content: "" !important;
position: absolute !important;
left: 14px !important;
right: 14px !important;
bottom: 6px !important;
top: auto !important;
width: auto !important;
height: 3px !important;
border-radius: 999px !important;
background: #fff !important;
box-shadow: 0 0 8px rgba(255, 255, 255, 0.4) !important;
pointer-events: none !important;
z-index: 3 !important;
opacity: 1 !important;
display: block !important;
}
}
}
+4 -1
View File
@@ -51,7 +51,10 @@ html.transparencyEffects {
backdrop-filter: blur(10px) !important;
}
#menu,
#menu {
backdrop-filter: blur(var(--bsplus-sidebar-blur, 50px));
}
.kanban-column,
.whatsnewContainer,
[class*="Message__Message___"] {
@@ -0,0 +1,577 @@
<script lang="ts">
import Select from "@/interface/components/Select.svelte";
import Slider from "@/interface/components/Slider.svelte";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import {
DEFAULT_SIDEBAR_BLUR,
DEFAULT_SIDEBAR_RADIUS,
getSidebarStyle,
normalizeSidebarBlur,
normalizeSidebarDensity,
normalizeSidebarIndicator,
normalizeSidebarRadius,
normalizeSidebarStyleId,
normalizeSidebarWidth,
SIDEBAR_STYLES,
type SidebarStyleId,
} from "@/seqta/ui/sidebar/sidebarStyles";
const PREVIEW_ITEMS = [
{ label: "Home", active: true },
{ label: "Timetable", active: false },
{ label: "Assessments", active: false },
{ label: "Messages", active: false },
{ label: "Documents", active: false },
] as const;
const selectedId = $derived(
normalizeSidebarStyleId($settingsState.sidebarStyle),
);
const selected = $derived(getSidebarStyle(selectedId));
const density = $derived(
normalizeSidebarDensity($settingsState.sidebarDensity),
);
const indicator = $derived(
normalizeSidebarIndicator($settingsState.sidebarActiveIndicator),
);
const width = $derived(normalizeSidebarWidth($settingsState.sidebarWidth));
const radius = $derived(
normalizeSidebarRadius(
$settingsState.sidebarCornerRadius ?? DEFAULT_SIDEBAR_RADIUS,
),
);
const blur = $derived(
normalizeSidebarBlur($settingsState.sidebarBlur ?? DEFAULT_SIDEBAR_BLUR),
);
const transparencyOn = $derived($settingsState.transparencyEffects === true);
function selectStyle(id: SidebarStyleId) {
if (selectedId !== id) settingsState.sidebarStyle = id;
}
</script>
<div class="card">
<header class="card-header split">
<div>
<h2 class="title">Sidebar Style</h2>
<p class="subtitle">Choose how the navigation menu looks</p>
</div>
<div class="selected-meta" aria-live="polite">
<span class="selected-label">{selected.label}</span>
<span class="selected-desc">{selected.description}</span>
</div>
</header>
<div class="picker-body">
<div class="preview-pane" aria-hidden="true">
<div class={`preview style-${selectedId}`}>
<div class="preview-chrome">
<div class="preview-logo"></div>
{#each PREVIEW_ITEMS as item (item.label)}
<div class="preview-item" class:active={item.active}>
<span class="preview-dot"></span>
<span class="preview-label">{item.label}</span>
</div>
{/each}
</div>
</div>
</div>
<div
class="options"
role="listbox"
tabindex="0"
aria-label="Sidebar styles"
aria-activedescendant={`sidebar-style-${selectedId}`}
>
{#each SIDEBAR_STYLES as style (style.id)}
{@const active = style.id === selectedId}
<button
type="button"
id={`sidebar-style-${style.id}`}
class="option"
class:active
role="option"
aria-selected={active}
onclick={() => selectStyle(style.id)}
>
<div class={`thumb style-${style.id}`}>
<div class="thumb-chrome">
{#each PREVIEW_ITEMS.slice(0, 3) as item (item.label)}
<div class="thumb-item" class:active={item.active}></div>
{/each}
</div>
</div>
<span class="option-label">{style.label}</span>
</button>
{/each}
</div>
</div>
</div>
<div class="card">
<header class="card-header">
<h2 class="title">Sidebar Look</h2>
<p class="subtitle">Density, size, and active-state details</p>
</header>
<div class="rows">
<div class="row">
<div class="copy">
<h3 class="row-title">Item Size</h3>
<p class="row-desc">Spacing and type size for menu rows</p>
</div>
<div class="control">
<Select
value={density}
onChange={(value) => (settingsState.sidebarDensity = value)}
options={[
{ value: "compact", label: "Compact" },
{ value: "comfortable", label: "Comfortable" },
{ value: "large", label: "Large" },
]}
/>
</div>
</div>
<div class="row">
<div class="copy">
<h3 class="row-title">Corner Radius</h3>
<p class="row-desc">Roundness of menu items ({radius}px)</p>
</div>
<div class="control slider">
<Slider
state={radius}
min={0}
max={24}
step={1}
onChange={(value) => (settingsState.sidebarCornerRadius = value)}
/>
</div>
</div>
<div class="row">
<div class="copy">
<h3 class="row-title">Active Indicator</h3>
<p class="row-desc">How the current page is highlighted</p>
</div>
<div class="control">
<Select
value={indicator}
onChange={(value) => (settingsState.sidebarActiveIndicator = value)}
options={[
{ value: "fill", label: "Fill" },
{ value: "bar", label: "Left bar" },
{ value: "outline", label: "Outline" },
{ value: "underline", label: "Underline" },
]}
/>
</div>
</div>
<div class="row">
<div class="copy">
<h3 class="row-title">Sidebar Width</h3>
<p class="row-desc">Overall navigation column width</p>
</div>
<div class="control">
<Select
value={width}
onChange={(value) => (settingsState.sidebarWidth = value)}
options={[
{ value: "narrow", label: "Narrow" },
{ value: "default", label: "Default" },
{ value: "wide", label: "Wide" },
]}
/>
</div>
</div>
<div class="row" class:disabled={!transparencyOn}>
<div class="copy">
<h3 class="row-title">Blur Strength</h3>
<p class="row-desc">
{#if transparencyOn}
Glass blur on the sidebar ({blur}px)
{:else}
Enable Transparency Effects to use blur
{/if}
</p>
</div>
<div class="control slider">
<Slider
state={blur}
min={0}
max={80}
step={1}
onChange={(value) => (settingsState.sidebarBlur = value)}
/>
</div>
</div>
</div>
</div>
<style>
.card {
margin: 4px 0;
padding: 4px;
border-radius: 0.75rem;
border: 1px solid rgb(228 228 231 / 0.5);
background: linear-gradient(to bottom right, white, rgb(244 244 245));
box-shadow: 0 1px 2px rgb(0 0 0 / 0.05);
}
:global(.dark) .card {
border-color: rgb(63 63 70 / 0.4);
background: linear-gradient(
to bottom right,
rgb(24 24 27 / 0.4),
rgb(24 24 27 / 0.5)
);
}
.card-header {
padding: 1rem 1.25rem 0.5rem;
}
.card-header.split {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: flex-start;
}
.title {
font-size: 1.25rem;
font-weight: 700;
line-height: 1.3;
}
.subtitle {
margin-top: 0.15rem;
font-size: 1rem;
color: rgb(82 82 91);
}
:global(.dark) .subtitle {
color: rgb(212 212 216);
}
.selected-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
text-align: right;
max-width: 14rem;
gap: 0.15rem;
}
.selected-label {
font-size: 0.95rem;
font-weight: 700;
}
.selected-desc {
font-size: 0.8rem;
line-height: 1.35;
color: rgb(113 113 122);
}
:global(.dark) .selected-desc {
color: rgb(161 161 170);
}
.picker-body {
display: grid;
grid-template-columns: minmax(160px, 210px) 1fr;
gap: 1rem;
padding: 0.5rem 1rem 1rem;
}
@media (max-width: 720px) {
.picker-body {
grid-template-columns: 1fr;
}
}
.preview-pane {
display: flex;
align-items: center;
}
.preview {
width: 100%;
max-width: 196px;
height: 280px;
border-radius: 18px;
overflow: hidden;
box-shadow: 0 14px 32px rgb(0 0 0 / 0.22);
background: linear-gradient(160deg, #aa053a, #141414 70%);
}
.preview-chrome {
display: flex;
flex-direction: column;
height: 100%;
padding: 14px 0 10px;
color: #fff;
}
.preview-logo {
width: 42%;
height: 10px;
margin: 0 16px 14px;
border-radius: 999px;
background: rgb(255 255 255 / 0.35);
}
.preview-item {
display: flex;
align-items: center;
gap: 8px;
margin: 0 8px 6px;
padding: 8px 10px;
border-radius: 12px;
}
.preview-item.active {
background: rgba(0, 0, 0, 0.35);
}
.preview.style-soft .preview-item {
margin: 0 10px 8px;
border-radius: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
}
.preview.style-pill .preview-item {
margin: 0 12px 7px;
border-radius: 999px;
}
.preview.style-glass .preview-item {
margin: 0 10px 7px;
background: rgba(255, 255, 255, 0.12);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16);
}
.preview.style-glass .preview-item.active {
background: rgba(255, 255, 255, 0.22);
}
.preview.style-sharp .preview-item {
margin: 0 6px 4px;
border-radius: 4px;
}
.preview.style-strip .preview-item {
margin: 0;
border-radius: 0;
background: transparent;
}
.preview.style-strip .preview-item.active {
position: relative;
background: rgba(255, 255, 255, 0.12);
}
.preview.style-strip .preview-item.active::after {
content: "";
position: absolute;
left: 0;
top: 6px;
bottom: 6px;
width: 3px;
border-radius: 2px;
background: #fff;
box-shadow: 0 0 8px rgba(255, 255, 255, 0.55);
}
.preview.style-neon .preview-item.active {
background: rgba(255, 255, 255, 0.1);
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.55),
0 0 12px rgba(255, 255, 255, 0.4),
0 0 22px rgba(255, 255, 255, 0.22);
}
.preview-dot {
width: 10px;
height: 10px;
border-radius: 999px;
background: rgb(255 255 255 / 0.75);
flex: 0 0 auto;
}
.preview-label {
font-size: 11px;
font-weight: 600;
white-space: nowrap;
}
.options {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
gap: 0.65rem;
align-content: start;
}
.option {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.4rem;
padding: 0.45rem;
border: 1px solid transparent;
border-radius: 0.75rem;
background: rgb(255 255 255 / 0.55);
color: inherit;
cursor: pointer;
transition:
transform 0.2s ease,
border-color 0.2s ease,
box-shadow 0.2s ease;
}
:global(.dark) .option {
background: rgb(39 39 42 / 0.55);
}
.option:hover {
transform: scale(1.02);
}
.option:active {
transform: scale(0.98);
}
.option.active {
border-color: var(--theme-primary, #f43f5e);
box-shadow: 0 0 0 1px var(--theme-primary, #f43f5e);
}
.option:focus-visible {
outline: none;
box-shadow: 0 0 0 2px var(--theme-primary, #f43f5e);
}
.thumb {
width: 100%;
aspect-ratio: 3 / 4;
border-radius: 10px;
overflow: hidden;
background: linear-gradient(160deg, #aa053a, #141414 70%);
}
.thumb-chrome {
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
height: 100%;
padding: 8px 6px;
}
.thumb-item {
height: 10px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.18);
}
.thumb-item.active {
background: rgba(0, 0, 0, 0.4);
}
.thumb.style-pill .thumb-item {
border-radius: 999px;
}
.thumb.style-sharp .thumb-item {
border-radius: 2px;
}
.thumb.style-strip .thumb-item {
border-radius: 0;
}
.thumb.style-strip .thumb-item.active {
background: rgba(255, 255, 255, 0.22);
box-shadow: inset 3px 0 0 #fff;
}
.thumb.style-glass .thumb-item {
background: rgba(255, 255, 255, 0.22);
}
.thumb.style-neon .thumb-item.active {
background: rgba(255, 255, 255, 0.2);
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.5),
0 0 8px rgba(255, 255, 255, 0.55);
}
.option-label {
font-size: 0.8rem;
font-weight: 700;
}
.rows {
display: flex;
flex-direction: column;
padding: 0.25rem 0.5rem 0.75rem;
}
.row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.85rem 0.75rem;
border-top: 1px solid rgb(244 244 245);
}
:global(.dark) .row {
border-top-color: rgb(63 63 70 / 0.5);
}
.row:first-child {
border-top: none;
}
.row.disabled {
opacity: 0.55;
}
.copy {
min-width: 0;
flex: 1 1 auto;
padding-right: 0.5rem;
}
.row-title {
font-size: 1.125rem;
font-weight: 700;
line-height: 1.3;
}
.row-desc {
margin-top: 0.1rem;
font-size: 0.95rem;
color: rgb(82 82 91);
}
:global(.dark) .row-desc {
color: rgb(212 212 216);
}
.control {
flex: 0 0 auto;
min-width: 9.5rem;
max-width: 12rem;
}
.control.slider {
min-width: 10rem;
width: 11rem;
}
</style>
+61 -29
View File
@@ -24,9 +24,6 @@
isGhReleaseUpdateCheckEnabled,
type GhReleaseUpdateInfo,
} from "@/utils/githubReleaseUpdate";
import { getAllPluginSettings } from "@/plugins";
import { isSeqtaEngageExperience } from "@/seqta/utils/isSeqtaEngage";
type PageId = "settings" | "shortcuts" | "themes";
type NavItem = {
@@ -37,6 +34,37 @@
let devModeSequence = "";
let activePage = $state<PageId>("settings");
let activeSection = $state("general");
let navTrackEl = $state<HTMLElement | null>(null);
let indicatorY = $state(0);
let indicatorH = $state(40);
let indicatorReady = $state(false);
const updateNavIndicator = () => {
if (!navTrackEl || activePage !== "settings") {
indicatorReady = false;
return;
}
const btn = navTrackEl.querySelector<HTMLElement>(
`[data-nav-section="${activeSection}"]`,
);
if (!btn) {
indicatorReady = false;
return;
}
const trackRect = navTrackEl.getBoundingClientRect();
const btnRect = btn.getBoundingClientRect();
indicatorY = btnRect.top - trackRect.top + navTrackEl.scrollTop;
indicatorH = btnRect.height;
indicatorReady = true;
};
$effect(() => {
activeSection;
activePage;
navTrackEl;
queueMicrotask(updateNavIndicator);
});
let showDisclaimerModal = $state(false);
let disclaimerCallbacks = $state<{ onConfirm: () => void; onCancel: () => void } | null>(null);
let disclaimerTitle = $state("Confirm");
@@ -51,26 +79,18 @@
{ id: "themes", title: "Themes" },
];
const pluginNavItems = getAllPluginSettings()
.filter((plugin) => !(isSeqtaEngageExperience() && plugin.pluginId === "global-search"))
.filter(
(plugin) =>
(plugin as { disableToggle?: boolean }).disableToggle ||
Object.keys(plugin.settings ?? {}).length > 0,
)
.map((plugin) => ({
id: `plugin:${plugin.pluginId}`,
label: plugin.name,
}));
const userNav: NavItem[] = [
{ id: "account", label: "My Account" },
{ id: "general", label: "General" },
{ id: "appearance", label: "Appearance" },
{ id: "home", label: "Home" },
];
const appNav: NavItem[] = [...pluginNavItems, { id: "advanced", label: "Advanced" }];
const appNav: NavItem[] = [
{ id: "timetable", label: "Timetable" },
{ id: "assessments", label: "Assessments" },
{ id: "features", label: "Features" },
{ id: "advanced", label: "Advanced" },
];
const sectionTitle = $derived.by(() => {
if (activePage === "shortcuts") return "Shortcuts";
@@ -209,11 +229,12 @@
{#snippet navButton(item: NavItem)}
<button
type="button"
data-nav-section={item.id}
onclick={() => selectSection(item.id)}
class="w-full px-3 py-2 text-left text-base rounded-lg transition-all duration-200
class="relative z-10 w-full px-3 py-2.5 text-left text-lg rounded-lg transition-colors duration-200
{activePage === 'settings' && activeSection === item.id
? 'bg-zinc-200/80 dark:bg-zinc-700/80 text-zinc-900 dark:text-white font-medium'
: 'text-zinc-600 dark:text-zinc-300 hover:bg-zinc-200/50 dark:hover:bg-zinc-700/40 hover:text-zinc-900 dark:hover:text-white'}"
? 'text-zinc-900 dark:text-white font-medium'
: 'text-zinc-600 dark:text-zinc-300 hover:text-zinc-900 dark:hover:text-white'}"
>
{item.label}
</button>
@@ -221,7 +242,7 @@
{#snippet settingsShell()}
<div
class="flex flex-col h-full min-h-0 overflow-hidden bg-white dark:bg-zinc-800 dark:text-white {standalone
class="flex flex-col h-full min-h-0 overflow-hidden bg-white dark:bg-zinc-800 dark:text-white text-[18px] {standalone
? ''
: 'rounded-xl shadow-2xl border border-zinc-200/60 dark:border-zinc-700/60'}"
>
@@ -257,7 +278,7 @@
role="tab"
aria-selected={activePage === page.id}
onclick={() => selectPage(page.id)}
class="flex-1 px-4 py-2 text-base rounded-full transition-all duration-200
class="flex-1 px-4 py-2.5 text-lg rounded-full transition-all duration-200
{activePage === page.id
? 'bg-white dark:bg-zinc-700 text-zinc-900 dark:text-white font-semibold shadow-sm'
: 'text-zinc-500 dark:text-zinc-400 hover:text-zinc-800 dark:hover:text-zinc-200'}"
@@ -334,10 +355,20 @@
: 'w-[260px] px-4 py-5'}"
aria-label="Settings categories"
>
<div class="relative flex flex-col gap-5" bind:this={navTrackEl}>
{#if activePage === "settings" && indicatorReady}
<div
class="absolute left-0 right-0 top-0 z-0 rounded-lg bg-zinc-200/90 dark:bg-zinc-700/90 pointer-events-none"
style="transform: translateY({indicatorY}px); height: {indicatorH}px; transition: {$settingsState.animations
? 'transform 0.28s cubic-bezier(0.22, 1, 0.36, 1), height 0.28s cubic-bezier(0.22, 1, 0.36, 1)'
: 'none'};"
></div>
{/if}
{#if activePage === "settings"}
<div class="flex flex-col gap-1">
<p
class="px-3 mb-1.5 text-xs font-semibold tracking-wider uppercase text-zinc-400 dark:text-zinc-500"
class="relative z-10 px-3 mb-1.5 text-sm font-semibold tracking-wider uppercase text-zinc-400 dark:text-zinc-500"
>
User Settings
</p>
@@ -347,7 +378,7 @@
</div>
<div class="flex flex-col gap-1">
<p
class="px-3 mb-1.5 text-xs font-semibold tracking-wider uppercase text-zinc-400 dark:text-zinc-500"
class="relative z-10 px-3 mb-1.5 text-sm font-semibold tracking-wider uppercase text-zinc-400 dark:text-zinc-500"
>
App Settings
</p>
@@ -358,13 +389,13 @@
{:else if activePage === "shortcuts"}
<div class="flex flex-col gap-1">
<p
class="px-3 mb-1.5 text-xs font-semibold tracking-wider uppercase text-zinc-400 dark:text-zinc-500"
class="px-3 mb-1.5 text-sm font-semibold tracking-wider uppercase text-zinc-400 dark:text-zinc-500"
>
Shortcuts
</p>
<button
type="button"
class="w-full px-3 py-2 text-left text-base rounded-lg font-medium bg-zinc-200/80 dark:bg-zinc-700/80 text-zinc-900 dark:text-white"
class="w-full px-3 py-2.5 text-left text-lg rounded-lg font-medium bg-zinc-200/80 dark:bg-zinc-700/80 text-zinc-900 dark:text-white"
>
Shortcuts
</button>
@@ -372,23 +403,24 @@
{:else}
<div class="flex flex-col gap-1">
<p
class="px-3 mb-1.5 text-xs font-semibold tracking-wider uppercase text-zinc-400 dark:text-zinc-500"
class="px-3 mb-1.5 text-sm font-semibold tracking-wider uppercase text-zinc-400 dark:text-zinc-500"
>
Themes
</p>
<button
type="button"
class="w-full px-3 py-2 text-left text-base rounded-lg font-medium bg-zinc-200/80 dark:bg-zinc-700/80 text-zinc-900 dark:text-white"
class="w-full px-3 py-2.5 text-left text-lg rounded-lg font-medium bg-zinc-200/80 dark:bg-zinc-700/80 text-zinc-900 dark:text-white"
>
Themes
</button>
</div>
{/if}
</div>
</nav>
<div class="flex flex-col flex-1 min-w-0 min-h-0">
<div class="shrink-0 px-6 pt-5 pb-3">
<h1 class="text-2xl font-semibold tracking-tight text-zinc-900 dark:text-white">
<h1 class="text-3xl font-semibold tracking-tight text-zinc-900 dark:text-white">
{sectionTitle}
</h1>
</div>
+69 -48
View File
@@ -10,6 +10,7 @@
import type { SettingsList } from "@/interface/types/SettingsProps"
import { settingsState } from "@/seqta/utils/listeners/SettingsState.ts"
import PickerSwatch from "@/interface/components/PickerSwatch.svelte"
import SidebarAppearance from "@/interface/components/SidebarAppearance.svelte"
import ConnectMobileApp from "@/interface/components/ConnectMobileApp.svelte"
import CloudSettingsSync from "@/interface/components/CloudSettingsSync.svelte"
import CloudHeader from "@/interface/components/store/CloudHeader.svelte"
@@ -143,11 +144,25 @@
activeSection?: string;
}>();
const activePluginId = $derived(
activeSection.startsWith("plugin:")
? activeSection.slice("plugin:".length)
: null,
);
/** Map each plugin into the settings sidebar category that fits its purpose. */
const pluginSectionById: Record<string, string> = {
"profile-picture": "account",
"animated-background": "appearance",
"background-music": "appearance",
timetable: "timetable",
timetableEdit: "timetable",
"assessments-overview": "assessments",
"assessments-average": "assessments",
"grade-analytics": "assessments",
"global-search": "features",
"enhanced-navigation": "features",
messageFolders: "features",
notificationCollector: "features",
};
const pluginBelongsInSection = (pluginId: string) =>
(pluginSectionById[pluginId] ?? "features") === activeSection;
async function exportCloudSettingsJsonToFile() {
const payload = await getSnapshotForUpload();
@@ -164,10 +179,10 @@
</script>
{#snippet Setting({ title, description, Component, props }: SettingsList) }
<div class="flex justify-between items-center px-5 py-4">
<div class="flex justify-between items-center px-5 py-5">
<div class="pr-5">
<h2 class="text-base font-bold">{title}</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">{description}</p>
<h2 class="text-xl font-bold">{title}</h2>
<p class="text-base text-zinc-600 dark:text-zinc-300">{description}</p>
</div>
<div class="shrink-0">
<Component {...props} />
@@ -189,8 +204,8 @@
<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="pr-4">
<h2 class="text-base font-bold">BetterSEQTA Cloud</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">Account & sync</p>
<h2 class="text-xl font-bold">BetterSEQTA Cloud</h2>
<p class="text-base text-zinc-600 dark:text-zinc-300">Account & sync</p>
</div>
<div>
<CloudHeader alwaysShowUserName onClick={showCloudPanel} />
@@ -345,12 +360,18 @@
{@render Setting(option)}
{/each}
{#if !isEngage}
<div class="border-none">
<SidebarAppearance />
</div>
{/if}
<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="flex justify-between items-center px-5 py-4">
<div class="pr-4">
<h2 class="text-base font-bold">Adaptive Theme Colour</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">Change the theme colour based on the current class (e.g. when viewing a course or assessments page)</p>
<h2 class="text-xl font-bold">Adaptive Theme Colour</h2>
<p class="text-base text-zinc-600 dark:text-zinc-300">Change the theme colour based on the current class (e.g. when viewing a course or assessments page)</p>
</div>
<div>
<Switch
@@ -362,8 +383,8 @@
{#if $settingsState.adaptiveThemeColour}
<div class="flex justify-between items-center px-5 py-4 pl-7 border-t border-zinc-100 dark:border-zinc-700/50">
<div class="pr-4">
<h2 class="text-base font-bold">Soft Gradient</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">Use a soft gradient instead of a solid colour when viewing a class</p>
<h2 class="text-xl font-bold">Soft Gradient</h2>
<p class="text-base text-zinc-600 dark:text-zinc-300">Use a soft gradient instead of a solid colour when viewing a class</p>
</div>
<div>
<Switch
@@ -374,8 +395,8 @@
</div>
<div class="flex justify-between items-center px-5 py-4 pl-7 border-t border-zinc-100 dark:border-zinc-700/50">
<div class="pr-4">
<h2 class="text-base font-bold">Smooth colour transition</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">Ease between class/subject colours when navigating instead of switching instantly</p>
<h2 class="text-xl font-bold">Smooth colour transition</h2>
<p class="text-base text-zinc-600 dark:text-zinc-300">Ease between class/subject colours when navigating instead of switching instantly</p>
</div>
<div>
<Switch
@@ -389,19 +410,19 @@
</div>
{/if}
{#if activeSection === "home"}
{#if activeSection === "general"}
<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="flex justify-between items-center px-5 py-4">
<div class="pr-4">
<h2 class="text-base font-bold">Home Page Assessments</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">Limit upcoming assessments shown on the home page by subject</p>
<h2 class="text-xl font-bold">Home Page Assessments</h2>
<p class="text-base text-zinc-600 dark:text-zinc-300">Limit upcoming assessments shown on the home page by subject</p>
</div>
</div>
<div class="flex justify-between items-center px-5 py-4 pl-7 border-t border-zinc-100 dark:border-zinc-700/50">
<div class="pr-4">
<h2 class="text-base font-bold">Include Past Assessments</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">Show past-due assessments from the upcoming list, matching the Assessments page</p>
<h2 class="text-xl font-bold">Include Past Assessments</h2>
<p class="text-base text-zinc-600 dark:text-zinc-300">Show past-due assessments from the upcoming list, matching the Assessments page</p>
</div>
<div>
<Switch
@@ -412,8 +433,8 @@
</div>
<div class="flex justify-between items-center px-5 py-4 pl-7 border-t border-zinc-100 dark:border-zinc-700/50">
<div class="pr-4">
<h2 class="text-base font-bold">Maximum Subjects</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">Number of subjects to include, ordered by soonest due date</p>
<h2 class="text-xl font-bold">Maximum Subjects</h2>
<p class="text-base text-zinc-600 dark:text-zinc-300">Number of subjects to include, ordered by soonest due date</p>
</div>
<Select
value={String($settingsState.homeUpcomingSubjectsMax ?? 5)}
@@ -430,8 +451,8 @@
</div>
<div class="flex justify-between items-center px-5 py-4 pl-7 border-t border-zinc-100 dark:border-zinc-700/50">
<div class="pr-4">
<h2 class="text-base font-bold">Maximum Assessments per Subject</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">Assessments shown for each included subject</p>
<h2 class="text-xl font-bold">Maximum Assessments per Subject</h2>
<p class="text-base text-zinc-600 dark:text-zinc-300">Assessments shown for each included subject</p>
</div>
<Select
value={String($settingsState.homeUpcomingAssessmentsPerSubjectMax ?? 0)}
@@ -451,14 +472,14 @@
{/if}
{#each pluginSettings as plugin (plugin.pluginId)}
{#if activePluginId === plugin.pluginId}
{#if pluginBelongsInSection(plugin.pluginId)}
<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' : ''}">
<!-- Always show enable toggle if disableToggle is true -->
{#if (plugin as any).disableToggle}
<div class="flex justify-between items-center px-5 py-4">
<div class="pr-4">
<h2 class="flex gap-2 items-center text-base font-bold">
<h2 class="flex gap-2 items-center text-xl font-bold">
Enable {plugin.name}
{#if plugin.beta}
<span class="px-2 py-0.5 text-xs font-medium text-orange-800 bg-orange-100 rounded-full border border-orange-300/30 dark:bg-orange-900/30 dark:text-orange-300 dark:border-orange-900/30">
@@ -466,7 +487,7 @@
</span>
{/if}
</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">{plugin.description}</p>
<p class="text-base text-zinc-600 dark:text-zinc-300">{plugin.description}</p>
</div>
<div>
<Switch
@@ -496,8 +517,8 @@
{#if key !== 'enabled' && !(key === 'useCloudPfp' && !cloudState.isLoggedIn)}
<div class="flex justify-between items-center px-5 py-4">
<div class="pr-4">
<h2 class="text-base font-bold">{setting.title || key}</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">{setting.description || ''}</p>
<h2 class="text-xl font-bold">{setting.title || key}</h2>
<p class="text-base text-zinc-600 dark:text-zinc-300">{setting.description || ''}</p>
</div>
<div>
{#if setting.type === 'boolean'}
@@ -585,8 +606,8 @@
<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="pr-4">
<h2 class="text-base font-bold">Developer Mode</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">Enables developer mode, allowing you to test new features and changes.</p>
<h2 class="text-xl font-bold">Developer Mode</h2>
<p class="text-base text-zinc-600 dark:text-zinc-300">Enables developer mode, allowing you to test new features and changes.</p>
</div>
<div>
<Switch state={$settingsState.devMode} onChange={(isOn: boolean) => settingsState.devMode = isOn} />
@@ -594,8 +615,8 @@
</div>
<div class="flex justify-between items-center px-5 py-4">
<div class="pr-4">
<h2 class="text-base font-bold">Verbose logging</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">Show diagnostic console output (indexer, theme manager, timetable colour patch, etc.)</p>
<h2 class="text-xl font-bold">Verbose logging</h2>
<p class="text-base text-zinc-600 dark:text-zinc-300">Show diagnostic console output (indexer, theme manager, timetable colour patch, etc.)</p>
</div>
<div>
<Switch
@@ -606,8 +627,8 @@
</div>
<div class="flex justify-between items-center px-5 py-4">
<div class="pr-4">
<h2 class="text-base font-bold">Sensitive Hider</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">Replace sensitive content with mock data</p>
<h2 class="text-xl font-bold">Sensitive Hider</h2>
<p class="text-base text-zinc-600 dark:text-zinc-300">Replace sensitive content with mock data</p>
</div>
<div>
<Switch
@@ -618,8 +639,8 @@
</div>
<div class="flex justify-between items-center px-5 py-4">
<div class="pr-4">
<h2 class="text-base font-bold">Mock Notices</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">Use fake notice data on homepage instead of real data</p>
<h2 class="text-xl font-bold">Mock Notices</h2>
<p class="text-base text-zinc-600 dark:text-zinc-300">Use fake notice data on homepage instead of real data</p>
</div>
<div>
<Switch
@@ -630,8 +651,8 @@
</div>
<div class="flex justify-between items-center px-5 py-4">
<div class="pr-4">
<h2 class="text-base font-bold">Show Privacy Notification</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">Show the privacy notification popup on next page load</p>
<h2 class="text-xl font-bold">Show Privacy Notification</h2>
<p class="text-base text-zinc-600 dark:text-zinc-300">Show the privacy notification popup on next page load</p>
</div>
<div>
<Button
@@ -649,8 +670,8 @@
</div>
<div class="flex justify-between items-center px-5 py-4">
<div class="pr-4">
<h2 class="text-base font-bold">Show Theme of the Month</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">Fetch and show the current month's popup now (ignores dismissed state)</p>
<h2 class="text-xl font-bold">Show Theme of the Month</h2>
<p class="text-base text-zinc-600 dark:text-zinc-300">Fetch and show the current month's popup now (ignores dismissed state)</p>
</div>
<div>
<Button
@@ -665,8 +686,8 @@
</div>
<div class="flex justify-between items-center px-5 py-4">
<div class="pr-4">
<h2 class="text-base font-bold">Export cloud settings JSON</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">Download the same payload as cloud sync (OAuth tokens stripped). For debugging and server testing.</p>
<h2 class="text-xl font-bold">Export cloud settings JSON</h2>
<p class="text-base text-zinc-600 dark:text-zinc-300">Download the same payload as cloud sync (OAuth tokens stripped). For debugging and server testing.</p>
</div>
<div>
<Button onClick={exportCloudSettingsJsonToFile} text="Export to file" />
@@ -675,8 +696,8 @@
<div class="flex flex-col gap-2 px-4 py-3">
<div class="flex justify-between items-start gap-3">
<div class="pr-4">
<h2 class="text-base font-bold">API Base URL (session only)</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">Override the content API host for this browser session. Cleared on restart. Affects themes, theme of the month, and other server-driven content.</p>
<h2 class="text-xl font-bold">API Base URL (session only)</h2>
<p class="text-base text-zinc-600 dark:text-zinc-300">Override the content API host for this browser session. Cleared on restart. Affects themes, theme of the month, and other server-driven content.</p>
{#if devApiBaseActive}
<p class="text-xs mt-1 text-amber-600 dark:text-amber-400">
Override active: <span class="font-mono">{devApiBaseActive}</span>
@@ -699,8 +720,8 @@
</div>
<div class="flex flex-col gap-2 px-4 py-3">
<div>
<h2 class="text-base font-bold">GitHub latest version override</h2>
<p class="text-sm text-zinc-600 dark:text-zinc-300">Pretend a newer GitHub release exists to test the update badge. Only applies when dev mode is on.</p>
<h2 class="text-xl font-bold">GitHub latest version override</h2>
<p class="text-base text-zinc-600 dark:text-zinc-300">Pretend a newer GitHub release exists to test the update badge. Only applies when dev mode is on.</p>
</div>
<input
type="text"
@@ -196,7 +196,7 @@
transition={{ duration: 0.3 }}
>
<button
class="w-full px-4 py-2 mb-4 text-[13px] dark:text-white transition rounded-xl bg-zinc-200 dark:bg-zinc-700/50"
class="w-full px-4 py-3 mb-4 text-base dark:text-white transition rounded-xl bg-zinc-200 dark:bg-zinc-700/50"
onclick={isFormVisible ? addNewCustomShortcut : toggleForm}
>
{#if isFormVisible}
@@ -224,7 +224,7 @@
<div class="flex justify-between items-center px-4 py-3">
<div class="pr-4">
<!-- Use DisplayName if it exists, otherwise use the key (shortcut[0]) as a fallback -->
<h2 class="text-sm">{shortcut[1].DisplayName || shortcut[0]}</h2>
<h2 class="text-lg font-medium">{shortcut[1].DisplayName || shortcut[0]}</h2>
</div>
<Switch state={$settingsState.shortcuts.find(s => s.name === shortcut[0])?.enabled ?? false} onChange={() => switchChange(shortcut[0])} />
</div>
+1 -1
View File
@@ -15,7 +15,7 @@
{#if !standalone.standalone}
<button
onclick={() => selectNoBackground()}
class="w-full px-4 py-2 mb-4 text-[13px] dark:text-white transition rounded-xl bg-zinc-200 dark:bg-zinc-700/50">
class="w-full px-4 py-3 mb-4 text-base dark:text-white transition rounded-xl bg-zinc-200 dark:bg-zinc-700/50">
{ clearTheme ? 'Clear Theme' : 'Select a Theme' }
</button>
<div class="relative w-full">
+116 -16
View File
@@ -1,4 +1,6 @@
<script lang="ts">
import { onDestroy } from "svelte";
import Sortable from "sortablejs";
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
import {
restoreCustomMenuActive,
@@ -16,8 +18,10 @@
let { menuEl }: Props = $props();
let dragKey = $state<string | null>(null);
let listEl = $state<HTMLElement | null>(null);
let coverEl: HTMLElement | null = null;
let sortable: Sortable | null = null;
let dragging = $state(false);
const BACK_SVG = `<svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true"><g style="fill: currentcolor;"><path d="M15.422 16.078l-1.406 1.406-6-6 6-6 1.406 1.406-4.594 4.594z"></path></g></svg>`;
@@ -50,17 +54,80 @@
sidebarState.setItemVisibility(key, visible);
}
function onDragStart(key: string) {
dragKey = key;
function destroySortable() {
sortable?.destroy();
sortable = null;
dragging = false;
document.documentElement.style.removeProperty("--bsplus-drag-width");
}
function onDrop(key: string) {
if (dragKey && sidebarState.editMode) {
sidebarState.reorderRoot(dragKey, key);
function syncSortableFromState() {
if (!sortable || dragging) return;
const order = sidebarState.editRootItems.map((item) => item.key);
sortable.sort(order, true);
}
dragKey = null;
function restoreDefault() {
sidebarState.restoreDefaultOrder();
queueMicrotask(syncSortableFromState);
}
$effect(() => {
const editing = sidebarState.editMode;
const list = listEl;
destroySortable();
if (!editing || !list) return;
sortable = Sortable.create(list, {
animation: 220,
easing: "cubic-bezier(0.22, 1, 0.36, 1)",
draggable: ".bsplus-sidebar-item.draggable",
filter: ".toggle, .toggle input, .bsplus-sidebar-edit-header, .bsplus-sidebar-edit-actions",
preventOnFilter: false,
dataIdAttr: "data-key",
ghostClass: "bsplus-sortable-ghost",
chosenClass: "bsplus-sortable-chosen",
dragClass: "bsplus-sortable-drag",
forceFallback: true,
fallbackOnBody: true,
fallbackTolerance: 3,
swapThreshold: 0.65,
direction: "vertical",
onStart: (evt) => {
dragging = true;
const width = evt.item.getBoundingClientRect().width;
document.documentElement.style.setProperty(
"--bsplus-drag-width",
`${Math.round(width)}px`,
);
// Fallback clone is created just after onStart; pin size + drop
// `.active` so theme/active rules don't expand it to full width.
requestAnimationFrame(() => {
const dragEl = document.querySelector(
".bsplus-sortable-drag",
) as HTMLElement | null;
if (!dragEl) return;
dragEl.style.width = `${Math.round(width)}px`;
dragEl.style.maxWidth = `${Math.round(width)}px`;
dragEl.classList.remove("active");
});
},
onEnd: (evt) => {
dragging = false;
document.documentElement.style.removeProperty("--bsplus-drag-width");
if (evt.from !== evt.to) return;
if (evt.oldIndex == null || evt.newIndex == null) return;
if (evt.oldIndex === evt.newIndex) return;
sidebarState.applyMenuOrder(sortable?.toArray() ?? []);
},
});
return () => {
destroySortable();
};
});
$effect(() => {
const editing = sidebarState.editMode;
const container = document.getElementById("container");
@@ -111,6 +178,12 @@
root.scrollTop = 0;
});
});
onDestroy(() => {
destroySortable();
coverEl?.remove();
coverEl = null;
});
</script>
<!--
@@ -124,32 +197,28 @@
class:drilling={sidebarState.isDrilling}
class:compact={sidebarState.compact}
class:edit-mode={sidebarState.editMode}
class:is-sorting={dragging}
aria-label="Main"
bind:this={listEl}
>
{#if sidebarState.editMode}
<li class="item bsplus-sidebar-edit-header" aria-hidden="true">
<!-- svelte-ignore a11y_label_has_associated_control -->
<label><span class="label">Edit Sidebar</span></label>
</li>
{#each sidebarState.items as item (item.key)}
{#each sidebarState.editRootItems as item (item.key)}
<SidebarItem
{item}
active={sidebarState.activeKey === item.key}
compact={sidebarState.compact}
compact={false}
editMode={true}
visible={itemVisible(item.key)}
{onActivate}
{onToggleVisible}
{onDragStart}
{onDrop}
/>
{/each}
<li class="item bsplus-sidebar-edit-actions">
<button
type="button"
class="edit-btn"
onclick={() => sidebarState.restoreDefaultOrder()}
>
<button type="button" class="edit-btn" onclick={restoreDefault}>
Restore Default
</button>
<button type="button" class="edit-btn primary" onclick={closeEdit}>
@@ -306,6 +375,11 @@
align-items: center;
}
.bsplus-sidebar-list.is-sorting {
cursor: grabbing;
user-select: none;
}
.bsplus-sidebar-edit-header {
pointer-events: none;
width: 85%;
@@ -361,4 +435,30 @@
.edit-btn.primary {
background: rgba(0, 0, 0, 0.4);
}
/* SortableJS classes (applied to our items; must be :global). */
.bsplus-sidebar-list :global(.bsplus-sortable-ghost) {
opacity: 0.35 !important;
background: rgba(255, 255, 255, 0.08) !important;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18);
}
.bsplus-sidebar-list :global(.bsplus-sortable-chosen) {
background: rgba(0, 0, 0, 0.28) !important;
}
.bsplus-sidebar-list :global(.bsplus-sortable-drag),
:global(.bsplus-sortable-drag) {
opacity: 1 !important;
cursor: grabbing !important;
z-index: 10000 !important;
width: var(--bsplus-drag-width, 240px) !important;
max-width: var(--bsplus-drag-width, 240px) !important;
box-sizing: border-box !important;
box-shadow:
0 14px 32px rgba(0, 0, 0, 0.35),
0 0 0 1px rgba(255, 255, 255, 0.12) !important;
transform: scale(1.03);
background: rgba(0, 0, 0, 0.45) !important;
}
</style>
+31 -17
View File
@@ -12,8 +12,6 @@
drillEnter?: boolean;
onActivate: (item: SidebarItem) => void;
onToggleVisible?: (key: string, visible: boolean) => void;
onDragStart?: (key: string) => void;
onDrop?: (key: string) => void;
};
let {
@@ -25,11 +23,10 @@
drillEnter = false,
onActivate,
onToggleVisible,
onDragStart,
onDrop,
}: Props = $props();
const CHEVRON_SVG = `<svg width="24" height="24" viewBox="0 0 24 24" aria-hidden="true"><g style="fill: currentcolor;"><path d="M8.578 16.359l4.594-4.594-4.594-4.594 1.406-1.406 6 6-6 6z"></path></g></svg>`;
const GRIP_SVG = `<svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><circle cx="5" cy="3" r="1.35" fill="currentColor"/><circle cx="11" cy="3" r="1.35" fill="currentColor"/><circle cx="5" cy="8" r="1.35" fill="currentColor"/><circle cx="11" cy="8" r="1.35" fill="currentColor"/><circle cx="5" cy="13" r="1.35" fill="currentColor"/><circle cx="11" cy="13" r="1.35" fill="currentColor"/></svg>`;
</script>
<!-- SEQTA class names (item / hasChildren / active) so theme CSS keeps matching. -->
@@ -51,11 +48,7 @@
tabindex={editMode ? -1 : 0}
aria-label={item.label}
aria-current={active ? "page" : undefined}
draggable={editMode}
in:fly={{ x: drillEnter ? 24 : 0, duration: drillEnter ? 180 : 0 }}
ondragstart={() => onDragStart?.(item.key)}
ondragover={(e) => e.preventDefault()}
ondrop={() => onDrop?.(item.key)}
onclick={(e) => {
// Keep SEQTA's #menu handlers from seeing custom-list clicks — that fights
// our drill UI and can freeze the tab (Goals / Folios / etc.).
@@ -72,6 +65,9 @@
}
}}
>
{#if editMode}
<span class="drag-grip" aria-hidden="true">{@html GRIP_SVG}</span>
{/if}
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>
{#if item.iconHtml}
@@ -118,7 +114,7 @@
box-sizing: border-box;
transition:
background-color 0.2s ease,
transform 0.2s ease,
box-shadow 0.2s ease,
opacity 0.2s ease;
user-select: none;
}
@@ -165,18 +161,36 @@
.bsplus-sidebar-item.edit-mode {
cursor: grab;
width: 100%;
max-width: 100%;
gap: 4px;
padding-right: 4px;
box-sizing: border-box;
gap: 0;
}
.bsplus-sidebar-item.edit-mode:active {
cursor: grabbing;
}
/* Keep the same label padding/size as normal items; only reserve toggle space. */
.bsplus-sidebar-item.edit-mode > label:not(.toggle) {
flex: 1 1 0;
width: 0;
flex: 1 1 auto;
width: auto;
min-width: 0;
padding-right: 4px;
padding: 12px;
padding-left: 4px;
}
.drag-grip {
display: inline-flex;
align-items: center;
justify-content: center;
flex: 0 0 auto;
width: 20px;
margin-left: 10px;
opacity: 0.45;
color: inherit;
pointer-events: none;
}
.bsplus-sidebar-item.edit-mode:hover .drag-grip {
opacity: 0.8;
}
.bsplus-sidebar-item :global(label > svg) {
+27 -1
View File
@@ -9,6 +9,11 @@ import {
clearNativeDrillActive,
sidebarState,
} from "./sidebarState.svelte";
import {
applySidebarLook,
applySidebarStyleClass,
clearSidebarAppearance,
} from "./sidebarStyles";
const ROOT_ID = "bsplus-sidebar-root";
const MENU_CLASS = "bsplus-custom-sidebar";
@@ -166,6 +171,8 @@ export function prepareCustomSidebarEarly() {
earlyPrepareStarted = true;
document.documentElement.classList.add(PENDING_CLASS);
// Width/blur vars before mount so layout doesn't jump.
applySidebarLook(document.getElementById("menu"));
void mountCustomSidebar();
}
@@ -183,6 +190,8 @@ export async function mountCustomSidebar(): Promise<boolean> {
// Already mounted — re-sync after Home/News/Analytics injections.
if (app && menuEl) {
ensureDefaultMenuOrder(menuEl);
applySidebarStyleClass(menuEl, settingsState.sidebarStyle);
applySidebarLook(menuEl);
sidebarState.syncSettings();
syncFromMenu();
startCatchupSync();
@@ -214,6 +223,8 @@ export async function mountCustomSidebar(): Promise<boolean> {
}
menu.classList.add(MENU_CLASS);
applySidebarStyleClass(menu, settingsState.sidebarStyle);
applySidebarLook(menu);
document.getElementById(ROOT_ID)?.remove();
app = mount(Sidebar, {
@@ -265,6 +276,18 @@ export async function mountCustomSidebar(): Promise<boolean> {
clearSettingListeners();
registerSetting("iconOnlySidebar", () => sidebarState.syncSettings());
registerSetting("sidebarStyle", () =>
applySidebarStyleClass(menuEl, settingsState.sidebarStyle),
);
for (const key of [
"sidebarDensity",
"sidebarCornerRadius",
"sidebarActiveIndicator",
"sidebarWidth",
"sidebarBlur",
] as const) {
registerSetting(key, () => applySidebarLook(menuEl));
}
const resync = () => syncFromMenu();
registerSetting("menuorder", resync);
registerSetting("menuitems", resync);
@@ -305,7 +328,10 @@ export function unmountCustomSidebar() {
}
document.getElementById(ROOT_ID)?.remove();
menuEl?.classList.remove(MENU_CLASS, "bsplus-sidebar-edit-mode");
if (menuEl) {
menuEl.classList.remove(MENU_CLASS, "bsplus-sidebar-edit-mode");
clearSidebarAppearance(menuEl);
}
menuEl = null;
sidebarState.resetDrill();
sidebarState.setEditMode(false);
+12 -2
View File
@@ -191,6 +191,11 @@ class SidebarState {
filterVisible(orderItems(this.items, settingsState.menuorder ?? [])),
);
/** Full ordered root list for edit mode (includes hidden items). */
editRootItems = $derived(
orderItems(this.items, settingsState.menuorder ?? []),
);
isDrilling = $derived(this.drillStack.length > 0);
compact = $derived(this.iconOnly && !this.isDrilling && !this.editMode);
@@ -305,9 +310,14 @@ class SidebarState {
if (enabled) this.resetDrill();
}
applyMenuOrder(keys: string[]) {
if (!keys.length) return;
settingsState.menuorder = [...keys];
}
reorderRoot(fromKey: string, toKey: string) {
if (fromKey === toKey) return;
const keys = this.visibleRootItems.map((item) => item.key);
const keys = this.editRootItems.map((item) => item.key);
const from = keys.indexOf(fromKey);
const to = keys.indexOf(toKey);
if (from < 0 || to < 0) return;
@@ -315,7 +325,7 @@ class SidebarState {
const next = [...keys];
const [moved] = next.splice(from, 1);
next.splice(to, 0, moved);
settingsState.menuorder = next;
this.applyMenuOrder(next);
}
setItemVisibility(key: string, visible: boolean) {
+158
View File
@@ -0,0 +1,158 @@
import { settingsState } from "@/seqta/utils/listeners/SettingsState";
export type SidebarStyleId =
| "classic"
| "soft"
| "pill"
| "glass"
| "sharp"
| "strip"
| "neon";
export type SidebarDensity = "compact" | "comfortable" | "large";
export type SidebarActiveIndicator = "fill" | "bar" | "outline" | "underline";
export type SidebarWidth = "narrow" | "default" | "wide";
export type SidebarStyleDef = {
id: SidebarStyleId;
label: string;
description: string;
};
export const SIDEBAR_STYLES: readonly SidebarStyleDef[] = [
{ id: "classic", label: "Classic", description: "Default BetterSEQTA look" },
{ id: "soft", label: "Soft", description: "Roomier spacing with gentle shadows" },
{ id: "pill", label: "Pill", description: "Fully rounded capsule items" },
{ id: "glass", label: "Glass", description: "Frosted translucent rows" },
{ id: "sharp", label: "Sharp", description: "Compact, squared edges" },
{ id: "strip", label: "Strip", description: "Flat rows with a strong active accent" },
{ id: "neon", label: "Neon", description: "Glowing active highlight" },
] as const;
const STYLE_IDS = SIDEBAR_STYLES.map((s) => s.id);
/** Defaults match the current custom sidebar (no visual change). */
export const DEFAULT_SIDEBAR_STYLE: SidebarStyleId = "classic";
export const DEFAULT_SIDEBAR_DENSITY: SidebarDensity = "comfortable";
export const DEFAULT_SIDEBAR_INDICATOR: SidebarActiveIndicator = "fill";
export const DEFAULT_SIDEBAR_WIDTH: SidebarWidth = "default";
export const DEFAULT_SIDEBAR_RADIUS = 12;
export const DEFAULT_SIDEBAR_BLUR = 50;
export const SIDEBAR_WIDTH_PX = {
narrow: 220,
default: 270,
wide: 320,
} as const;
export const STYLE_CLASS_PREFIX = "bsplus-sidebar-style-";
export const DENSITY_CLASS_PREFIX = "bsplus-sidebar-density-";
export const INDICATOR_CLASS_PREFIX = "bsplus-sidebar-indicator-";
function oneOf<T extends string>(value: unknown, allowed: readonly T[], fallback: T): T {
return typeof value === "string" && (allowed as readonly string[]).includes(value)
? (value as T)
: fallback;
}
function clampInt(value: unknown, min: number, max: number, fallback: number): number {
const n = typeof value === "number" ? value : Number(value);
if (!Number.isFinite(n)) return fallback;
return Math.min(max, Math.max(min, Math.round(n)));
}
export const normalizeSidebarStyleId = (v: unknown) =>
oneOf(v, STYLE_IDS, DEFAULT_SIDEBAR_STYLE);
export const normalizeSidebarDensity = (v: unknown) =>
oneOf(v, ["compact", "comfortable", "large"] as const, DEFAULT_SIDEBAR_DENSITY);
export const normalizeSidebarIndicator = (v: unknown) =>
oneOf(
v,
["fill", "bar", "outline", "underline"] as const,
DEFAULT_SIDEBAR_INDICATOR,
);
export const normalizeSidebarWidth = (v: unknown) =>
oneOf(v, ["narrow", "default", "wide"] as const, DEFAULT_SIDEBAR_WIDTH);
export const normalizeSidebarRadius = (v: unknown) =>
clampInt(v, 0, 24, DEFAULT_SIDEBAR_RADIUS);
export const normalizeSidebarBlur = (v: unknown) =>
clampInt(v, 0, 80, DEFAULT_SIDEBAR_BLUR);
export function getSidebarStyle(id: unknown): SidebarStyleDef {
const normalized = normalizeSidebarStyleId(id);
return SIDEBAR_STYLES.find((s) => s.id === normalized) ?? SIDEBAR_STYLES[0];
}
function clearPrefixed(el: HTMLElement, prefix: string) {
for (const cls of [...el.classList]) {
if (cls.startsWith(prefix)) el.classList.remove(cls);
}
}
function setExclusiveClass(
el: HTMLElement,
prefix: string,
value: string,
skipDefault?: string,
) {
clearPrefixed(el, prefix);
if (value !== skipDefault) el.classList.add(`${prefix}${value}`);
}
/** Apply style preset class on `#menu` (classic = no class). */
export function applySidebarStyleClass(
menu: HTMLElement | null | undefined,
styleId: unknown,
) {
if (!menu) return;
setExclusiveClass(
menu,
STYLE_CLASS_PREFIX,
normalizeSidebarStyleId(styleId),
DEFAULT_SIDEBAR_STYLE,
);
}
/**
* Apply Look CSS variables + non-default density/indicator classes.
* Safe to call with no menu (still sets width/blur/radius on `:root`).
*/
export function applySidebarLook(
menu: HTMLElement | null | undefined = document.getElementById("menu"),
) {
const density = normalizeSidebarDensity(settingsState.sidebarDensity);
const indicator = normalizeSidebarIndicator(settingsState.sidebarActiveIndicator);
const width = normalizeSidebarWidth(settingsState.sidebarWidth);
const radius = normalizeSidebarRadius(settingsState.sidebarCornerRadius);
const blur = normalizeSidebarBlur(settingsState.sidebarBlur);
const root = document.documentElement;
root.style.setProperty("--bsplus-sidebar-width", `${SIDEBAR_WIDTH_PX[width]}px`);
root.style.setProperty("--bsplus-sidebar-radius", `${radius}px`);
root.style.setProperty("--bsplus-sidebar-blur", `${blur}px`);
if (!menu) return;
setExclusiveClass(menu, DENSITY_CLASS_PREFIX, density, DEFAULT_SIDEBAR_DENSITY);
setExclusiveClass(
menu,
INDICATOR_CLASS_PREFIX,
indicator,
DEFAULT_SIDEBAR_INDICATOR,
);
}
export function clearSidebarAppearance(menu: HTMLElement | null | undefined) {
const root = document.documentElement;
root.style.removeProperty("--bsplus-sidebar-width");
root.style.removeProperty("--bsplus-sidebar-radius");
root.style.removeProperty("--bsplus-sidebar-blur");
if (!menu) return;
clearPrefixed(menu, STYLE_CLASS_PREFIX);
clearPrefixed(menu, DENSITY_CLASS_PREFIX);
clearPrefixed(menu, INDICATOR_CLASS_PREFIX);
}
+6
View File
@@ -63,6 +63,12 @@ export function getDefaultSettingsState(): SettingsState {
notificationCollector: false,
newsSource: "australia",
iconOnlySidebar: false,
sidebarStyle: "classic",
sidebarDensity: "comfortable",
sidebarCornerRadius: 12,
sidebarActiveIndicator: "fill",
sidebarWidth: "default",
sidebarBlur: 50,
adaptiveThemeColour: false,
adaptiveThemeGradient: false,
adaptiveThemeColourTransition: true,
+12
View File
@@ -66,6 +66,18 @@ export interface SettingsState {
mockNotices?: boolean;
hideSensitiveContent?: boolean;
iconOnlySidebar?: boolean;
/** Visual style for the custom Learn sidebar (`classic` default). */
sidebarStyle?: string;
/** Item density: `compact` | `comfortable` (default) | `large`. */
sidebarDensity?: string;
/** Corner radius in px for sidebar items (default `12`). */
sidebarCornerRadius?: number;
/** Active item indicator: `fill` (default) | `bar` | `outline` | `underline`. */
sidebarActiveIndicator?: string;
/** Sidebar width: `narrow` | `default` (270px) | `wide`. */
sidebarWidth?: string;
/** Backdrop blur strength in px when transparency is on (default `50`). */
sidebarBlur?: number;
adaptiveThemeColour?: boolean;
adaptiveThemeGradient?: boolean;
adaptiveThemeColourTransition?: boolean;
+2
View File
@@ -7,6 +7,7 @@ import { base64Loader } from "./lib/base64loader";
import type { BuildTarget, Manifest } from "./lib/types";
import ClosePlugin from "./lib/closePlugin";
import fixCrxWorkerLiveReload from "./lib/fixCrxWorkerLiveReload";
import stabilizeCrxDevHmr from "./lib/stabilizeCrxDevHmr";
import { firefoxStripFunctionProbe } from "./lib/firefoxStripFunctionProbe";
import { extensionChunkUrls } from "./lib/extensionChunkUrls";
@@ -93,6 +94,7 @@ export default defineConfig(({ command, mode: viteMode }) => {
browser: mode.toLowerCase() === "firefox" ? "firefox" : "chrome",
}),
fixCrxWorkerLiveReload(),
stabilizeCrxDevHmr(),
touchGlobalCSSPlugin(),
...(command === "build" ? [ClosePlugin(), firefoxStripFunctionProbe()] : []),
],