style: Update default theme
This commit is contained in:
@@ -18,7 +18,7 @@ theme/
|
||||
│ ├── constants/
|
||||
│ │ └── theme-llm-guide.constants.ts LLM prompt context for AI-assisted theme editing
|
||||
│ ├── logic/
|
||||
│ │ ├── theme-defaults.logic.ts Default theme document, JSON template, legacy detection
|
||||
│ │ ├── theme-defaults.logic.ts Built-in presets, default theme document, JSON template, legacy detection
|
||||
│ │ ├── theme-registry.logic.ts Static registry of themeable elements and layout containers
|
||||
│ │ ├── theme-schema.logic.ts Schema field definitions, animation starters, suggested defaults
|
||||
│ │ └── theme-validation.logic.ts Theme document validation against registry + schema
|
||||
@@ -42,6 +42,12 @@ theme/
|
||||
└── index.ts Barrel exports
|
||||
```
|
||||
|
||||
## Built-in presets
|
||||
|
||||
`theme-defaults.logic.ts` exports `BUILT_IN_THEME_PRESETS` for themes that ship with the app and do not depend on the Electron saved-theme library. The default preset is `Toju Website Dark`, which mirrors the website palette and removes the previous green radial chat background bubble. The previous app default remains available as `Toju Default Dark` and can be applied from Theme Studio.
|
||||
|
||||
The importable artifact at `project-files/themes/toju-website-dark.json` is kept byte-for-byte aligned with `DEFAULT_THEME_JSON` by the ThemeService spec.
|
||||
|
||||
## Layer composition
|
||||
|
||||
```mermaid
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { DOCUMENT } from '@angular/common';
|
||||
import { EnvironmentInjector, createEnvironmentInjector } from '@angular/core';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import { DEFAULT_THEME_JSON, createDefaultThemeDocument } from '../../domain/logic/theme-defaults.logic';
|
||||
import {
|
||||
BUILT_IN_THEME_PRESETS,
|
||||
DEFAULT_THEME_JSON,
|
||||
createDefaultThemeDocument
|
||||
} from '../../domain/logic/theme-defaults.logic';
|
||||
import { validateThemeDocument } from '../../domain/logic/theme-validation.logic';
|
||||
import { ThemeService } from './theme.service';
|
||||
|
||||
@@ -30,55 +36,58 @@ describe('ThemeService theme application', () => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('uses the compact Toju dark theme as the built-in default JSON', () => {
|
||||
expect(JSON.parse(DEFAULT_THEME_JSON) as unknown).toEqual({
|
||||
it('uses the website dark theme as the built-in default JSON', () => {
|
||||
const defaultTheme = JSON.parse(DEFAULT_THEME_JSON) as Record<string, unknown>;
|
||||
|
||||
expect(defaultTheme['css']).toEqual(expect.not.stringContaining('radial-gradient'));
|
||||
expect(defaultTheme).toEqual(expect.objectContaining({
|
||||
meta: {
|
||||
name: 'Toju Default Dark',
|
||||
version: '2.0.0',
|
||||
description: 'Built-in dark glass theme for the full Toju app shell.'
|
||||
name: 'Toju Website Dark',
|
||||
version: '1.0.0',
|
||||
description: 'Website-inspired dark app theme using the charcoal, green, and amber palette from the public Toju site.'
|
||||
},
|
||||
tokens: {
|
||||
colors: {
|
||||
background: '224 28% 7%',
|
||||
foreground: '210 40% 96%',
|
||||
card: '224 25% 10%',
|
||||
cardForeground: '210 40% 96%',
|
||||
popover: '224 26% 9%',
|
||||
popoverForeground: '210 40% 96%',
|
||||
primary: '193 95% 68%',
|
||||
primaryForeground: '222 47% 11%',
|
||||
secondary: '223 19% 16%',
|
||||
secondaryForeground: '210 40% 96%',
|
||||
muted: '223 18% 14%',
|
||||
mutedForeground: '215 20% 70%',
|
||||
accent: '218 22% 18%',
|
||||
accentForeground: '210 40% 98%',
|
||||
background: '210 18% 7%',
|
||||
foreground: '42 33% 94%',
|
||||
card: '210 17% 10%',
|
||||
cardForeground: '42 33% 94%',
|
||||
popover: '210 17% 9%',
|
||||
popoverForeground: '42 33% 94%',
|
||||
primary: '154 49% 55%',
|
||||
primaryForeground: '210 18% 7%',
|
||||
secondary: '210 14% 15%',
|
||||
secondaryForeground: '42 33% 94%',
|
||||
muted: '210 14% 15%',
|
||||
mutedForeground: '42 13% 67%',
|
||||
accent: '38 64% 61%',
|
||||
accentForeground: '210 18% 7%',
|
||||
destructive: '0 72% 55%',
|
||||
destructiveForeground: '0 0% 100%',
|
||||
border: '222 18% 22%',
|
||||
input: '222 18% 22%',
|
||||
ring: '193 95% 68%',
|
||||
railBackground: '226 33% 8%',
|
||||
workspaceBackground: '224 30% 9%',
|
||||
panelBackground: '224 24% 11%',
|
||||
panelBackgroundAlt: '222 22% 13%',
|
||||
titleBarBackground: '226 34% 7%',
|
||||
surfaceHighlight: '193 95% 68%',
|
||||
surfaceHighlightAlt: '261 82% 72%'
|
||||
border: '210 13% 22%',
|
||||
input: '210 13% 22%',
|
||||
ring: '154 49% 55%',
|
||||
railBackground: '210 19% 6%',
|
||||
workspaceBackground: '210 18% 8%',
|
||||
panelBackground: '210 17% 10%',
|
||||
panelBackgroundAlt: '210 14% 13%',
|
||||
titleBarBackground: '210 19% 6%',
|
||||
surfaceHighlight: '154 49% 55%',
|
||||
surfaceHighlightAlt: '38 64% 61%'
|
||||
},
|
||||
spacing: {},
|
||||
radii: {
|
||||
radius: '0.875rem',
|
||||
surface: '1.35rem',
|
||||
radius: '0.6rem',
|
||||
surface: '0.85rem',
|
||||
pill: '999px'
|
||||
},
|
||||
effects: {
|
||||
panelShadow: '0 24px 60px rgba(0, 0, 0, 0.42)',
|
||||
softShadow: '0 14px 36px rgba(0, 0, 0, 0.28)',
|
||||
glassBlur: 'blur(18px) saturate(135%)'
|
||||
panelShadow: '0 28px 64px rgba(0, 0, 0, 0.34)',
|
||||
softShadow: '0 16px 36px rgba(0, 0, 0, 0.22)',
|
||||
glassBlur: 'blur(16px) saturate(125%)'
|
||||
}
|
||||
},
|
||||
layout: {
|
||||
layout: expect.objectContaining({
|
||||
serversRail: {
|
||||
container: 'appShell',
|
||||
grid: { x: 0, y: 0, w: 1, h: 1 }
|
||||
@@ -99,10 +108,76 @@ describe('ThemeService theme application', () => {
|
||||
container: 'roomLayout',
|
||||
grid: { x: 16, y: 0, w: 4, h: 12 }
|
||||
}
|
||||
}
|
||||
}),
|
||||
elements: expect.objectContaining({
|
||||
titleBar: expect.objectContaining({ border: '0' })
|
||||
}),
|
||||
animations: {}
|
||||
}));
|
||||
});
|
||||
|
||||
it('exposes both built-in theme presets and applies the legacy default preset', () => {
|
||||
expect(BUILT_IN_THEME_PRESETS.map((preset) => preset.theme.meta.name)).toEqual(['Toju Website Dark', 'Toju Default Dark']);
|
||||
expect(service.activeThemeName()).toBe('Toju Website Dark');
|
||||
|
||||
const applied = service.applyBuiltInPreset('Toju Default Dark');
|
||||
|
||||
expect(applied).toBe(true);
|
||||
expect(service.activeThemeName()).toBe('Toju Default Dark');
|
||||
expect(service.getHostStyles('appRoot')).toMatchObject({
|
||||
'--background': '224 28% 7%',
|
||||
'--primary': '193 95% 68%'
|
||||
});
|
||||
});
|
||||
|
||||
it('resets to the website dark preset as the new default', () => {
|
||||
expect(service.applyBuiltInPreset('Toju Default Dark')).toBe(true);
|
||||
|
||||
service.resetToDefault();
|
||||
|
||||
expect(service.activeThemeName()).toBe('Toju Website Dark');
|
||||
expect(service.getHostStyles('appRoot')).toMatchObject({
|
||||
'--background': '210 18% 7%',
|
||||
'--primary': '154 49% 55%',
|
||||
'--accent': '38 64% 61%'
|
||||
});
|
||||
|
||||
expect(service.activeThemeText()).not.toContain('radial-gradient');
|
||||
});
|
||||
|
||||
it('keeps the importable website dark artifact aligned with the default preset', () => {
|
||||
const artifact = JSON.parse(readFileSync(resolve('../project-files/themes/toju-website-dark.json'), 'utf8')) as unknown;
|
||||
const defaultTheme = JSON.parse(DEFAULT_THEME_JSON) as unknown;
|
||||
|
||||
expect(artifact).toEqual(defaultTheme);
|
||||
});
|
||||
|
||||
it('keeps all built-in presets valid', () => {
|
||||
for (const preset of BUILT_IN_THEME_PRESETS) {
|
||||
const result = validateThemeDocument(preset.theme);
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
it('applies a CSS-only theme over the new default JSON theme', () => {
|
||||
const applied = service.applyCssOnlyTheme('.css-only-theme { background: hsl(var(--background)); }');
|
||||
|
||||
expect(applied).toBe(true);
|
||||
expect(service.activeThemeName()).toBe('Toju Website Dark');
|
||||
expect(service.getLayoutItemStyles('dmChatPanel')).toMatchObject({
|
||||
gridColumn: '5 / span 16',
|
||||
gridRow: '1 / span 12'
|
||||
});
|
||||
|
||||
expect(service.getHostStyles('appRoot')).toMatchObject({
|
||||
'--background': '210 18% 7%'
|
||||
});
|
||||
|
||||
expect(styleElements.some((styleElement) => styleElement.textContent === '.css-only-theme { background: hsl(var(--background)); }')).toBe(true);
|
||||
});
|
||||
|
||||
it('applies a JSON theme with tokens, layout, backgrounds, effects, metadata, and animations', () => {
|
||||
const theme = createCompleteThemeDocument();
|
||||
const loaded = service.loadThemeText(JSON.stringify(theme), 'apply', 'Theme applied.', 'complete JSON theme');
|
||||
@@ -185,23 +260,6 @@ describe('ThemeService theme application', () => {
|
||||
expect(service.activeThemeText()).toContain('"css"');
|
||||
});
|
||||
|
||||
it('applies a CSS-only theme over the default JSON theme', () => {
|
||||
const applied = service.applyCssOnlyTheme('.css-only-theme { background: hsl(var(--background)); }');
|
||||
|
||||
expect(applied).toBe(true);
|
||||
expect(service.activeThemeName()).toBe('Toju Default Dark');
|
||||
expect(service.getLayoutItemStyles('dmChatPanel')).toMatchObject({
|
||||
gridColumn: '5 / span 16',
|
||||
gridRow: '1 / span 12'
|
||||
});
|
||||
|
||||
expect(service.getHostStyles('appRoot')).toMatchObject({
|
||||
'--background': '224 28% 7%'
|
||||
});
|
||||
|
||||
expect(styleElements.some((styleElement) => styleElement.textContent === '.css-only-theme { background: hsl(var(--background)); }')).toBe(true);
|
||||
});
|
||||
|
||||
it('applies CSS over the current JSON draft theme', () => {
|
||||
const draftTheme = createDefaultThemeDocument();
|
||||
|
||||
@@ -261,7 +319,7 @@ describe('ThemeService theme application', () => {
|
||||
}
|
||||
});
|
||||
|
||||
expect(service.activeThemeName()).toBe('Toju Default Dark');
|
||||
expect(service.activeThemeName()).toBe('Toju Website Dark');
|
||||
});
|
||||
|
||||
it('validates the dedicated DM workspace layout container', () => {
|
||||
@@ -378,6 +436,32 @@ describe('ThemeService theme application', () => {
|
||||
expect(service.getHostStyles('titleBar')).toEqual({});
|
||||
expect(service.activeThemeText()).not.toContain('"elements"');
|
||||
});
|
||||
|
||||
it('loads the website dark saved-theme artifact', () => {
|
||||
const themeText = readFileSync(resolve('../project-files/themes/toju-website-dark.json'), 'utf8');
|
||||
const loaded = service.loadThemeText(themeText, 'apply', 'Theme applied.', 'website dark saved theme');
|
||||
|
||||
expect(loaded).toBe(true);
|
||||
expect(service.activeThemeName()).toBe('Toju Website Dark');
|
||||
expect(service.getHostStyles('appRoot')).toMatchObject({
|
||||
'--background': '210 18% 7%',
|
||||
'--primary': '154 49% 55%',
|
||||
'--accent': '38 64% 61%'
|
||||
});
|
||||
|
||||
expect(service.getHostStyles('titleBar')).toMatchObject({
|
||||
border: '0',
|
||||
backgroundColor: 'hsl(var(--title-bar-background) / 0.88)',
|
||||
backdropFilter: 'var(--theme-effect-glass-blur)'
|
||||
});
|
||||
|
||||
expect(service.getHostStyles('serversRail')).toMatchObject({ border: '0' });
|
||||
expect(service.getHostStyles('chatRoomChannelsPanel')).toMatchObject({ border: '0' });
|
||||
expect(service.getHostStyles('chatRoomMembersPanel')).toMatchObject({ border: '0' });
|
||||
expect(service.getHostStyles('chatComposerBar')).toMatchObject({ border: '0' });
|
||||
|
||||
expect(service.activeThemeText()).toContain('Website-inspired app shell surfaces');
|
||||
});
|
||||
});
|
||||
|
||||
interface TestStyleElement {
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
ThemeElementStyles
|
||||
} from '../../domain/models/theme.model';
|
||||
import {
|
||||
BUILT_IN_THEME_PRESETS,
|
||||
DEFAULT_THEME_JSON,
|
||||
createDefaultThemeDocument,
|
||||
createDefaultThemeLayout,
|
||||
@@ -137,6 +138,7 @@ export class ThemeService {
|
||||
readonly activeThemeName: Signal<string>;
|
||||
readonly knownAnimationClasses: Signal<string[]>;
|
||||
readonly isDraftDirty: Signal<boolean>;
|
||||
readonly builtInPresets = BUILT_IN_THEME_PRESETS;
|
||||
|
||||
private readonly documentRef = inject(DOCUMENT);
|
||||
|
||||
@@ -325,6 +327,26 @@ export class ThemeService {
|
||||
this.setStatusMessage(reason === 'shortcut' ? 'Theme reset to the default preset by shortcut.' : 'Theme reset to the default preset.');
|
||||
}
|
||||
|
||||
applyBuiltInPreset(name: string): boolean {
|
||||
const preset = this.builtInPresets.find((builtInPreset) => builtInPreset.theme.meta.name === name || builtInPreset.key === name);
|
||||
|
||||
if (!preset) {
|
||||
this.setStatusMessage('Built-in theme preset not found.');
|
||||
return false;
|
||||
}
|
||||
|
||||
const theme = structuredClone(preset.theme);
|
||||
const result = validateThemeDocument(theme);
|
||||
|
||||
if (!result.valid || !result.value) {
|
||||
this.setStatusMessage(result.errors[0] ?? 'Built-in theme preset could not be applied.');
|
||||
return false;
|
||||
}
|
||||
|
||||
this.commitTheme(result.value, stringifyTheme(result.value), `${result.value.meta.name} preset applied.`);
|
||||
return true;
|
||||
}
|
||||
|
||||
handleGlobalShortcut(event: KeyboardEvent): boolean {
|
||||
const usesModifier = event.ctrlKey || event.metaKey;
|
||||
|
||||
|
||||
@@ -111,7 +111,12 @@ function allElementsEmpty(elements: Record<string, ThemeElementStyles>): boolean
|
||||
return Object.values(elements).every((elementStyles) => Object.keys(elementStyles).length === 0);
|
||||
}
|
||||
|
||||
export function createDefaultThemeDocument(): ThemeDocument {
|
||||
export interface BuiltInThemePreset {
|
||||
key: string;
|
||||
theme: ThemeDocument;
|
||||
}
|
||||
|
||||
function createLegacyDefaultDarkThemeDocument(): ThemeDocument {
|
||||
return {
|
||||
css: '',
|
||||
meta: {
|
||||
@@ -166,6 +171,147 @@ export function createDefaultThemeDocument(): ThemeDocument {
|
||||
};
|
||||
}
|
||||
|
||||
export function createDefaultThemeDocument(): ThemeDocument {
|
||||
return {
|
||||
css: `/* Website-inspired app shell surfaces */
|
||||
app-chat-messages .chat-layout,
|
||||
app-dm-chat .chat-layout {
|
||||
background-image: linear-gradient(180deg, hsl(var(--workspace-background) / 0.96), hsl(var(--background)) 38rem) !important;
|
||||
}
|
||||
|
||||
app-chat-message-list > div {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
app-chat-message-item > div[data-message-id] {
|
||||
margin: 8px 0 !important;
|
||||
border: 1px solid hsl(var(--border) / 0.56) !important;
|
||||
border-radius: 0.7rem !important;
|
||||
background: hsl(var(--card) / 0.72) !important;
|
||||
box-shadow: 0 14px 34px rgb(0 0 0 / 0.2) !important;
|
||||
backdrop-filter: blur(12px) saturate(120%) !important;
|
||||
-webkit-backdrop-filter: blur(12px) saturate(120%) !important;
|
||||
}
|
||||
|
||||
app-chat-message-item > div[data-message-id]:hover {
|
||||
border-color: hsl(var(--primary) / 0.42) !important;
|
||||
background: hsl(var(--card) / 0.84) !important;
|
||||
}
|
||||
|
||||
app-chat-message-composer,
|
||||
.chat-bottom-bar {
|
||||
border-top: 0 !important;
|
||||
background: hsl(var(--background) / 0.84) !important;
|
||||
backdrop-filter: blur(16px) saturate(125%) !important;
|
||||
-webkit-backdrop-filter: blur(16px) saturate(125%) !important;
|
||||
}
|
||||
|
||||
app-chat-message-composer textarea,
|
||||
app-chat-message-composer [contenteditable="true"] {
|
||||
background: hsl(var(--panel-background-alt) / 0.8) !important;
|
||||
}
|
||||
`,
|
||||
meta: {
|
||||
name: 'Toju Website Dark',
|
||||
version: '1.0.0',
|
||||
description: 'Website-inspired dark app theme using the charcoal, green, and amber palette from the public Toju site.'
|
||||
},
|
||||
tokens: {
|
||||
colors: {
|
||||
background: '210 18% 7%',
|
||||
foreground: '42 33% 94%',
|
||||
card: '210 17% 10%',
|
||||
cardForeground: '42 33% 94%',
|
||||
popover: '210 17% 9%',
|
||||
popoverForeground: '42 33% 94%',
|
||||
primary: '154 49% 55%',
|
||||
primaryForeground: '210 18% 7%',
|
||||
secondary: '210 14% 15%',
|
||||
secondaryForeground: '42 33% 94%',
|
||||
muted: '210 14% 15%',
|
||||
mutedForeground: '42 13% 67%',
|
||||
accent: '38 64% 61%',
|
||||
accentForeground: '210 18% 7%',
|
||||
destructive: '0 72% 55%',
|
||||
destructiveForeground: '0 0% 100%',
|
||||
border: '210 13% 22%',
|
||||
input: '210 13% 22%',
|
||||
ring: '154 49% 55%',
|
||||
railBackground: '210 19% 6%',
|
||||
workspaceBackground: '210 18% 8%',
|
||||
panelBackground: '210 17% 10%',
|
||||
panelBackgroundAlt: '210 14% 13%',
|
||||
titleBarBackground: '210 19% 6%',
|
||||
surfaceHighlight: '154 49% 55%',
|
||||
surfaceHighlightAlt: '38 64% 61%'
|
||||
},
|
||||
spacing: {},
|
||||
radii: {
|
||||
radius: '0.6rem',
|
||||
surface: '0.85rem',
|
||||
pill: '999px'
|
||||
},
|
||||
effects: {
|
||||
panelShadow: '0 28px 64px rgba(0, 0, 0, 0.34)',
|
||||
softShadow: '0 16px 36px rgba(0, 0, 0, 0.22)',
|
||||
glassBlur: 'blur(16px) saturate(125%)'
|
||||
}
|
||||
},
|
||||
layout: createDefaultThemeLayout(),
|
||||
elements: {
|
||||
titleBar: {
|
||||
border: '0',
|
||||
backgroundColor: 'hsl(var(--title-bar-background) / 0.88)',
|
||||
boxShadow: '0 12px 28px rgba(0, 0, 0, 0.22)',
|
||||
backdropFilter: 'var(--theme-effect-glass-blur)'
|
||||
},
|
||||
serversRail: {
|
||||
border: '0',
|
||||
backgroundColor: 'hsl(var(--rail-background) / 0.94)',
|
||||
boxShadow: 'var(--theme-effect-panel-shadow)'
|
||||
},
|
||||
appWorkspace: {
|
||||
backgroundColor: 'hsl(var(--workspace-background))'
|
||||
},
|
||||
chatRoomChannelsPanel: {
|
||||
border: '0',
|
||||
backgroundColor: 'hsl(var(--panel-background) / 0.86)',
|
||||
backdropFilter: 'var(--theme-effect-glass-blur)'
|
||||
},
|
||||
chatRoomMembersPanel: {
|
||||
border: '0',
|
||||
backgroundColor: 'hsl(var(--panel-background) / 0.82)',
|
||||
backdropFilter: 'var(--theme-effect-glass-blur)'
|
||||
},
|
||||
chatRoomMainPanel: {
|
||||
backgroundColor: 'hsl(var(--workspace-background))'
|
||||
},
|
||||
chatSurface: {
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
chatMessageBubble: {
|
||||
border: '1px solid hsl(var(--border) / 0.56)',
|
||||
borderRadius: '0.7rem',
|
||||
backgroundColor: 'hsl(var(--card) / 0.72)',
|
||||
boxShadow: 'var(--theme-effect-soft-shadow)',
|
||||
backdropFilter: 'blur(12px) saturate(120%)'
|
||||
},
|
||||
chatComposerBar: {
|
||||
border: '0',
|
||||
backgroundColor: 'hsl(var(--background) / 0.84)',
|
||||
backdropFilter: 'var(--theme-effect-glass-blur)'
|
||||
},
|
||||
chatComposerInput: {
|
||||
border: '1px solid hsl(var(--border) / 0.6)',
|
||||
borderRadius: '0.6rem',
|
||||
backgroundColor: 'hsl(var(--panel-background-alt) / 0.8)',
|
||||
color: 'hsl(var(--foreground))'
|
||||
}
|
||||
},
|
||||
animations: {}
|
||||
};
|
||||
}
|
||||
|
||||
export function isLegacyDefaultThemeDocument(document: ThemeDocument): boolean {
|
||||
return (
|
||||
document.meta.name === 'Toju Default Theme' &&
|
||||
@@ -182,12 +328,18 @@ export function isLegacyDefaultThemeDocument(document: ThemeDocument): boolean {
|
||||
}
|
||||
|
||||
export const DEFAULT_THEME_DOCUMENT: ThemeDocument = createDefaultThemeDocument();
|
||||
export const DEFAULT_THEME_JSON = JSON.stringify(
|
||||
export const BUILT_IN_THEME_PRESETS: readonly BuiltInThemePreset[] = [
|
||||
{
|
||||
meta: DEFAULT_THEME_DOCUMENT.meta,
|
||||
tokens: DEFAULT_THEME_DOCUMENT.tokens,
|
||||
layout: DEFAULT_THEME_DOCUMENT.layout
|
||||
key: 'toju-website-dark',
|
||||
theme: DEFAULT_THEME_DOCUMENT
|
||||
},
|
||||
{
|
||||
key: 'toju-default-dark',
|
||||
theme: createLegacyDefaultDarkThemeDocument()
|
||||
}
|
||||
];
|
||||
export const DEFAULT_THEME_JSON = JSON.stringify(
|
||||
DEFAULT_THEME_DOCUMENT,
|
||||
null,
|
||||
2
|
||||
);
|
||||
|
||||
@@ -164,6 +164,41 @@
|
||||
|
||||
<div class="theme-settings__workspace min-h-0 flex-1">
|
||||
<aside class="theme-settings__sidebar">
|
||||
<section class="theme-studio-card p-3.5">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<p class="text-sm font-semibold text-foreground">Preset Themes</p>
|
||||
<span class="rounded-full bg-secondary px-2.5 py-1 text-[11px] font-medium text-muted-foreground">
|
||||
{{ builtInPresets.length }} built in
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="theme-settings__saved-theme-list mt-4">
|
||||
@for (preset of builtInPresets; track preset.key) {
|
||||
<button
|
||||
type="button"
|
||||
(click)="applyBuiltInPreset(preset.theme.meta.name)"
|
||||
class="theme-settings__saved-theme-button"
|
||||
[class.theme-settings__saved-theme-button--active]="activeTheme().meta.name === preset.theme.meta.name"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-foreground">{{ preset.theme.meta.name }}</p>
|
||||
<p class="mt-1 font-mono text-[11px] text-muted-foreground">{{ preset.key }}</p>
|
||||
</div>
|
||||
|
||||
@if (preset.theme.meta.name === 'Toju Website Dark') {
|
||||
<span class="rounded-full bg-primary/12 px-2 py-0.5 text-[10px] font-medium text-primary">Default</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (preset.theme.meta.description) {
|
||||
<p class="mt-2 text-xs leading-5 text-muted-foreground">{{ preset.theme.meta.description }}</p>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@if (savedThemesAvailable()) {
|
||||
<section class="theme-studio-card p-3.5">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
|
||||
@@ -62,6 +62,7 @@ export class ThemeSettingsComponent {
|
||||
readonly isDraftDirty = this.theme.isDraftDirty;
|
||||
readonly isFullscreen = this.modal.themeStudioFullscreen;
|
||||
readonly activeTheme = this.theme.activeTheme;
|
||||
readonly builtInPresets = this.theme.builtInPresets;
|
||||
readonly draftTheme = this.theme.draftTheme;
|
||||
readonly THEME_ANIMATION_FIELDS = THEME_ANIMATION_FIELD_HINTS;
|
||||
readonly animationKeys = this.theme.knownAnimationClasses;
|
||||
@@ -353,6 +354,14 @@ export class ThemeSettingsComponent {
|
||||
await this.themeLibrary.removeSelectedTheme();
|
||||
}
|
||||
|
||||
applyBuiltInPreset(name: string): void {
|
||||
this.theme.applyBuiltInPreset(name);
|
||||
this.activeWorkspace.set('editor');
|
||||
this.activeEditorTab.set('json');
|
||||
this.syncCssOnlyTextFromTheme();
|
||||
this.focusEditor();
|
||||
}
|
||||
|
||||
restoreDefaultTheme(): void {
|
||||
this.theme.resetToDefault('button');
|
||||
this.activeWorkspace.set('editor');
|
||||
|
||||
Reference in New Issue
Block a user