Add Classic | Studio displayMode preference and Settings cards.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-09-18 13:07:13 -05:00
co-authored by Cursor
parent b194b3cefa
commit 292d53a20e
5 changed files with 200 additions and 4 deletions
+80
View File
@@ -0,0 +1,80 @@
/* Studio 2 display skins — Classic | Studio. Layout tokens only; palette matches main.css. */
:root,
[data-display='classic'],
[data-display='studio'] {
--display-bg: #09090b;
--display-fg: #e4e4e7;
--display-muted: #a1a1aa;
--display-border: rgba(255, 255, 255, 0.1);
--display-amber: #fbbf24;
--display-amber-fg: #09090b;
--display-radius: 8px;
--display-focus: rgba(252, 211, 77, 0.5);
}
[data-display='classic'] {
--chrome-gap: 0.5rem;
--rail-h: 2.25rem;
--card-pad: 0.75rem;
--display-type-title: 1.75rem;
--display-type-body: 0.875rem;
}
[data-display='studio'] {
--chrome-gap: 1rem;
--rail-h: 3rem;
--card-pad: 1rem;
--display-type-title: 1.875rem;
--display-type-body: 0.9375rem;
}
[data-display] .display-rail {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
min-height: var(--rail-h);
}
[data-display] .display-rail__seg {
min-height: var(--rail-h);
border-radius: var(--display-radius);
border: 1px solid var(--display-border);
padding: 0.5rem 1rem;
font-size: var(--display-type-body);
outline: none;
transition: border-color 0.15s ease, background-color 0.15s ease, color 0.15s ease;
}
[data-display] .display-rail__seg:focus-visible {
box-shadow: 0 0 0 2px var(--display-focus);
}
[data-display] .display-rail__seg[aria-pressed='true'],
[data-display] .display-rail__seg[data-active='true'] {
border-color: var(--display-amber);
background: var(--display-amber);
color: var(--display-amber-fg);
}
[data-display] .display-rail__seg:disabled,
[data-display] .display-rail__seg[aria-disabled='true'] {
opacity: 0.55;
cursor: not-allowed;
}
[data-display='studio'] .display-rail__seg {
font-weight: 600;
padding: 0.625rem 1.25rem;
}
[data-display] .display-cta {
border-radius: calc(var(--display-radius) + 4px);
background: var(--display-amber);
color: var(--display-amber-fg);
font-weight: 600;
}
[data-display] .display-cta:focus-visible {
box-shadow: 0 0 0 2px var(--display-focus);
}
+35 -2
View File
@@ -20,6 +20,37 @@
:class="noticeError ? 'border-red-400/30 bg-red-950/40 text-red-100' : 'border-emerald-400/20 bg-emerald-950/30 text-emerald-100'" :class="noticeError ? 'border-red-400/30 bg-red-950/40 text-red-100' : 'border-emerald-400/20 bg-emerald-950/30 text-emerald-100'"
>{{ notice }}</p> >{{ notice }}</p>
<section v-if="activePanel === 'display'" class="glass rounded-3xl p-5 sm:p-6">
<h2 class="font-display text-xl font-bold">Display</h2>
<p class="mt-1 text-sm text-zinc-400">Layout only. Jobs, engines, and library IDs stay the same.</p>
<div class="mt-4 grid gap-3 sm:grid-cols-2">
<button
type="button"
class="rounded-2xl border p-4 text-left outline-none ring-amber-300/40 focus-visible:ring-2"
:class="storedDisplayMode === 'classic'
? 'border-amber-300/60 bg-amber-400/10'
: 'border-white/10 bg-zinc-950/50 hover:border-white/25'"
:aria-pressed="storedDisplayMode === 'classic'"
@click="setDisplayMode('classic')"
>
<span class="block font-display text-lg font-semibold text-zinc-50">Classic</span>
<span class="mt-1 block text-xs text-zinc-400">Packed header, compact selects, tight gaps.</span>
</button>
<button
type="button"
class="rounded-2xl border p-4 text-left outline-none ring-amber-300/40 focus-visible:ring-2"
:class="storedDisplayMode === 'studio'
? 'border-amber-300/60 bg-amber-400/10'
: 'border-white/10 bg-zinc-950/50 hover:border-white/25'"
:aria-pressed="storedDisplayMode === 'studio'"
@click="setDisplayMode('studio')"
>
<span class="block font-display text-lg font-semibold text-zinc-50">Studio</span>
<span class="mt-1 block text-xs text-zinc-400">Task and model rails, locks by slots, eye toggles.</span>
</button>
</div>
</section>
<section v-if="activePanel === 'gpu'" class="glass rounded-3xl p-5 sm:p-6"> <section v-if="activePanel === 'gpu'" class="glass rounded-3xl p-5 sm:p-6">
<h2 class="font-display text-xl font-bold">GPU & defaults</h2> <h2 class="font-display text-xl font-bold">GPU & defaults</h2>
<div class="mt-4 space-y-3 rounded-2xl border border-white/10 bg-zinc-950/50 px-4 py-3"> <div class="mt-4 space-y-3 rounded-2xl border border-white/10 bg-zinc-950/50 px-4 py-3">
@@ -422,11 +453,12 @@ export interface SettingsFolder {
unlocked: boolean unlocked: boolean
} }
type SettingsPanel = 'gpu' | 'storage' | 'login' | 'folders' | 'loras' | 'keeps' | 'prompts' type SettingsPanel = 'display' | 'gpu' | 'storage' | 'login' | 'folders' | 'loras' | 'keeps' | 'prompts'
type LoraKindFilter = 'all' | 'image' | 'video' type LoraKindFilter = 'all' | 'image' | 'video'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const { storedDisplayMode, setDisplayMode } = useDisplayMode()
const folders = ref<SettingsFolder[]>([]) const folders = ref<SettingsFolder[]>([])
const authMode = ref('') const authMode = ref('')
@@ -435,7 +467,7 @@ const notice = ref('')
const noticeError = ref(false) const noticeError = ref(false)
let noticeTimer: ReturnType<typeof setTimeout> | null = null let noticeTimer: ReturnType<typeof setTimeout> | null = null
const panelIds: SettingsPanel[] = ['gpu', 'storage', 'login', 'folders', 'loras', 'keeps', 'prompts'] const panelIds: SettingsPanel[] = ['display', 'gpu', 'storage', 'login', 'folders', 'loras', 'keeps', 'prompts']
function parsePanel(raw: unknown): SettingsPanel { function parsePanel(raw: unknown): SettingsPanel {
const value = String(raw || '') const value = String(raw || '')
@@ -456,6 +488,7 @@ const activePanel = computed({
const navItems = computed(() => { const navItems = computed(() => {
const items: Array<{ id: SettingsPanel; label: string }> = [ const items: Array<{ id: SettingsPanel; label: string }> = [
{ id: 'display', label: 'Display' },
{ id: 'gpu', label: 'GPU & defaults' }, { id: 'gpu', label: 'GPU & defaults' },
{ id: 'storage', label: 'Storage' } { id: 'storage', label: 'Storage' }
] ]
+2 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<main class="w-full min-w-0 space-y-5 p-4 md:p-6 xl:p-8"> <main class="w-full min-w-0 space-y-5 p-4 md:p-6 xl:p-8" :data-display="displayMode">
<header class="flex flex-wrap items-end justify-between gap-4"> <header class="flex flex-wrap items-end justify-between gap-4">
<div> <div>
<h1 class="font-display text-3xl font-bold tracking-tight text-zinc-50">Studio 2</h1> <h1 class="font-display text-3xl font-bold tracking-tight text-zinc-50">Studio 2</h1>
@@ -255,6 +255,7 @@ import {randomSeed} from '~/shared/studio2/seed.mjs'
import {DEFAULT_MUSIC_LYRICS,DEFAULT_MUSIC_TAGS} from '~/utils/music' import {DEFAULT_MUSIC_LYRICS,DEFAULT_MUSIC_TAGS} from '~/utils/music'
const xaigen=useRuntimeConfig().public.studio==='xaigen' const xaigen=useRuntimeConfig().public.studio==='xaigen'
const {displayMode}=useDisplayMode()
const picker=ref('') const picker=ref('')
const uploading=ref(false),uploadMessage=ref('') const uploading=ref(false),uploadMessage=ref('')
const revealed=ref<Set<string>>(new Set()) const revealed=ref<Set<string>>(new Set())
+82
View File
@@ -0,0 +1,82 @@
export type DisplayMode = 'classic' | 'studio'
const STORAGE_KEY = 'aigen.displayMode'
function parseMode(raw: unknown): DisplayMode | null {
const value = String(raw || '').trim().toLowerCase()
return value === 'classic' || value === 'studio' ? value : null
}
function readStored(): DisplayMode {
if (!import.meta.client) return 'studio'
try {
return parseMode(localStorage.getItem(STORAGE_KEY)) || 'studio'
} catch {
return 'studio'
}
}
function writeStored(mode: DisplayMode) {
if (!import.meta.client) return
try {
localStorage.setItem(STORAGE_KEY, mode)
} catch { /* ignore quota / private mode */ }
}
/** Classic | Studio chrome skin. One Bench tree; layout only. */
export function useDisplayMode() {
const route = useRoute()
const displayMode = useState<DisplayMode>('aigen.displayMode', () => 'studio')
const sessionOverride = useState<DisplayMode | null>('aigen.displayMode.session', () => null)
const hydrated = useState('aigen.displayMode.hydrated', () => false)
const resolved = computed<DisplayMode>(() => {
const fromQuery = parseMode(route.query.display)
if (fromQuery) return fromQuery
if (sessionOverride.value) return sessionOverride.value
return displayMode.value
})
function setDisplayMode(mode: DisplayMode, opts?: { persist?: boolean; session?: boolean }) {
const persist = opts?.persist !== false
if (opts?.session) sessionOverride.value = mode
else sessionOverride.value = null
displayMode.value = mode
if (persist) writeStored(mode)
}
function hydrate() {
if (hydrated.value) return
hydrated.value = true
const fromQuery = parseMode(route.query.display)
if (fromQuery) {
sessionOverride.value = fromQuery
return
}
displayMode.value = readStored()
}
if (import.meta.client) {
onMounted(hydrate)
watch(
() => route.query.display,
(raw) => {
const fromQuery = parseMode(raw)
if (fromQuery) sessionOverride.value = fromQuery
else if (sessionOverride.value && !parseMode(route.query.display)) {
// Drop session override when query cleared; keep stored preference.
sessionOverride.value = null
displayMode.value = readStored()
}
}
)
}
return {
displayMode: resolved,
storedDisplayMode: displayMode,
setDisplayMode,
isStudio: computed(() => resolved.value === 'studio'),
isClassic: computed(() => resolved.value === 'classic')
}
}
+1 -1
View File
@@ -4,7 +4,7 @@ const xaigenStudio = /^xaigen$/i.test(String(process.env.NUXT_PUBLIC_STUDIO || '
export default defineNuxtConfig({ export default defineNuxtConfig({
compatibilityDate: '2025-08-01', compatibilityDate: '2025-08-01',
modules: ['@nuxtjs/tailwindcss'], modules: ['@nuxtjs/tailwindcss'],
css: ['~/assets/css/main.css'], css: ['~/assets/css/main.css', '~/assets/css/display.css'],
app: { app: {
head: { head: {
title: 'AIGen — MiniMax H3', title: 'AIGen — MiniMax H3',