diff --git a/.vscode/settings.json b/.vscode/settings.json index 93af937..a76730c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -32,5 +32,6 @@ "**/node_modules": true }, "search.useIgnoreFiles": false, - "nxConsole.generateAiAgentRules": true + "nxConsole.generateAiAgentRules": true, + "nxConsole.nxWorkspacePath": "frontend" } diff --git a/frontend/libs/shared/theme-toggle/README.md b/frontend/libs/shared/theme-toggle/README.md new file mode 100644 index 0000000..610ee33 --- /dev/null +++ b/frontend/libs/shared/theme-toggle/README.md @@ -0,0 +1,7 @@ +# theme-toggle + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test theme-toggle` to execute the unit tests. diff --git a/frontend/libs/shared/theme-toggle/eslint.config.mjs b/frontend/libs/shared/theme-toggle/eslint.config.mjs new file mode 100644 index 0000000..e920498 --- /dev/null +++ b/frontend/libs/shared/theme-toggle/eslint.config.mjs @@ -0,0 +1,34 @@ +import nx from '@nx/eslint-plugin'; +import baseConfig from '../../../eslint.base.config.mjs'; + +export default [ + ...baseConfig, + ...nx.configs['flat/angular'], + ...nx.configs['flat/angular-template'], + { + files: ['**/*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'lib', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'lib', + style: 'kebab-case', + }, + ], + }, + }, + { + files: ['**/*.html'], + // Override or add rules here + rules: {}, + }, +]; diff --git a/frontend/libs/shared/theme-toggle/jest.config.ts b/frontend/libs/shared/theme-toggle/jest.config.ts new file mode 100644 index 0000000..4c8d0a9 --- /dev/null +++ b/frontend/libs/shared/theme-toggle/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'theme-toggle', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/libs/shared/theme-toggle', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/tsconfig.spec.json', + stringifyContentPathRegex: '\\.(html|svg)$', + }, + ], + }, + transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'], + snapshotSerializers: [ + 'jest-preset-angular/build/serializers/no-ng-attributes', + 'jest-preset-angular/build/serializers/ng-snapshot', + 'jest-preset-angular/build/serializers/html-comment', + ], +}; diff --git a/frontend/libs/shared/theme-toggle/project.json b/frontend/libs/shared/theme-toggle/project.json new file mode 100644 index 0000000..a08cbb9 --- /dev/null +++ b/frontend/libs/shared/theme-toggle/project.json @@ -0,0 +1,20 @@ +{ + "name": "theme-toggle", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/shared/theme-toggle/src", + "prefix": "lib", + "projectType": "library", + "tags": [], + "targets": { + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], + "options": { + "jestConfig": "libs/shared/theme-toggle/jest.config.ts" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/frontend/libs/shared/theme-toggle/src/index.ts b/frontend/libs/shared/theme-toggle/src/index.ts new file mode 100644 index 0000000..6b5fe4b --- /dev/null +++ b/frontend/libs/shared/theme-toggle/src/index.ts @@ -0,0 +1 @@ +export * from './lib/theme-toggle.service'; diff --git a/frontend/libs/shared/theme-toggle/src/lib/theme-toggle.service.ts b/frontend/libs/shared/theme-toggle/src/lib/theme-toggle.service.ts new file mode 100644 index 0000000..2253cc6 --- /dev/null +++ b/frontend/libs/shared/theme-toggle/src/lib/theme-toggle.service.ts @@ -0,0 +1,54 @@ +import { OverlayContainer } from '@angular/cdk/overlay'; +import { computed, DOCUMENT, effect, inject, Injectable } from '@angular/core'; +import { stored } from '@mmstack/primitives'; + +@Injectable({ + providedIn: 'root', +}) +export class ThemeToggleService { + private readonly overlayCls = + inject(OverlayContainer).getContainerElement().classList; + private readonly dockument = inject(DOCUMENT); + + private readonly theme = stored<'light' | 'dark'>('light', { + key: 'user-theme', + syncTabs: true, + }); + + public isDark = computed(() => this.theme() === 'dark'); + + constructor() { + const prefersDark = + window.matchMedia && + window.matchMedia('(prefers-color-scheme: dark)').matches; + if (prefersDark && this.theme() === 'light') { + this.theme.set('dark'); + } + + effect(() => + this.isDark() ? this.addDarkThemeClass() : this.removeDarkThemeClass() + ); + + effect(() => + this.dockument.body.setAttribute( + 'style', + `color-scheme: ${this.isDark() ? 'dark' : 'light'}` + ) + ); + } + + public toggleTheme() { + this.theme.set(this.isDark() ? 'light' : 'dark'); + } + + private addDarkThemeClass() { + this.overlayCls.add('dark-theme'); + this.dockument.body.classList.add('dark-theme'); + } + + private removeDarkThemeClass() { + this.overlayCls.remove('dark-theme'); + this.dockument.body.classList.remove('dark-theme'); + } +} + diff --git a/frontend/libs/shared/theme-toggle/src/test-setup.ts b/frontend/libs/shared/theme-toggle/src/test-setup.ts new file mode 100644 index 0000000..bc3333b --- /dev/null +++ b/frontend/libs/shared/theme-toggle/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/frontend/libs/shared/theme-toggle/tsconfig.json b/frontend/libs/shared/theme-toggle/tsconfig.json new file mode 100644 index 0000000..f460bb2 --- /dev/null +++ b/frontend/libs/shared/theme-toggle/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "es2022", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ], + "extends": "../../../tsconfig.base.json", + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/frontend/libs/shared/theme-toggle/tsconfig.lib.json b/frontend/libs/shared/theme-toggle/tsconfig.lib.json new file mode 100644 index 0000000..a28f762 --- /dev/null +++ b/frontend/libs/shared/theme-toggle/tsconfig.lib.json @@ -0,0 +1,17 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/**/*.spec.ts", + "src/test-setup.ts", + "jest.config.ts", + "src/**/*.test.ts" + ], + "include": ["src/**/*.ts"] +} diff --git a/frontend/libs/shared/theme-toggle/tsconfig.spec.json b/frontend/libs/shared/theme-toggle/tsconfig.spec.json new file mode 100644 index 0000000..fb72535 --- /dev/null +++ b/frontend/libs/shared/theme-toggle/tsconfig.spec.json @@ -0,0 +1,16 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "module": "commonjs", + "target": "es2016", + "types": ["jest", "node"] + }, + "files": ["src/test-setup.ts"], + "include": [ + "jest.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/frontend/src/app/app.component.ts b/frontend/src/app/app.component.ts index 81dc7e3..d3d6939 100644 --- a/frontend/src/app/app.component.ts +++ b/frontend/src/app/app.component.ts @@ -1,35 +1,74 @@ import { CommonModule } from '@angular/common'; import { Component, OnInit, inject } from '@angular/core'; +import { MatButtonModule } from '@angular/material/button'; +import { MatCardModule } from '@angular/material/card'; +import { MatIconModule } from '@angular/material/icon'; +import { MatToolbarModule } from '@angular/material/toolbar'; import { RouterModule } from '@angular/router'; import { StatsWsService } from '@frontend/shared/stats-ws'; +import { ThemeToggleService } from '@frontend/shared/theme-toggle'; @Component({ standalone: true, - imports: [RouterModule, CommonModule], + imports: [ + RouterModule, + MatButtonModule, + MatCardModule, + MatIconModule, + MatToolbarModule, + CommonModule, + ], selector: 'app-root', template: ` -
-

Docker Containers

+ + Docker Containers + + + +
@for (container of containers(); track container.id) { -
-

{{ container.name }}

-

ID: {{ container.id }}

-

Image: {{ container.image }}

-

Status: {{ container.status }}

-

State: {{ container.state }}

-
+ + + {{ container.name }} + + +

ID: {{ container.id }}

+

Image: {{ container.image }}

+

Status: {{ container.status }}

+

State: {{ container.state }}

+
+
}
`, + styles: [ + ` + .spacer { + flex: 1 1 auto; + } + .content-container { + padding: 16px; + } + `, + ], }) export class AppComponent implements OnInit { private readonly service = inject(StatsWsService); + private readonly themeToggleService = inject(ThemeToggleService); protected containers = this.service.containers; + // Add this to check current theme for the template + protected isDarkTheme = () => this.themeToggleService.isDark(); + + switchTheme() { + this.themeToggleService.toggleTheme(); + } + ngOnInit() { this.service.initContainers(); } } - diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index 90d4ee0..b7aed53 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -1 +1,16 @@ -/* You can add global styles to this file, and also import other style files */ +html, +body { + height: 100%; + max-width: 100vw; + overflow-x: hidden; +} +body { + margin: 0; + font-family: Roboto, 'Helvetica Neue', sans-serif; + background-color: #fafafa; + transition: background-color 0.3s ease; +} + +body.dark-theme { + background-color: #303030; +} diff --git a/frontend/tsconfig.base.json b/frontend/tsconfig.base.json index 71a4b90..33d2fb9 100644 --- a/frontend/tsconfig.base.json +++ b/frontend/tsconfig.base.json @@ -16,7 +16,8 @@ "baseUrl": ".", "paths": { "@frontend/shared/environment": ["libs/shared/environment/src/index.ts"], - "@frontend/shared/stats-ws": ["libs/shared/stats-ws/src/index.ts"] + "@frontend/shared/stats-ws": ["libs/shared/stats-ws/src/index.ts"], + "@frontend/shared/theme-toggle": ["libs/shared/theme-toggle/src/index.ts"] } }, "exclude": ["node_modules", "tmp"]