From bb9e9ff90e692b7ad00c63ba845880d4882b9b2d Mon Sep 17 00:00:00 2001 From: Lorenz Hilpert Date: Fri, 5 Dec 2025 21:05:31 +0100 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat(shell-tabs):=20scaffold=20shel?= =?UTF-8?q?l-tabs=20library=20with=20tab=20components?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds new shell-tabs library with ShellTabsComponent and ShellTabItemComponent for managing application tabs in the shell header area. --- libs/shell/tabs/README.md | 7 +++ libs/shell/tabs/eslint.config.cjs | 34 ++++++++++++ libs/shell/tabs/project.json | 20 +++++++ libs/shell/tabs/src/index.ts | 1 + .../components/shell-tab-item.component.css | 23 ++++++++ .../components/shell-tab-item.component.html | 53 +++++++++++++++++++ .../components/shell-tab-item.component.ts | 48 +++++++++++++++++ .../tabs/src/lib/shell-tabs.component.css | 3 ++ .../tabs/src/lib/shell-tabs.component.html | 5 ++ .../tabs/src/lib/shell-tabs.component.ts | 43 +++++++++++++++ libs/shell/tabs/src/test-setup.ts | 13 +++++ libs/shell/tabs/tsconfig.json | 30 +++++++++++ libs/shell/tabs/tsconfig.lib.json | 27 ++++++++++ libs/shell/tabs/tsconfig.spec.json | 29 ++++++++++ libs/shell/tabs/vite.config.mts | 27 ++++++++++ tsconfig.base.json | 1 + 16 files changed, 364 insertions(+) create mode 100644 libs/shell/tabs/README.md create mode 100644 libs/shell/tabs/eslint.config.cjs create mode 100644 libs/shell/tabs/project.json create mode 100644 libs/shell/tabs/src/index.ts create mode 100644 libs/shell/tabs/src/lib/components/shell-tab-item.component.css create mode 100644 libs/shell/tabs/src/lib/components/shell-tab-item.component.html create mode 100644 libs/shell/tabs/src/lib/components/shell-tab-item.component.ts create mode 100644 libs/shell/tabs/src/lib/shell-tabs.component.css create mode 100644 libs/shell/tabs/src/lib/shell-tabs.component.html create mode 100644 libs/shell/tabs/src/lib/shell-tabs.component.ts create mode 100644 libs/shell/tabs/src/test-setup.ts create mode 100644 libs/shell/tabs/tsconfig.json create mode 100644 libs/shell/tabs/tsconfig.lib.json create mode 100644 libs/shell/tabs/tsconfig.spec.json create mode 100644 libs/shell/tabs/vite.config.mts diff --git a/libs/shell/tabs/README.md b/libs/shell/tabs/README.md new file mode 100644 index 000000000..b6e18217c --- /dev/null +++ b/libs/shell/tabs/README.md @@ -0,0 +1,7 @@ +# shell-tabs + +This library was generated with [Nx](https://nx.dev). + +## Running unit tests + +Run `nx test shell-tabs` to execute the unit tests. diff --git a/libs/shell/tabs/eslint.config.cjs b/libs/shell/tabs/eslint.config.cjs new file mode 100644 index 000000000..0d748b70d --- /dev/null +++ b/libs/shell/tabs/eslint.config.cjs @@ -0,0 +1,34 @@ +const nx = require('@nx/eslint-plugin'); +const baseConfig = require('../../../eslint.config.js'); + +module.exports = [ + ...baseConfig, + ...nx.configs['flat/angular'], + ...nx.configs['flat/angular-template'], + { + files: ['**/*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'shell', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'shell', + style: 'kebab-case', + }, + ], + }, + }, + { + files: ['**/*.html'], + // Override or add rules here + rules: {}, + }, +]; diff --git a/libs/shell/tabs/project.json b/libs/shell/tabs/project.json new file mode 100644 index 000000000..630686604 --- /dev/null +++ b/libs/shell/tabs/project.json @@ -0,0 +1,20 @@ +{ + "name": "shell-tabs", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "libs/shell/tabs/src", + "prefix": "shell", + "projectType": "library", + "tags": [], + "targets": { + "test": { + "executor": "@nx/vite:test", + "outputs": ["{options.reportsDirectory}"], + "options": { + "reportsDirectory": "../../../coverage/libs/shell/tabs" + } + }, + "lint": { + "executor": "@nx/eslint:lint" + } + } +} diff --git a/libs/shell/tabs/src/index.ts b/libs/shell/tabs/src/index.ts new file mode 100644 index 000000000..bab95ad96 --- /dev/null +++ b/libs/shell/tabs/src/index.ts @@ -0,0 +1 @@ +export * from './lib/shell-tabs.component'; diff --git a/libs/shell/tabs/src/lib/components/shell-tab-item.component.css b/libs/shell/tabs/src/lib/components/shell-tab-item.component.css new file mode 100644 index 000000000..2d08992ea --- /dev/null +++ b/libs/shell/tabs/src/lib/components/shell-tab-item.component.css @@ -0,0 +1,23 @@ +:host { + @apply block opacity-40; +} + +:host.active { + @apply opacity-100; +} + +a { + @apply h-14; +} + +a.compact { + @apply h-[1.625rem] items-center; +} + +button { + @apply size-6; +} + +a.compact button { + @apply size-[1.625rem]; +} diff --git a/libs/shell/tabs/src/lib/components/shell-tab-item.component.html b/libs/shell/tabs/src/lib/components/shell-tab-item.component.html new file mode 100644 index 000000000..1c88c0e18 --- /dev/null +++ b/libs/shell/tabs/src/lib/components/shell-tab-item.component.html @@ -0,0 +1,53 @@ + + + + +
+
+ {{ tab().name }} +
+ @if (!compact()) { +
+ {{ tab().subtitle }} +
+ } +
+ +
diff --git a/libs/shell/tabs/src/lib/components/shell-tab-item.component.ts b/libs/shell/tabs/src/lib/components/shell-tab-item.component.ts new file mode 100644 index 000000000..b46bdfe30 --- /dev/null +++ b/libs/shell/tabs/src/lib/components/shell-tab-item.component.ts @@ -0,0 +1,48 @@ +import { + ChangeDetectionStrategy, + Component, + input, + inject, + computed, +} from '@angular/core'; +import { Tab } from '@isa/core/tabs'; +import { NgIcon, provideIcons } from '@ng-icons/core'; +import { isaActionClose } from '@isa/icons'; +import { TabService } from '@isa/core/tabs'; +import { RouterLinkWithHref } from '@angular/router'; +import { Router } from '@angular/router'; + +@Component({ + selector: 'shell-tab-item', + templateUrl: './shell-tab-item.component.html', + styleUrls: ['./shell-tab-item.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + imports: [NgIcon, RouterLinkWithHref], + providers: [provideIcons({ isaActionClose })], + host: { + '[class.active]': 'active()', + }, +}) +export class ShellTabItemComponent { + #router = inject(Router); + #tabService = inject(TabService); + + tab = input.required(); + + compact = input(); + + active = computed(() => { + const activeId = this.#tabService.activatedTabId(); + return activeId === this.tab().id; + }); + + route = computed(() => { + const tab = this.tab(); + return tab.location.locations[tab.location.current]; + }); + + async close() { + await this.#router.navigateByUrl('/'); // Navigate away before closing to avoid errors + this.#tabService.removeTab(this.tab().id); + } +} diff --git a/libs/shell/tabs/src/lib/shell-tabs.component.css b/libs/shell/tabs/src/lib/shell-tabs.component.css new file mode 100644 index 000000000..94410c759 --- /dev/null +++ b/libs/shell/tabs/src/lib/shell-tabs.component.css @@ -0,0 +1,3 @@ +:host { + @apply block; +} diff --git a/libs/shell/tabs/src/lib/shell-tabs.component.html b/libs/shell/tabs/src/lib/shell-tabs.component.html new file mode 100644 index 000000000..650ad95aa --- /dev/null +++ b/libs/shell/tabs/src/lib/shell-tabs.component.html @@ -0,0 +1,5 @@ + + @for (tab of tabs(); track tab.id) { + + } + diff --git a/libs/shell/tabs/src/lib/shell-tabs.component.ts b/libs/shell/tabs/src/lib/shell-tabs.component.ts new file mode 100644 index 000000000..1e3177275 --- /dev/null +++ b/libs/shell/tabs/src/lib/shell-tabs.component.ts @@ -0,0 +1,43 @@ +import { Component, inject, computed, signal, ElementRef } from '@angular/core'; +import { ShellTabItemComponent } from './components/shell-tab-item.component'; +import { TabService } from '@isa/core/tabs'; +import { CarouselComponent } from '@isa/ui/carousel'; + +const PROXIMITY_THRESHOLD_PX = 50; + +@Component({ + selector: 'shell-tabs', + imports: [ShellTabItemComponent, CarouselComponent], + templateUrl: './shell-tabs.component.html', + styleUrl: './shell-tabs.component.css', + host: { + '(document:mousemove)': 'onMouseMove($event)', + }, +}) +export class ShellTabsComponent { + #tabService = inject(TabService); + #elementRef = inject(ElementRef); + + readonly tabs = this.#tabService.entities; + + #isNear = signal(false); + + compact = computed(() => !this.#isNear()); + + onMouseMove(event: MouseEvent): void { + const rect = this.#elementRef.nativeElement.getBoundingClientRect(); + const mouseY = event.clientY; + const mouseX = event.clientX; + + const isWithinX = mouseX >= rect.left && mouseX <= rect.right; + const distanceY = + mouseY < rect.top + ? rect.top - mouseY + : mouseY > rect.bottom + ? mouseY - rect.bottom + : 0; + + const isNear = isWithinX && distanceY <= PROXIMITY_THRESHOLD_PX; + this.#isNear.set(isNear); + } +} diff --git a/libs/shell/tabs/src/test-setup.ts b/libs/shell/tabs/src/test-setup.ts new file mode 100644 index 000000000..cebf5ae72 --- /dev/null +++ b/libs/shell/tabs/src/test-setup.ts @@ -0,0 +1,13 @@ +import '@angular/compiler'; +import '@analogjs/vitest-angular/setup-zone'; + +import { + BrowserTestingModule, + platformBrowserTesting, +} from '@angular/platform-browser/testing'; +import { getTestBed } from '@angular/core/testing'; + +getTestBed().initTestEnvironment( + BrowserTestingModule, + platformBrowserTesting(), +); diff --git a/libs/shell/tabs/tsconfig.json b/libs/shell/tabs/tsconfig.json new file mode 100644 index 000000000..3268ed4dc --- /dev/null +++ b/libs/shell/tabs/tsconfig.json @@ -0,0 +1,30 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "importHelpers": true, + "moduleResolution": "bundler", + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "module": "preserve" + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "typeCheckHostBindings": true, + "strictTemplates": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/libs/shell/tabs/tsconfig.lib.json b/libs/shell/tabs/tsconfig.lib.json new file mode 100644 index 000000000..312ee86bb --- /dev/null +++ b/libs/shell/tabs/tsconfig.lib.json @@ -0,0 +1,27 @@ +{ + "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", + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx" + ], + "include": ["src/**/*.ts"] +} diff --git a/libs/shell/tabs/tsconfig.spec.json b/libs/shell/tabs/tsconfig.spec.json new file mode 100644 index 000000000..5785a8a5f --- /dev/null +++ b/libs/shell/tabs/tsconfig.spec.json @@ -0,0 +1,29 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ] + }, + "include": [ + "vite.config.ts", + "vite.config.mts", + "vitest.config.ts", + "vitest.config.mts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ], + "files": ["src/test-setup.ts"] +} diff --git a/libs/shell/tabs/vite.config.mts b/libs/shell/tabs/vite.config.mts new file mode 100644 index 000000000..e9b5f10ba --- /dev/null +++ b/libs/shell/tabs/vite.config.mts @@ -0,0 +1,27 @@ +/// +import { defineConfig } from 'vite'; +import angular from '@analogjs/vite-plugin-angular'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; +import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'; + +export default defineConfig(() => ({ + root: __dirname, + cacheDir: '../../../node_modules/.vite/libs/shell/tabs', + plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])], + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + test: { + watch: false, + globals: true, + environment: 'jsdom', + include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + setupFiles: ['src/test-setup.ts'], + reporters: ['default'], + coverage: { + reportsDirectory: '../../../coverage/libs/shell/tabs', + provider: 'v8' as const, + }, + }, +})); diff --git a/tsconfig.base.json b/tsconfig.base.json index 49e561422..c7455f52c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -150,6 +150,7 @@ "@isa/shell/layout": ["libs/shell/layout/src/index.ts"], "@isa/shell/navigation": ["libs/shell/navigation/src/index.ts"], "@isa/shell/notifications": ["libs/shell/notifications/src/index.ts"], + "@isa/shell/tabs": ["libs/shell/tabs/src/index.ts"], "@isa/ui/bullet-list": ["libs/ui/bullet-list/src/index.ts"], "@isa/ui/buttons": ["libs/ui/buttons/src/index.ts"], "@isa/ui/carousel": ["libs/ui/carousel/src/index.ts"],