Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/36-blocks/src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ export class AppComponent extends BaseComponent implements OnInit, OnDestroy {
}

public ngOnInit(): void {
this.uiSettings.setTheme(this.uiSettings.theme);
// Keep the root theme service alive for the whole session, including system-scheme changes.
this.uiSettings.themePreference();

this.logoutActionComplete$.subscribe((res) => {
if (res) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -771,7 +771,7 @@ export class CreateFeatureComponent extends BaseComponent implements OnDestroy,

/** Default primary color: black in light theme, white in dark theme */
public getDefaultPrimaryColor(): string {
return this.uiSettings.theme === 'dark-theme' ? '#ffffff' : '#000000';
return this.uiSettings.isDark() ? '#ffffff' : '#000000';
}

/** Returns the effective primary color based on the selected branding theme */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,13 @@ <h1 class="text-base font-semibold text-color leading-tight">Widget Preview</h1>
}
</mat-nav-list>

<div class="px-2 pb-2">
<p class="m-0 mb-1 px-1 text-xs secondary-text-color">Console theme</p>
<proxy-theme-switcher class="theme-switcher--full" />
</div>
<!-- User menu footer — consistent with main layout sidebar -->
@if (logInData$ | async; as user) {
<proxy-sidebar-user-menu
[user]="user"
[isDarkMode]="isDarkMode()"
(darkModeToggled)="switchedDarkMode($event)"
(loggedOut)="logOut()"
></proxy-sidebar-user-menu>
<proxy-sidebar-user-menu [user]="user" (loggedOut)="logOut()"></proxy-sidebar-user-menu>
}
</div>
</mat-drawer>
Expand All @@ -59,7 +58,7 @@ <h1 class="text-base font-semibold text-color leading-tight">Widget Preview</h1>
hideSingleSelectionIndicator
[value]="theme()"
(change)="onThemeChange($event.value)"
aria-label="Select theme"
aria-label="Widget preview theme"
>
<mat-button-toggle [value]="WidgetTheme.Light" matTooltip="Light theme">
<mat-icon aria-hidden="true">light_mode</mat-icon>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { selectLogInData } from '../../../../../website/home/ngrx/selector/login
import { ILogInFeatureStateWithRootState } from '../../../../../website/home/ngrx/store/login.state';
import * as logInActions from '../../../../../website/home/ngrx/actions/login.action';
import { SidebarUserMenuComponent } from '../../../../layout/sidebar-user-menu/sidebar-user-menu.component';
import { UiSettingsService } from '../../../../layout/ui-settings.service';
import { ThemeSwitcherComponent } from '../../../../layout/theme-switcher/theme-switcher.component';
import { SideNavService } from '../../../../layout/side-nav.service';
import { Location } from '@angular/common';
import { environment } from '../../../../../../environments/environment';
Expand Down Expand Up @@ -61,6 +61,7 @@ export type PreviewTab =
CopyButtonComponent,
A11yModule,
SidebarUserMenuComponent,
ThemeSwitcherComponent,
],
templateUrl: './widget-preview.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
Expand All @@ -74,13 +75,11 @@ export class WidgetPreviewComponent {
private breakpointObserver = inject(BreakpointObserver);
private readonly _injector = inject(Injector);
private readonly store = inject<Store<ILogInFeatureStateWithRootState>>(Store);
private readonly uiSettings = inject(UiSettingsService);
private readonly sideNavService = inject(SideNavService);

@ViewChild('drawer') drawer!: MatDrawer;

public readonly isMobile = signal<boolean>(false);
public readonly isDarkMode = signal<boolean>(false);
public readonly logInData$: Observable<IFirebaseUserModel>;
private readonly originUrl: string | null;

Expand Down Expand Up @@ -129,7 +128,6 @@ export class WidgetPreviewComponent {
constructor() {
this.originUrl = this.resolveOriginUrl();
this.logInData$ = this.store.pipe(select(selectLogInData), distinctUntilChanged(isEqual));
this.isDarkMode.set(this.uiSettings.theme === 'dark-theme');
this.breakpointObserver
.observe([Breakpoints.XSmall, Breakpoints.Small])
.pipe(takeUntilDestroyed())
Expand Down Expand Up @@ -260,12 +258,6 @@ export class WidgetPreviewComponent {
this.store.dispatch(logInActions.logoutAction());
}

public switchedDarkMode(isDark: boolean): void {
const hostClass = isDark ? 'dark-theme' : 'light-theme';
this.uiSettings.setTheme(hostClass);
this.isDarkMode.set(isDark);
}

public launchWidget(): void {
const tab = this.activeTab();
const isAuthTab = tab === PublicScriptType.Authorization;
Expand Down
9 changes: 2 additions & 7 deletions apps/36-blocks/src/app/panel/layout/layout.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -94,13 +94,7 @@
></proxy-main-left-side-nav>
<!-- Proxy Sidenav::END -->
@if (logInData$ | async; as user) {
<proxy-sidebar-user-menu
class="mt-auto"
[user]="user"
[isDarkMode]="isDarkMode()"
(darkModeToggled)="switchedDarkMode($event)"
(loggedOut)="logOut()"
></proxy-sidebar-user-menu>
<proxy-sidebar-user-menu class="mt-auto" [user]="user" (loggedOut)="logOut()"></proxy-sidebar-user-menu>
}
<div
class="sidebar-billing-link shrink-0"
Expand Down Expand Up @@ -150,6 +144,7 @@
><span class="context-area">{{ currentArea() }}</span>
</div>
<div class="context-meta">
<proxy-theme-switcher class="theme-switcher--responsive" />
<span class="environment-label"
><span class="environment-prefix">Console: </span>{{ environmentLabel }}</span
><a href="https://developers.36blocks.com">Developer docs ↗</a>
Expand Down
22 changes: 6 additions & 16 deletions apps/36-blocks/src/app/panel/layout/layout.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
Component,
OnDestroy,
OnInit,
effect,
inject,
signal,
} from '@angular/core';
Expand All @@ -22,6 +23,7 @@ import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { ScrollingModule } from '@angular/cdk/scrolling';
import { MainLeftSideNavComponent } from './main-left-side-nav/main-left-side-nav.component';
import { SidebarUserMenuComponent } from './sidebar-user-menu/sidebar-user-menu.component';
import { ThemeSwitcherComponent } from './theme-switcher/theme-switcher.component';
import { IClient, IClientSettings, IFirebaseUserModel, IPaginatedResponse } from '@proxy/models/root-models';
import { BaseComponent } from '@proxy/ui/base-component';
import { Store, select } from '@ngrx/store';
Expand Down Expand Up @@ -59,6 +61,7 @@ import { take } from 'rxjs/operators';
ScrollingModule,
MainLeftSideNavComponent,
SidebarUserMenuComponent,
ThemeSwitcherComponent,
],
host: { class: 'flex w-full h-screen' },
templateUrl: './layout.component.html',
Expand All @@ -70,8 +73,6 @@ export class LayoutComponent extends BaseComponent implements OnInit, OnDestroy
public clients$: Observable<IPaginatedResponse<IClient[]>>;
public swtichClientSuccess$: Observable<boolean>;

public isDarkMode = signal<boolean>(false);

public toggleMenuSideBar: boolean;
public showContainer = false;
readonly currentArea = signal('Overview');
Expand Down Expand Up @@ -114,6 +115,9 @@ export class LayoutComponent extends BaseComponent implements OnInit, OnDestroy

constructor() {
super();
effect(() => {
this.sendThemeToChatbot(this.uiSettings.isDark() ? 'dark' : 'light');
});
this.logInData$ = this.store.pipe(
select(selectLogInData),
distinctUntilChanged(isEqual),
Expand Down Expand Up @@ -169,7 +173,6 @@ export class LayoutComponent extends BaseComponent implements OnInit, OnDestroy
ngOnInit(): void {
this.getClients();
this.getClientSettings();
this.getCurrentTheme();
document.body.classList.remove('website-active');

if (this.isMobileDevice()) {
Expand Down Expand Up @@ -272,19 +275,6 @@ export class LayoutComponent extends BaseComponent implements OnInit, OnDestroy
this.store.dispatch(rootActions.switchClient({ request: { client_id: clientId } }));
}

public getCurrentTheme() {
this.switchedDarkMode(this.uiSettings.theme === 'dark-theme');
}

public switchedDarkMode(isDarkMode: boolean) {
const hostClass = isDarkMode ? 'dark-theme' : 'light-theme';
this.uiSettings.setTheme(hostClass);
this.isDarkMode.set(isDarkMode);

// This GTWY chatbot config for forced theme
this.sendThemeToChatbot(this.isDarkMode() ? 'dark' : 'light');
}

private sendThemeToChatbot(theme: 'light' | 'dark'): void {
(window as any).Chatbot?.sendData({ theme });
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@
}
<mat-menu #menu="matMenu" class="profile-menu ml-1 w-[260px]">
<h4 class="m-0 py-2 px-6 text-dark font-medium">{{ user.displayName }}</h4>
<button type="button" mat-menu-item class="flex items-center gap-2" (click)="darkModeToggled.emit(!isDarkMode)">
<mat-icon class="mat-icon-18 material-icons-outlined">{{ isDarkMode ? 'dark_mode' : 'light_mode' }}</mat-icon>
<span>{{ isDarkMode ? 'Light Mode' : 'Dark Mode' }}</span>
</button>
<div class="theme-menu" (click)="$event.stopPropagation()" (mousedown)="$event.stopPropagation()">
<p class="theme-menu__label">Theme</p>
<proxy-theme-switcher class="theme-switcher--full" />
</div>
<button
type="button"
mat-menu-item
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.theme-menu {
padding: 2px 12px 8px;
}

.theme-menu__label {
color: var(--experience-muted);
font-size: 11px;
margin: 0 4px 6px;
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { ChangeDetectionStrategy, Component, Input, Output, EventEmitter, inject, signal } from '@angular/core';
import { ChangeDetectionStrategy, Component, Input, Output, EventEmitter, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatMenuModule } from '@angular/material/menu';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatTooltipModule } from '@angular/material/tooltip';
import { IFirebaseUserModel } from '@proxy/models/root-models';
import { SideNavService } from '../side-nav.service';
import { ThemeSwitcherComponent } from '../theme-switcher/theme-switcher.component';

@Component({
selector: 'proxy-sidebar-user-menu',
templateUrl: './sidebar-user-menu.component.html',
styleUrl: './sidebar-user-menu.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, MatMenuModule, MatButtonModule, MatIconModule, MatTooltipModule],
imports: [CommonModule, MatMenuModule, MatButtonModule, MatIconModule, MatTooltipModule, ThemeSwitcherComponent],
})
export class SidebarUserMenuComponent {
@Input({ required: true }) user!: IFirebaseUserModel;
@Input() isDarkMode = false;
@Output() darkModeToggled = new EventEmitter<boolean>();
@Output() loggedOut = new EventEmitter<void>();

public sideNavService = inject(SideNavService);
Expand Down
81 changes: 81 additions & 0 deletions apps/36-blocks/src/app/panel/layout/theme-preference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
export type ThemePreference = 'light' | 'dark' | 'system';
export type AppTheme = 'light-theme' | 'dark-theme';

export const UI_SETTINGS_STORAGE_KEY = 'ui-settings';
export const UI_SETTINGS_TTL_MS = 30 * 24 * 60 * 60 * 1000;

export interface StoredUiSettings {
theme: AppTheme;
themePreference: ThemePreference;
sideNavOpen: boolean;
_savedAt: number;
}

const PREFERENCES: readonly ThemePreference[] = ['light', 'dark', 'system'];

export function isThemePreference(value: unknown): value is ThemePreference {
return value === 'light' || value === 'dark' || value === 'system';
}

/** Map a saved record onto a preference. Older records only stored the resolved class. */
export function preferenceFromStored(stored: unknown): ThemePreference {
if (!stored || typeof stored !== 'object') {
return 'light';
}
const record = stored as { themePreference?: unknown; theme?: unknown };
if (isThemePreference(record.themePreference)) {
return record.themePreference;
}
if (record.theme === 'dark-theme') {
return 'dark';
}
if (record.theme === 'light-theme') {
return 'light';
}
return 'light';
}

/** The class that actually paints the dashboard. System follows the OS scheme. */
export function resolveThemeClass(preference: ThemePreference, systemDark: boolean): AppTheme {
if (preference === 'dark') {
return 'dark-theme';
}
if (preference === 'light') {
return 'light-theme';
}
return systemDark ? 'dark-theme' : 'light-theme';
}

/**
* Read `localStorage['ui-settings']`. Expired, corrupt, or empty records return null
* so the caller can apply defaults. `apps/36-blocks/src/index.html` mirrors this before
* Angular boots; keep the key, TTL, preference names, and class names in step.
*/
export function readStoredUiSettings(raw: string | null, now: number, systemDark: boolean): StoredUiSettings | null {
if (!raw) {
return null;
}
try {
const parsed: unknown = JSON.parse(raw);
if (!parsed || typeof parsed !== 'object') {
return null;
}
const record = parsed as { _savedAt?: unknown; sideNavOpen?: unknown };
if (typeof record._savedAt !== 'number' || now - record._savedAt >= UI_SETTINGS_TTL_MS) {
return null;
}
const themePreference = preferenceFromStored(parsed);
return {
theme: resolveThemeClass(themePreference, systemDark),
themePreference,
sideNavOpen: typeof record.sideNavOpen === 'boolean' ? record.sideNavOpen : true,
_savedAt: record._savedAt,
};
} catch {
return null;
}
}

export function themePreferences(): readonly ThemePreference[] {
return PREFERENCES;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<mat-button-toggle-group
class="theme-switcher"
hideSingleSelectionIndicator
[value]="preference()"
(change)="select($event.value)"
aria-label="Color theme"
>
<mat-button-toggle value="light" matTooltip="Light">
<span class="theme-switcher__option">
<mat-icon class="material-icons-outlined" aria-hidden="true">light_mode</mat-icon>
<span class="theme-switcher__label">Light</span>
</span>
</mat-button-toggle>
<mat-button-toggle value="dark" matTooltip="Dark">
<span class="theme-switcher__option">
<mat-icon class="material-icons-outlined" aria-hidden="true">dark_mode</mat-icon>
<span class="theme-switcher__label">Dark</span>
</span>
</mat-button-toggle>
<mat-button-toggle value="system" matTooltip="Match system setting">
<span class="theme-switcher__option">
<mat-icon class="material-icons-outlined" aria-hidden="true">contrast</mat-icon>
<span class="theme-switcher__label">System</span>
</span>
</mat-button-toggle>
</mat-button-toggle-group>
Loading
Loading