* Unsloth: appearance palettes, customization options, and control restyle Adds Standard, Classic, and Minimal color palettes to Appearance settings, each adapting to light and dark mode. Classic is a neutral enterprise look that reserves its blue accent for toggles, badges, and focus rings; Minimal is strictly black, grey, and white. Adds customization options scoped to the active mode: accent, background, and foreground colors with an in-app color picker, UI and code fonts with a searchable dropdown covering bundled, device, and imported fonts, font file import, UI and code font sizes, contrast, pointer cursors, reduce motion, font smoothing, and translucent sidebar. Settings persist through the personalization API with backend validation and sync across devices. Restyles core controls for a cleaner, flatter look in both modes: bordered white input fields, fully rounded pills for single-row controls, no drop shadows, simple straight-line chevrons replacing all rounded arrow icons, and consistent hover tones in dropdown menus. Popovers now portal into the open dialog so their lists scroll correctly inside modal dialogs. Moves Language into General settings and Chat defaults into the Chat tab above the Canvas section. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Unsloth: appearance follow-ups, font options, and settings search Neutralizes focus and selection rings across all palettes so highlighted elements, including typing boxes and the selected palette card, never take the accent color. The custom accent no longer recolors rings. Restyles the color controls as filled pills showing the hex value inside, with text and border contrast picked from the color's luminance. Menus in popovers now match the app's dropdown menus: rounded-lg corners, tighter padding, accent hover rows, and a bordered search field. Popovers inside modal dialogs are modal so their lists scroll with the wheel. Outline buttons share the same dark fills as dropdown triggers. Adds heading and chat font options next to the UI and code fonts, each using the searchable font dropdown and persisting through the personalization API. Removes the translucent sidebar option end to end. Adds settings search: a search field at the top of the settings sidebar that filters setting names across every tab, grouped by tab with icons, and jumps to the tab on click. * Unsloth: use the shared accent token for dark hover fills The settings dialog nav, its close button, the model selector, and the project switcher hovered with hardcoded blue tinted greys (#3a3d43, #2d2e32) in dark mode while every menu and sidebar uses --accent. All hover and active pill fills now use the accent token so dark hovers are the same everywhere and adapt to the active palette. * Unsloth: settings search polish and jump to matched setting Widens the settings dialog to 880px and the sidebar column to 248px so the search field has more room. The search pill aligns with the left start of the Settings title, gets more spacing above and below, and its icon and placeholder sit slightly further left. Search results now jump to the exact setting: rows and sections expose their label as a data attribute, and picking a result opens the tab, scrolls the matched row into view, and flashes it briefly. * Unsloth: settings search bar spans the full nav pill width The search field now starts and ends at the same edges as the nav hover pills instead of being inset to the title text. * Unsloth: address review findings on motion, sync, and font limits Reduce motion Off now opts back out of the OS reduced-motion preference for CSS animations via a force-motion class that the media rules skip, and forcing reduce motion On keeps the loader exceptions (spinners, loading dots, progress bars) animating. When the color scheme follows the system, the resolved mode is now part of the theme store snapshot, so an OS scheme flip re-renders consumers and reapplies per-mode custom colors instead of leaving stale inline variables from the previous mode. Imported fonts get an aggregate size cap (4.4M characters) on both the frontend sanitizer and the backend model so the persisted store always fits browser localStorage quotas, with a clear error toast when an import would exceed it. Backend validation also tightens imported font names (rejects CSS delimiter characters) and requires strict base64 font data URLs, matching the frontend patterns. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Unsloth: profile toggle to hide the sloth in the chat greeting Adds a Show greeting sloth switch to Settings > Profile. The chat welcome hides the mascot when it is off. The preference persists locally and through the personalization API, with backend validation and tests, and the row is reachable from settings search in all four locales. * Unsloth: control restyle, dropdown scrolling, and palette consistency Settings sidebar puts search on top with the tab list under a small Settings label. Combobox popups scroll with the wheel inside dialogs by falling back to manual list scrolling while a dialog scroll lock is active, and the local model selector popover became modal for the same reason. Number inputs swap native spinners for a shared grey stepper that clamps to min, max, and step. Run settings fields in light mode use the same white fill and border as the settings dialog. Selection and focus rings derive from each palette's border color instead of near black, hover borders soften the same way, the Classic sidebar stays white like Standard, decorative greens follow the palette accent, and meaning-carrying marks like the hub verified badge keep the brand green in every palette. * Unsloth: palette card selection keyed off the palette attribute Switching palettes restyles the whole page the moment data-palette lands on the html element, but the React re-render that moves the selection classes arrives later, so the ring and check briefly stayed on the previous card with the new palette's colors. The active ring and check now key off html[data-palette] in CSS, so they swap in the same style pass that swaps the tokens. Also adds breathing room around the settings search bar and under the Settings label, shortens the greeting sloth description, and renames the avatar section to Or pick a sloth profile picture in all locales. * Unsloth: restore neutral rings, drop the palette check, sidebar spacing Puts the ring tokens back to their fixed per palette values and removes the hover border darkening, undoing the derived border experiment. The selected palette card no longer shows a check since the ring already marks it. The settings sidebar search bar, nav pills, and search results get a little side padding, and the Settings label lines up with the pill text. * Unsloth: indicator restyle, sidebar menu customization, edge fade toggle - Derive focus and selection rings from the border color so indicators stay 1px and adapt to every theme and palette - Suppress mouse focus rings except on pressed controls to remove the selection flash on the avatar and palette pickers - Defer settings panel rendering so the active nav pill updates instantly - Customizable sidebar user menu with drag to reorder and shortcuts to the settings tabs - Grey hover for the standard light palette instead of green - Borderless controls in dark mode with fill based focus states - Profile picture: no picture option, pencil edit icon, atomic selection - Font dropdowns: narrower triggers and the resolved default shown as Inter Variable (Default) - System prompt border darkens on focus - New appearance setting to swap edge fades for thin divider lines - Move the theme bootstrap to an external script to satisfy CSP * Unsloth: harden theme boot and Firefox scroll container focus - Guard the theme and palette storage reads separately so a blocked localStorage (private browsing) still resolves a mode from the OS preference instead of skipping the boot entirely - Firefox makes scrollable containers keyboard focusable and drew its 3px UA outline on them; swap it for the app's soft 1px indicator * Unsloth: make the UI and code font settings reach the font utilities The theme block declared the sans and mono stacks as literals, so Tailwind inlined them into every font-sans and font-mono utility at build time and the runtime overrides from Settings > Appearance never applied. Reference the :root tokens instead, matching how the color tokens already work. * Unsloth: in-dropdown font upload, accent meters and avatar, naming cleanup - Move font importing into each font dropdown: Upload and Select folder sit side by side under the list, imported fonts get an inline remove, and the standalone Import font row is gone - Uploads reuse fonts the user already has (bundled, imported, or installed, matched by file name with style suffixes stripped) instead of embedding a duplicate copy; only new fonts are embedded - Folder scan lists font files from a picked folder in every dropdown for the session; picking one imports it through the same path - Fallback avatar uses the control accent with a readable foreground instead of the neutral primary that rendered black outside standard - Monitor bars, progress defaults, sliders, and usage meters use the control accent; warning and danger tiers stay amber and red - User facing strings that called the app just Studio now say Unsloth in all four locales, keeping Unsloth Studio and LM Studio intact * Unsloth: left align the font upload actions and divide them Upload and Select folder now read from the left like the list items, with a short vertical rule between the two. * Unsloth: keep sliders neutral and the chat greeting on Hellix - Sliders are controls, not meters, so their fill goes back to the neutral primary instead of the palette accent - The base h1 rule reads --font-heading with !important and the chat thread root resets that variable to the sans stack, which pulled the greeting off Hellix; restore the stack on the greeting element * Unsloth: move the None avatar cell last and keep footer actions on one line - None sits after the sloth pictures instead of leading the grid - Upload shrinks to its label so Select folder no longer wraps * Unsloth: size the folder action to its label Both footer actions now hug their content so the hover pill does not stretch across the leftover row width. * Unsloth: separators only between unrelated settings clusters Rows inside a titled section are related, so the per row divide-y is gone from SettingsSection. A SettingsGroupDivider marks the two real boundaries in the theme section (colors to fonts, fonts to contrast) and the Clear all chats row gets its destructive border back now that divide-y no longer draws one for it. * Unsloth: balance the two font upload actions Both actions share the footer row evenly again; nowrap keeps Select folder on one line at the narrower width. * Unsloth: drop the theme section dividers and split the chat menu groups The colors, fonts, and contrast rows read fine without rules, and the chat menu gains its one real boundary between the pin toggles and the disclaimer rows. * Unsloth: normalize oversized sidebar menus and reject newline font data URLs Two backend validation fixes in PersonalizationCustomization: - sidebarMenu refused any list longer than the number of distinct ids because Field(max_length) is enforced before the dedupe validator runs. A stale or duplicated payload that would normalize to one entry per id was rejected outright, defeating the normalizer that exists for exactly that case. Cap the incoming list at a generous multiple so it reaches the validator; a pathologically long list is still refused. - The imported font dataUrl validator used re.match on a pattern ending in $, which also matches just before a trailing newline, so "data:font/woff2;base64,AAAA\n" passed even though the frontend JS pattern rejects it. Use re.fullmatch for parity. Adds covering tests for both. * Unsloth: preview fonts in their own typeface and slim the color pills - Every font dropdown entry, the default item, and the closed trigger render in the font they name, falling back to the UI stack for families the browser cannot resolve - Color swatch pills drop from 36px to 28px so they sit closer to the row label height * Unsloth: drop the font row and theme section descriptions The labels carry the meaning on their own; the mode switching note in particular read long and confusing. * Unsloth: let the chat greeting follow the heading font setting The greeting stays on Hellix by default but adopts a chosen heading font through a --custom-heading-font variable the applier sets only while an override exists, so the thread root's sans reset for chat prose no longer hides the user's pick from the greeting. * Unsloth: divide the theme section clusters and align the color pill height Separators return between colors and fonts and between fonts and contrast, and the color pills share the 32px height of the font dropdown triggers. * Unsloth: color pills at half the dropdown width Fixed w-24 against the w-48 font triggers, with tighter padding so the hex value still fits. * Studio: update dep-removal test after next-themes was replaced The frontend no longer declares next-themes or imports it in src (it was replaced by the custom theme store and boot script), so the checker now reports its removal as a safe no-op. The C1 and C8 fixtures in test_frontend_dep_removal.py still asserted next-themes was a used dependency, which fails the studio frontend CI dependency-removal safety check. Update C1 to expect a no-op PASS and drop next-themes from the C8 expected failures so the suite matches the checker's correct output. * Studio: remove unused ageLabel and exportCollectionJsonl helpers * Studio: fix blocked-storage theme desync, search jump race, font validation - theme-store.ts: keep an in-memory currentTheme/currentPalette so a selected value survives when localStorage is blocked (private browsing). The snapshots previously re-read empty storage and reverted React state to the default while the DOM already changed. The matchMedia handler no longer re-reads storage, so it cannot clobber the in-memory choice; cross-tab storage events still adopt. - settings-dialog.tsx: the search jump waited a single fixed 60ms for the deferred tab panel to render, then silently missed under render lag. Retry across animation frames until the target row exists, then scroll and flash. - settings.py: apply the font-name character check to the four selected-font fields (uiFont/headingFont/chatFont/codeFont), and forbid backslash, comma, slash and control characters so a name cannot escape the quoted CSS font-family or smuggle extra fallbacks. Adds covering tests. * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Fix appearance customization edge cases for PR #7077 - Reset all local preferences now also clears palette and appearance customization - Number input wrapper keeps full width so fields fill their flex/grid cell, and the stepper stays pinned to the field edge - Number stepper snaps to the min anchored step grid like the native spinner instead of leaving a step-invalid value - Code font now applies to chat code fences and inline code via a dedicated token - Reduce motion (on/off) is honored by onboarding/tour confetti and the theme toggle view transition - Re-importing a font under the same name with new bytes now swaps the FontFace - Keep local customization when a synced record predates the customization field, and re-push it * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Align client font name sanitization with server validation for PR #7077 sanitizeFont now strips the same characters the backend _FONT_NAME_FORBIDDEN rejects (backslash, slash, comma, backtick) plus control chars, so a locally chosen font name can no longer pass the client but fail the personalization PUT and silently stall appearance sync. * Address follow-up review items for PR #7077 - Number input wrapper carries React Flow interaction classes (nodrag/nopan/nowheel) so clicking the stepper arrows increments instead of dragging the node - Preserve local palette and greeting-sloth toggle when the synced record predates those fields, and re-push them, mirroring the customization handling (new paletteSaved and greetingSlothSaved response flags) - Add settings-search scroll targets (data-settings-label) for the Profile title, description, display name, nickname, and avatar shape rows * Preserve absent personalization fields on PUT for PR #7077 A stale client that omits palette or customization previously had those defaults materialized by model_dump() and persisted, which flipped paletteSaved/customizationSaved to true and defeated the legacy detection. The PUT now dumps only the request's set fields and merges them onto the stored record, so omitted fields keep whatever was already stored. * Persist theme and palette via a fixed allow-list for PR #7077 The theme/palette values reach setTheme/setPalette from the authenticated personalization sync, which made the CodeQL clear-text-storage query treat writing them to localStorage as storing sensitive data. Store a re-derived literal from a constant map instead, so a plain UI preference is not tracked as sensitive; behavior is unchanged. * Harden imported-font handling for PR #7077 - syncImportedFonts: a rejected FontFace.load() only clears the registry entry if it still points at that face, so a same-name re-import while the old load was pending is no longer untracked/leaked. - Cap imported-font names to the backend length (100) so an over-long name can no longer pass the client but fail the personalization PUT and stall sync. - Add a backend test that a stale PUT preserves an existing stored palette and customization (not just that absent fields stay absent). * Return the merged personalization record from PUT The PUT /personalization handler returned the request payload, which Pydantic had already filled with defaults for any field the client omitted. A partial or stale write (for example a client sending only theme) therefore got back a response that contradicted both storage and the next GET: preserved fields like palette and the custom font showed their defaults instead of the stored values. Return model_validate(merged) so the response mirrors what was stored. The stored record is still the full merged dict, so legacy fields the model does not know about are preserved as before. * Fix small UI and keyboard-focus defects in appearance settings - Settings search now scrolls to the result within its destination tab instead of a same-named row in the previously rendered deferred tab (for example "Storage" and "Models folder" appear in both General and Resources). - The reduce-motion segmented control honors its own Off/On/System choice by reading useReducedMotionConfig instead of the OS-only useReducedMotion. - The color picker saturation/value area is operable by keyboard, so the role="slider" surface responds to the arrow keys it advertises. - Profile avatars and palette cards show a visible keyboard focus ring again. - Guard the persisted appearance-customization write so a blocked or full localStorage does not throw out of a store action, matching the theme store. - Import the appearance store symbols from the settings feature barrel. * Tighten appearance fix comments --------- Co-authored-by: shimmyshimmer <shimmyshimmer@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel Han <danielhanchen@gmail.com>
27 lines
1.1 KiB
JavaScript
27 lines
1.1 KiB
JavaScript
// SPDX-License-Identifier: AGPL-3.0-only
|
|
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
// Apply the stored theme and palette before the bundle loads so the first
|
|
// paint is never the wrong mode. Loaded as an external classic script (it
|
|
// blocks parsing, like an inline script) because the backend CSP only
|
|
// allows script-src 'self'.
|
|
try {
|
|
// Storage reads get their own guards so a blocked localStorage (private
|
|
// browsing) still resolves a mode from the OS preference.
|
|
var theme = "system";
|
|
var palette = null;
|
|
try {
|
|
theme = localStorage.getItem("theme") || "system";
|
|
palette = localStorage.getItem("palette");
|
|
} catch (e) {}
|
|
var dark =
|
|
theme === "dark" ||
|
|
(theme !== "light" && matchMedia("(prefers-color-scheme: dark)").matches);
|
|
var root = document.documentElement;
|
|
root.classList.toggle("dark", dark);
|
|
root.classList.toggle("light", !dark);
|
|
root.style.colorScheme = dark ? "dark" : "light";
|
|
if (palette === "classic" || palette === "minimal") {
|
|
root.setAttribute("data-palette", palette);
|
|
}
|
|
} catch (e) {}
|