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
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,17 @@ describe('DocsTokensOverview token value caching (PERF-02)', () => {
provideDocsLocale(DocsLocale.En),
provideRouter([]),
{ provide: ActivatedRoute, useValue: { url: of([{ path: DocsStructureTokensTab.Colors }]) } },
{ provide: KBQ_WINDOW, useValue: { getComputedStyle: () => ({ getPropertyValue }) } }
{
provide: KBQ_WINDOW,
useValue: {
getComputedStyle: () => ({ getPropertyValue }),
matchMedia: () => ({
matches: false,
addEventListener: () => {},
removeEventListener: () => {}
})
}
}
]
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
effect,
inject,
Injector,
input,
signal,
viewChild
} from '@angular/core';
import { KBQ_WINDOW, ThemeService } from '@koobiq/components/core';
import { KBQ_WINDOW, KbqThemeService } from '@koobiq/components/core';
import { KbqTableModule } from '@koobiq/components/table';
import { KbqTooltipTrigger } from '@koobiq/components/tooltip';
import { DocsLocaleState } from '../../services/locale';
Expand All @@ -20,7 +21,7 @@ import { DocsComponentViewerWrapperComponent } from '../component-viewer/compone

import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import { ActivatedRoute, UrlSegment } from '@angular/router';
import { map, skip } from 'rxjs';
import { map } from 'rxjs';

import { DocsAnchorsComponent } from '../anchors/anchors.component';
import { docsData as borderRadius } from './data/border-radius';
Expand Down Expand Up @@ -200,7 +201,7 @@ export class DocsTokensOverview extends DocsLocaleState implements AfterViewInit
protected readonly wrapper = viewChild.required(DocsComponentViewerWrapperComponent);
protected readonly anchors = viewChild.required(DocsAnchorsComponent);

protected readonly themeService = inject(ThemeService);
protected readonly themeService = inject(KbqThemeService);
protected readonly window = inject(KBQ_WINDOW);
protected readonly document = inject(DOCUMENT);
protected readonly activatedRoute = inject(ActivatedRoute);
Expand Down Expand Up @@ -237,7 +238,8 @@ export class DocsTokensOverview extends DocsLocaleState implements AfterViewInit

constructor() {
super();
this.themeService.current.pipe(skip(1), takeUntilDestroyed()).subscribe(() => {
effect(() => {
this.themeService.currentTheme();
this.tokensInfo.set(this.calculateViewData());
});

Expand All @@ -260,7 +262,7 @@ export class DocsTokensOverview extends DocsLocaleState implements AfterViewInit
protected calculateViewData(): DocsTokensInfo[] {
const styles = this.window.getComputedStyle(this.document.body);

const themeKey = this.themeService.getTheme()?.className ?? 'default';
const themeKey = this.themeService.currentTheme()?.className ?? 'default';
const themeCache = this.tokenValueCache.get(themeKey) ?? new Map<string, string>();

this.tokenValueCache.set(themeKey, themeCache);
Expand Down
19 changes: 9 additions & 10 deletions apps/docs/src/app/components/docsearch/docsearch.directive.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { afterNextRender, DestroyRef, Directive, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop';
import docsearch, { DocSearchInstance, DocSearchProps } from '@docsearch/js';
import { KBQ_WINDOW, ThemeService } from '@koobiq/components/core';
import { KBQ_WINDOW, KbqThemeService } from '@koobiq/components/core';
import { combineLatest } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { DocsLocale } from '../../constants/locale';
Expand Down Expand Up @@ -129,7 +129,11 @@ const TRANSLATIONS: Record<DocsLocale, DocSearchProps['translations']> = {
export class DocsDocsearchDirective extends DocsLocaleState {
private readonly window = inject(KBQ_WINDOW);
private readonly destroyRef = inject(DestroyRef);
private readonly theme = inject(ThemeService);
private readonly theme = inject(KbqThemeService);

// captured eagerly (in the constructor's injection context), since `toObservable()` can't be
// called lazily from the `afterNextRender()` callback in `init()`
private readonly colorScheme$ = toObservable(this.theme.colorScheme);

private instance: DocSearchInstance | null = null;

Expand All @@ -148,13 +152,8 @@ export class DocsDocsearchDirective extends DocsLocaleState {

private init(): void {
combineLatest([
this.theme.current.pipe(
map(
(theme) =>
(theme?.className.replace('kbq-', '') === 'dark'
? 'dark'
: 'light') satisfies DocSearchProps['theme']
),
this.colorScheme$.pipe(
map((colorScheme) => colorScheme satisfies DocSearchProps['theme']),
distinctUntilChanged()
),
this.docsLocaleService.changes.pipe(distinctUntilChanged())
Expand Down
122 changes: 20 additions & 102 deletions apps/docs/src/app/components/navbar/navbar.component.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,8 @@
import { AsyncPipe } from '@angular/common';
import {
afterNextRender,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
inject,
OnDestroy,
ViewEncapsulation
} from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, inject, ViewEncapsulation } from '@angular/core';
import { RouterLink } from '@angular/router';
import { KbqButtonModule } from '@koobiq/components/button';
import { KBQ_WINDOW, KbqTheme, KbqThemeSelector, ThemeService } from '@koobiq/components/core';
import { KbqThemeMode, KbqThemeNames, KbqThemeService } from '@koobiq/components/core';
import { KbqDropdownModule } from '@koobiq/components/dropdown';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqLinkModule } from '@koobiq/components/link';
Expand All @@ -22,7 +14,12 @@ import { DOCS_TRANSLATIONS } from 'src/app/services/i18n';
import { DocsLocaleState } from 'src/app/services/locale';
import { DocsDocStates, DocsNavbarState } from '../../services/doc-states';
import { DocsDocsearchDirective } from '../docsearch/docsearch.directive';
import { DocsNavbarProperty } from './navbar-property';

/** A theme mode selectable from the navbar's theme dropdown. */
interface DocsThemeOption {
mode: KbqThemeMode;
title: Record<DocsLocale, string>;
}

@Component({
selector: 'docs-navbar',
Expand All @@ -45,109 +42,30 @@ import { DocsNavbarProperty } from './navbar-property';
class: 'docs-navbar'
}
})
export class DocsNavbarComponent extends DocsLocaleState implements OnDestroy {
private readonly window = inject(KBQ_WINDOW);
private readonly cdr = inject(ChangeDetectorRef);
private readonly themeService = inject(ThemeService);
export class DocsNavbarComponent extends DocsLocaleState {
private readonly themeService = inject(KbqThemeService);

readonly docStates = inject(DocsDocStates);

readonly themeSwitch: DocsNavbarProperty;

// To add for checking of current color theme of OS preferences
private readonly colorAutomaticTheme = this.window.matchMedia('(prefers-color-scheme: light)');

private readonly kbqThemes: (KbqTheme & { title: Record<DocsLocale, string> })[] = [
{
name: 'system',
className: this.colorAutomaticTheme.matches ? KbqThemeSelector.Default : KbqThemeSelector.Dark,
selected: false,
title: DOCS_TRANSLATIONS.themeSystem
},
{
name: 'light',
className: KbqThemeSelector.Default,
selected: false,
title: DOCS_TRANSLATIONS.themeLight
},
{
name: 'dark',
className: KbqThemeSelector.Dark,
selected: false,
title: DOCS_TRANSLATIONS.themeDark
}
/** Options shown in the theme dropdown. `auto` follows the OS color scheme, handled inside `KbqThemeService`. */
readonly themeOptions: DocsThemeOption[] = [
{ mode: 'auto', title: DOCS_TRANSLATIONS.themeSystem },
{ mode: KbqThemeNames.Light, title: DOCS_TRANSLATIONS.themeLight },
{ mode: KbqThemeNames.Dark, title: DOCS_TRANSLATIONS.themeDark }
];

/** The currently selected mode — persistence and OS-preference resolution are handled by `KbqThemeService`. */
readonly mode = computed(() => this.themeService.mode());

readonly opened$: Observable<boolean> = this.docStates.navbarMenu.pipe(
map((state) => state === DocsNavbarState.Opened)
);

constructor() {
super();

// set custom theme configs for light/dark themes
this.themeService.setThemes(this.kbqThemes);

this.themeSwitch = new DocsNavbarProperty({
property: 'docs_theme',
data: this.kbqThemes,
updateSelected: false
});

// set theme when retrieval from storage completed
afterNextRender(() => {
this.themeService.setTheme(this.themeSwitch.currentValue);
// prevent NG0100 error
this.cdr.markForCheck();
});

try {
// Chrome & Firefox
this.colorAutomaticTheme.addEventListener('change', this.setAutoTheme);
} catch {
try {
// Safari
this.colorAutomaticTheme.addListener(this.setAutoTheme);
} catch (errSafari) {
console.error(errSafari);
}
}
}

ngOnDestroy() {
// NOTE: `ThemeService` is a root singleton and owns its own lifecycle — the navbar must not
// tear it down. Only this component's own media-query listener is removed here.
try {
this.colorAutomaticTheme.removeEventListener('change', this.setAutoTheme);
} catch (err) {
console.error(err);
}
}

toggleMenu() {
this.docStates.toggleNavbarMenu();
}

setTheme(i: number) {
// should be set to keep theme index in storage
this.themeSwitch.setValue(i);
this.themeService.setTheme(i);
setTheme(mode: DocsThemeOption['mode']) {
this.themeService.setMode(mode);
}

private setAutoTheme = (e: MediaQueryListEvent) => {
if (!this.themeService.themes[0]) return;

this.themeService.themes[0] = {
...this.themeService.themes[0],
className: e.matches ? KbqThemeSelector.Default : KbqThemeSelector.Dark
};

if (this.themeService.themes[0].selected) {
this.setTheme(0);
}

// The media-query listener runs outside Angular's event bindings, so trigger a check
// explicitly for the OnPush theme dropdown.
this.cdr.markForCheck();
};
}
6 changes: 3 additions & 3 deletions apps/docs/src/app/components/navbar/navbar.template.html
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,9 @@
<span class="kbq-caps-compact-strong">{{ t('themeGroupHeader') }}</span>
</div>

@for (theme of themeSwitch.data; track theme) {
<button kbq-dropdown-item [class.kbq-selected]="theme.selected" (click)="setTheme($index)">
{{ theme.title[locale()] }}
@for (option of themeOptions; track option.mode) {
<button kbq-dropdown-item [class.kbq-selected]="option.mode === mode()" (click)="setTheme(option.mode)">
{{ option.title[locale()] }}
</button>
}
</kbq-dropdown>
23 changes: 14 additions & 9 deletions apps/docs/src/app/components/welcome/welcome.component.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { NgOptimizedImage } from '@angular/common';
import { ChangeDetectionStrategy, Component, ElementRef, inject, OnInit, ViewEncapsulation } from '@angular/core';
import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
import {
ChangeDetectionStrategy,
Component,
computed,
ElementRef,
inject,
OnInit,
ViewEncapsulation
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { RouterLink } from '@angular/router';
import { ThemeService } from '@koobiq/components/core';
import { KbqThemeService } from '@koobiq/components/core';
import { KbqIconModule } from '@koobiq/components/icon';
import { KbqLinkModule } from '@koobiq/components/link';
import { fromEvent } from 'rxjs';
import { debounceTime, map } from 'rxjs/operators';
import { debounceTime } from 'rxjs/operators';
import { DocsDocStates } from 'src/app/services/doc-states';
import { DocsLocaleState } from 'src/app/services/locale';
import { docsGetCategories, DocsStructureCategory } from '../../structure';
Expand All @@ -30,13 +38,10 @@ import { DocsRegisterHeaderDirective } from '../register-header/register-header.
}
})
export class DocsWelcomeComponent extends DocsLocaleState implements OnInit {
private readonly themeService = inject(ThemeService);
private readonly themeService = inject(KbqThemeService);

protected structureCategories: DocsStructureCategory[];
readonly currentTheme = toSignal(
this.themeService.current.pipe(map((theme) => theme?.className.replace('kbq-', '') ?? 'light')),
{ initialValue: 'light' }
);
readonly currentTheme = computed(() => this.themeService.colorScheme());

private readonly elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
private readonly docStates = inject(DocsDocStates);
Expand Down
9 changes: 8 additions & 1 deletion apps/docs/src/app/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideRouter, TitleStrategy } from '@angular/router';
import { KBQ_LOCALE_SERVICE, KbqLocaleService, kbqLocaleServiceLangAttrNameProvider } from '@koobiq/components/core';
import {
KBQ_LOCALE_SERVICE,
KbqLocaleService,
kbqLocaleServiceLangAttrNameProvider,
kbqThemeProvider
} from '@koobiq/components/core';
import { kbqIconsResolverProvider } from '@koobiq/components/icon';
import { DOCS_ROUTES } from './routes';
import { docsProvideAnalytics } from './services/analytics';
Expand All @@ -14,6 +19,8 @@ export const appConfig: ApplicationConfig = {
providers: [
{ provide: KBQ_LOCALE_SERVICE, useClass: KbqLocaleService },
kbqLocaleServiceLangAttrNameProvider('examples-lang'),
// keeps the pre-existing localStorage key so users who already picked a theme don't lose it
kbqThemeProvider({ storageKey: 'docs_theme' }),
Comment thread
NikGurev marked this conversation as resolved.
kbqIconsResolverProvider((name) => `/assets/SVGIcons/${name.replace(/^kbq-/, '')}.svg`),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(DOCS_ROUTES),
Expand Down
Loading