feat: Add deafen to pc, fix mobiel view, fix freeze on startup

This commit is contained in:
2026-06-05 15:27:06 +02:00
parent 35f52b0356
commit a675f12e61
85 changed files with 2499 additions and 519 deletions

View File

@@ -44,9 +44,9 @@ theme/
## 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.
`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 Default Dark 11`, a blue-accented dark glass shell with hover tokens (`secondary`, `accent`) lifted above the background and card surfaces. `Toju Website Dark` and the legacy cyan `Toju Default Dark` presets remain available 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.
The importable artifact at `project-files/themes/toju-default-dark-11.json` is kept byte-for-byte aligned with `DEFAULT_THEME_JSON` by the ThemeService spec.
## Layer composition

View File

@@ -36,55 +36,55 @@ describe('ThemeService theme application', () => {
vi.unstubAllGlobals();
});
it('uses the website dark theme as the built-in default JSON', () => {
it('uses the default dark 11 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 Website Dark',
version: '1.0.0',
description: 'Website-inspired dark app theme using the charcoal, green, and amber palette from the public Toju site.'
name: 'Toju Default Dark 11',
version: '2.0.0',
description: 'Built-in dark glass theme for the full Toju app shell.'
},
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%',
background: '225 6% 12%',
foreground: '210 40% 96%',
card: '220 6% 18%',
cardForeground: '210 40% 96%',
popover: '220 6% 18%',
popoverForeground: '210 40% 96%',
primary: '234 85% 64%',
primaryForeground: '222 47% 11%',
secondary: '222 10% 24%',
secondaryForeground: '210 40% 96%',
muted: '223 18% 14%',
mutedForeground: '215 20% 70%',
accent: '234 32% 28%',
accentForeground: '210 40% 98%',
destructive: '358 82% 59%',
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%'
border: '222 18% 22%',
input: '222 18% 22%',
ring: '234 85% 64%',
railBackground: '225 6% 12%',
workspaceBackground: '220 6% 18%',
panelBackground: '220 6% 18%',
panelBackgroundAlt: '225 6% 15%',
titleBarBackground: '225 6% 9%',
surfaceHighlight: '234 85% 64%',
surfaceHighlightAlt: '261 82% 72%'
},
spacing: {},
radii: {
radius: '0.6rem',
surface: '0.85rem',
radius: '0.875rem',
surface: '1.35rem',
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%)'
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%)'
}
},
layout: expect.objectContaining({
@@ -116,9 +116,13 @@ describe('ThemeService theme application', () => {
}));
});
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');
it('exposes all built-in theme presets and applies the legacy default preset', () => {
expect(BUILT_IN_THEME_PRESETS.map((preset) => preset.theme.meta.name)).toEqual([
'Toju Default Dark 11',
'Toju Website Dark',
'Toju Default Dark'
]);
expect(service.activeThemeName()).toBe('Toju Default Dark 11');
const applied = service.applyBuiltInPreset('Toju Default Dark');
@@ -130,23 +134,34 @@ describe('ThemeService theme application', () => {
});
});
it('resets to the website dark preset as the new default', () => {
expect(service.applyBuiltInPreset('Toju Default Dark')).toBe(true);
it('resets to the default dark 11 preset as the built-in default', () => {
expect(service.applyBuiltInPreset('Toju Website Dark')).toBe(true);
service.resetToDefault();
expect(service.activeThemeName()).toBe('Toju Website Dark');
expect(service.activeThemeName()).toBe('Toju Default Dark 11');
expect(service.getHostStyles('appRoot')).toMatchObject({
'--background': '210 18% 7%',
'--primary': '154 49% 55%',
'--accent': '38 64% 61%'
'--background': '225 6% 12%',
'--primary': '234 85% 64%',
'--secondary': '222 10% 24%',
'--accent': '234 32% 28%'
});
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;
it('keeps hover tokens visually distinct from the background surface', () => {
const defaultTheme = createDefaultThemeDocument();
const { background, card, secondary, accent } = defaultTheme.tokens.colors;
expect(secondary).not.toBe(background);
expect(secondary).not.toBe(card);
expect(accent).not.toBe(background);
expect(accent).not.toBe(card);
});
it('keeps the importable default dark 11 artifact aligned with the default preset', () => {
const artifact = JSON.parse(readFileSync(resolve('../project-files/themes/toju-default-dark-11.json'), 'utf8')) as unknown;
const defaultTheme = JSON.parse(DEFAULT_THEME_JSON) as unknown;
expect(artifact).toEqual(defaultTheme);
@@ -165,14 +180,14 @@ describe('ThemeService theme application', () => {
const applied = service.applyCssOnlyTheme('.css-only-theme { background: hsl(var(--background)); }');
expect(applied).toBe(true);
expect(service.activeThemeName()).toBe('Toju Website Dark');
expect(service.activeThemeName()).toBe('Toju Default Dark 11');
expect(service.getLayoutItemStyles('dmChatPanel')).toMatchObject({
gridColumn: '5 / span 16',
gridRow: '1 / span 12'
});
expect(service.getHostStyles('appRoot')).toMatchObject({
'--background': '210 18% 7%'
'--background': '225 6% 12%'
});
expect(styleElements.some((styleElement) => styleElement.textContent === '.css-only-theme { background: hsl(var(--background)); }')).toBe(true);
@@ -319,7 +334,7 @@ describe('ThemeService theme application', () => {
}
});
expect(service.activeThemeName()).toBe('Toju Website Dark');
expect(service.activeThemeName()).toBe('Toju Default Dark 11');
});
it('validates the dedicated DM workspace layout container', () => {
@@ -437,16 +452,17 @@ describe('ThemeService theme application', () => {
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');
it('loads the default dark 11 saved-theme artifact', () => {
const themeText = readFileSync(resolve('../project-files/themes/toju-default-dark-11.json'), 'utf8');
const loaded = service.loadThemeText(themeText, 'apply', 'Theme applied.', 'default dark 11 saved theme');
expect(loaded).toBe(true);
expect(service.activeThemeName()).toBe('Toju Website Dark');
expect(service.activeThemeName()).toBe('Toju Default Dark 11');
expect(service.getHostStyles('appRoot')).toMatchObject({
'--background': '210 18% 7%',
'--primary': '154 49% 55%',
'--accent': '38 64% 61%'
'--background': '225 6% 12%',
'--primary': '234 85% 64%',
'--secondary': '222 10% 24%',
'--accent': '234 32% 28%'
});
expect(service.getHostStyles('titleBar')).toMatchObject({
@@ -460,7 +476,7 @@ describe('ThemeService theme application', () => {
expect(service.getHostStyles('chatRoomMembersPanel')).toMatchObject({ border: '0' });
expect(service.getHostStyles('chatComposerBar')).toMatchObject({ border: '0' });
expect(service.activeThemeText()).toContain('Website-inspired app shell surfaces');
expect(service.activeThemeText()).toContain('Dark glass app shell surfaces');
});
});

View File

@@ -5,6 +5,98 @@ import {
} from '../models/theme.model';
import { THEME_LAYOUT_CONTAINERS, getLayoutEditableThemeKeys } from './theme-registry.logic';
const DEFAULT_SHELL_CSS = `/* Dark glass 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(--secondary) / 0.88) !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: transparent !important;
}
`;
function createDefaultShellElements(): Record<string, ThemeElementStyles> {
return {
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))'
}
};
}
function createProvidedDefaultLayout(): Record<string, ThemeLayoutEntry> {
return {
serversRail: {
@@ -171,46 +263,9 @@ function createLegacyDefaultDarkThemeDocument(): ThemeDocument {
};
}
export function createDefaultThemeDocument(): ThemeDocument {
export function createWebsiteDarkThemeDocument(): 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;
}
`,
css: DEFAULT_SHELL_CSS.replace('Dark glass app shell surfaces', 'Website-inspired app shell surfaces'),
meta: {
name: 'Toju Website Dark',
version: '1.0.0',
@@ -258,56 +313,62 @@ app-chat-message-composer [contenteditable="true"] {
}
},
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)'
elements: createDefaultShellElements(),
animations: {}
};
}
export function createDefaultThemeDocument(): ThemeDocument {
return {
css: DEFAULT_SHELL_CSS,
meta: {
name: 'Toju Default Dark 11',
version: '2.0.0',
description: 'Built-in dark glass theme for the full Toju app shell.'
},
tokens: {
colors: {
background: '225 6% 12%',
foreground: '210 40% 96%',
card: '220 6% 18%',
cardForeground: '210 40% 96%',
popover: '220 6% 18%',
popoverForeground: '210 40% 96%',
primary: '234 85% 64%',
primaryForeground: '222 47% 11%',
secondary: '222 10% 24%',
secondaryForeground: '210 40% 96%',
muted: '223 18% 14%',
mutedForeground: '215 20% 70%',
accent: '234 32% 28%',
accentForeground: '210 40% 98%',
destructive: '358 82% 59%',
destructiveForeground: '0 0% 100%',
border: '222 18% 22%',
input: '222 18% 22%',
ring: '234 85% 64%',
railBackground: '225 6% 12%',
workspaceBackground: '220 6% 18%',
panelBackground: '220 6% 18%',
panelBackgroundAlt: '225 6% 15%',
titleBarBackground: '225 6% 9%',
surfaceHighlight: '234 85% 64%',
surfaceHighlightAlt: '261 82% 72%'
},
serversRail: {
border: '0',
backgroundColor: 'hsl(var(--rail-background) / 0.94)',
boxShadow: 'var(--theme-effect-panel-shadow)'
spacing: {},
radii: {
radius: '0.875rem',
surface: '1.35rem',
pill: '999px'
},
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))'
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%)'
}
},
layout: createDefaultThemeLayout(),
elements: createDefaultShellElements(),
animations: {}
};
}
@@ -330,9 +391,13 @@ export function isLegacyDefaultThemeDocument(document: ThemeDocument): boolean {
export const DEFAULT_THEME_DOCUMENT: ThemeDocument = createDefaultThemeDocument();
export const BUILT_IN_THEME_PRESETS: readonly BuiltInThemePreset[] = [
{
key: 'toju-website-dark',
key: 'toju-default-dark-11',
theme: DEFAULT_THEME_DOCUMENT
},
{
key: 'toju-website-dark',
theme: createWebsiteDarkThemeDocument()
},
{
key: 'toju-default-dark',
theme: createLegacyDefaultDarkThemeDocument()

View File

@@ -186,7 +186,7 @@
<p class="mt-1 font-mono text-[11px] text-muted-foreground">{{ preset.key }}</p>
</div>
@if (preset.theme.meta.name === 'Toju Website Dark') {
@if (preset.theme.meta.name === 'Toju Default Dark 11') {
<span class="rounded-full bg-primary/12 px-2 py-0.5 text-[10px] font-medium text-primary">Default</span>
}
</div>