From dafdea54adfc1e5dfe11a0a779cd70e52d7f40bb Mon Sep 17 00:00:00 2001 From: Simon Mumenthaler Date: Fri, 3 Jul 2026 21:17:39 +0200 Subject: [PATCH 1/7] docs: add testing fab domain glossary Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CONTEXT.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 CONTEXT.md diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..46a74f7 --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,33 @@ +# SC NG Commons + +Shared Library concepts for reusable frontend capabilities used by Consumer applications. + +## Language + +**Testing FAB**: +A floating action button for non-production environments that reveals configurable test and development controls. +_Avoid_: debug menu, dev button, toolbox + +**Action Widget**: +A configurable control rendered inside the expanded Testing FAB panel to alter app behavior for testing and development. +_Avoid_: option, toggle, tool + +**Select Query Param Widget**: +An Action Widget that presents selectable values and persists the selected value into a specific URL query parameter. +_Avoid_: query toggle, url switch + +**Custom Component Widget**: +An Action Widget that renders a Consumer-provided Angular component inside the Testing FAB panel. +_Avoid_: arbitrary template, inline html widget + +**URL-Authoritative Selection**: +For Select Query Param Widgets, the URL query parameter value is the source of truth for current selection and restoration across reloads. +_Avoid_: local-only state, hidden persistence + +**Opt-in Provisioning**: +The Testing FAB is activated only when a Consumer application explicitly registers it through `provideTestingFab`; there is no automatic environment-based enablement. +_Avoid_: auto-enable, implicit activation + +**Consumer-Owned Activation Safety**: +The Consumer application is solely responsible for deciding where Testing FAB is enabled, including preventing production activation. +_Avoid_: library-enforced safety, implicit prod blocking From d25e4f0231bc8ecc6f61efec5224406f83137776 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Fri, 3 Jul 2026 19:19:26 +0000 Subject: [PATCH 2/7] build(release): next version [skip_build] --- apps/styleguide/package.json | 2 +- lerna.json | 2 +- libs/components/package.json | 2 +- libs/core/package.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/styleguide/package.json b/apps/styleguide/package.json index d908644..a5898d6 100644 --- a/apps/styleguide/package.json +++ b/apps/styleguide/package.json @@ -1,6 +1,6 @@ { "name": "@shiftcode/styleguide", - "version": "15.2.0", + "version": "15.2.1-pr87.0", "private": true, "type": "module", "scripts": { diff --git a/lerna.json b/lerna.json index b15d307..39fd91d 100644 --- a/lerna.json +++ b/lerna.json @@ -2,7 +2,7 @@ "$schema": "node_modules/lerna/schemas/lerna-schema.json", "useNx": false, "packages": ["libs/*", "apps/*"], - "version": "15.2.0", + "version": "15.2.1-pr87.0", "command": { "version": { "allowBranch": "*", diff --git a/libs/components/package.json b/libs/components/package.json index 8ca23c2..1fba528 100644 --- a/libs/components/package.json +++ b/libs/components/package.json @@ -1,6 +1,6 @@ { "name": "@shiftcode/ngx-components", - "version": "15.2.0", + "version": "15.2.1-pr87.0", "repository": "https://github.com/shiftcode/sc-ng-commons-public", "license": "MIT", "author": "shiftcode GmbH ", diff --git a/libs/core/package.json b/libs/core/package.json index 27305d4..fd895a3 100644 --- a/libs/core/package.json +++ b/libs/core/package.json @@ -1,6 +1,6 @@ { "name": "@shiftcode/ngx-core", - "version": "15.2.0", + "version": "15.2.1-pr87.0", "repository": "https://github.com/shiftcode/sc-ng-commons-public", "license": "MIT", "author": "shiftcode GmbH ", From b9b75123cf72c743b9d72fc37e96e1a7686793f5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Jul 2026 19:25:17 +0000 Subject: [PATCH 3/7] feat(components): implement configurable testing fab widgets --- CONTEXT.md | 4 +- libs/components/README.md | 27 ++++ .../lib/testing-fab/provide-testing-fab.ts | 8 + .../testing-fab/testing-fab-config.token.ts | 7 + .../testing-fab/testing-fab-widget.type.ts | 31 ++++ .../testing-fab/testing-fab.component.scss | 68 +++++++++ .../testing-fab/testing-fab.component.spec.ts | 139 ++++++++++++++++++ .../lib/testing-fab/testing-fab.component.ts | 115 +++++++++++++++ libs/components/src/public-api.ts | 6 + 9 files changed, 403 insertions(+), 2 deletions(-) create mode 100644 libs/components/src/lib/testing-fab/provide-testing-fab.ts create mode 100644 libs/components/src/lib/testing-fab/testing-fab-config.token.ts create mode 100644 libs/components/src/lib/testing-fab/testing-fab-widget.type.ts create mode 100644 libs/components/src/lib/testing-fab/testing-fab.component.scss create mode 100644 libs/components/src/lib/testing-fab/testing-fab.component.spec.ts create mode 100644 libs/components/src/lib/testing-fab/testing-fab.component.ts diff --git a/CONTEXT.md b/CONTEXT.md index 46a74f7..350da47 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -14,11 +14,11 @@ _Avoid_: option, toggle, tool **Select Query Param Widget**: An Action Widget that presents selectable values and persists the selected value into a specific URL query parameter. -_Avoid_: query toggle, url switch +_Avoid_: query toggle, URL switch **Custom Component Widget**: An Action Widget that renders a Consumer-provided Angular component inside the Testing FAB panel. -_Avoid_: arbitrary template, inline html widget +_Avoid_: arbitrary template, inline HTML widget **URL-Authoritative Selection**: For Select Query Param Widgets, the URL query parameter value is the source of truth for current selection and restoration across reloads. diff --git a/libs/components/README.md b/libs/components/README.md index d21366d..1684714 100644 --- a/libs/components/README.md +++ b/libs/components/README.md @@ -74,3 +74,30 @@ Can be customized with css custom properties: --sc-tooltip-padding: 4px 8px; } ``` + +## [testing-fab](./src/lib/testing-fab/testing-fab.component.ts) + +Floating action button for non-production testing controls. + +```ts +import { provideTestingFab, TestingFabComponent } from '@shiftcode/ngx-components' + +bootstrapApplication(AppComponent, { + providers: [ + provideTestingFab([ + { + id: 'backend-env', + label: 'Backend Environment', + type: 'select-query-param', + queryParam: 'env', + options: [ + { label: 'QA', value: 'qa' }, + { label: 'Preview', value: 'preview' }, + ], + }, + ]), + ], +}) +``` + +Add `` once in a root-level template where it should be available. diff --git a/libs/components/src/lib/testing-fab/provide-testing-fab.ts b/libs/components/src/lib/testing-fab/provide-testing-fab.ts new file mode 100644 index 0000000..4dc5a52 --- /dev/null +++ b/libs/components/src/lib/testing-fab/provide-testing-fab.ts @@ -0,0 +1,8 @@ +import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core' + +import { TESTING_FAB_WIDGETS } from './testing-fab-config.token' +import { TestingFabWidget } from './testing-fab-widget.type' + +export function provideTestingFab(widgets: readonly TestingFabWidget[]): EnvironmentProviders { + return makeEnvironmentProviders([{ provide: TESTING_FAB_WIDGETS, useValue: widgets }]) +} diff --git a/libs/components/src/lib/testing-fab/testing-fab-config.token.ts b/libs/components/src/lib/testing-fab/testing-fab-config.token.ts new file mode 100644 index 0000000..2467fd8 --- /dev/null +++ b/libs/components/src/lib/testing-fab/testing-fab-config.token.ts @@ -0,0 +1,7 @@ +import { InjectionToken } from '@angular/core' + +import { TestingFabWidget } from './testing-fab-widget.type' + +export const TESTING_FAB_WIDGETS = new InjectionToken('sc.ng.common.testing-fab.widgets', { + factory: () => [], +}) diff --git a/libs/components/src/lib/testing-fab/testing-fab-widget.type.ts b/libs/components/src/lib/testing-fab/testing-fab-widget.type.ts new file mode 100644 index 0000000..b93c4b3 --- /dev/null +++ b/libs/components/src/lib/testing-fab/testing-fab-widget.type.ts @@ -0,0 +1,31 @@ +import { Type } from '@angular/core' + +export type TestingFabWidget = TestingFabActionWidget | TestingFabSelectQueryParamWidget | TestingFabCustomComponentWidget + +interface TestingFabWidgetBase { + id: string + label: string +} + +export interface TestingFabActionWidget extends TestingFabWidgetBase { + type: 'action' + buttonLabel?: string + action: () => void +} + +export interface TestingFabSelectQueryParamWidget extends TestingFabWidgetBase { + type: 'select-query-param' + queryParam: string + options: readonly TestingFabSelectOption[] +} + +export interface TestingFabSelectOption { + value: string + label: string +} + +export interface TestingFabCustomComponentWidget extends TestingFabWidgetBase { + type: 'custom-component' + component: Type + inputs?: Record +} diff --git a/libs/components/src/lib/testing-fab/testing-fab.component.scss b/libs/components/src/lib/testing-fab/testing-fab.component.scss new file mode 100644 index 0000000..fe9669c --- /dev/null +++ b/libs/components/src/lib/testing-fab/testing-fab.component.scss @@ -0,0 +1,68 @@ +:host { + position: fixed; + right: 16px; + bottom: 16px; + z-index: 2147483647; +} + +.sc-testing-fab { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 8px; +} + +.sc-testing-fab__trigger { + width: 48px; + height: 48px; + border-radius: 50%; + border: 0; + background: #1d4ed8; + color: #fff; + cursor: pointer; + font-size: 22px; + box-shadow: 0 2px 8px rgb(0 0 0 / 40%); +} + +.sc-testing-fab__panel { + min-width: 240px; + max-width: min(320px, calc(100vw - 32px)); + max-height: min(60vh, 560px); + overflow: auto; + border: 1px solid #d1d5db; + border-radius: 8px; + background: #fff; + color: #111827; + padding: 12px; + box-shadow: 0 8px 24px rgb(0 0 0 / 25%); +} + +.sc-testing-fab__widget { + display: grid; + gap: 8px; +} + +.sc-testing-fab__widget + .sc-testing-fab__widget { + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid #e5e7eb; +} + +.sc-testing-fab__widget-label { + font-weight: 600; + font-size: 13px; +} + +.sc-testing-fab__select, +.sc-testing-fab__button { + width: 100%; + border: 1px solid #9ca3af; + border-radius: 6px; + padding: 6px 8px; + background: #fff; + color: inherit; +} + +.sc-testing-fab__button { + cursor: pointer; +} diff --git a/libs/components/src/lib/testing-fab/testing-fab.component.spec.ts b/libs/components/src/lib/testing-fab/testing-fab.component.spec.ts new file mode 100644 index 0000000..4cb145c --- /dev/null +++ b/libs/components/src/lib/testing-fab/testing-fab.component.spec.ts @@ -0,0 +1,139 @@ +import { Component } from '@angular/core' +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { DefaultUrlSerializer, Router } from '@angular/router' +import { describe, expect, test, vi } from 'vitest' + +import { TESTING_FAB_WIDGETS } from './testing-fab-config.token' +import { TestingFabComponent } from './testing-fab.component' +import { TestingFabWidget } from './testing-fab-widget.type' + +@Component({ + selector: 'sc-test-custom-widget', + template: `
custom
`, + standalone: true, +}) +class TestCustomWidgetComponent {} + +const serializer = new DefaultUrlSerializer() + +function setup(widgets: readonly TestingFabWidget[], url = '/'): { fixture: ComponentFixture; routerMock: Router } { + const routerMock = { + url, + parseUrl: vi.fn((value) => serializer.parse(value)), + navigate: vi.fn().mockResolvedValue(true), + } as unknown as Router + + TestBed.configureTestingModule({ + imports: [TestingFabComponent], + providers: [ + { provide: TESTING_FAB_WIDGETS, useValue: widgets }, + { provide: Router, useValue: routerMock }, + ], + }) + + const fixture = TestBed.createComponent(TestingFabComponent) + fixture.detectChanges() + + return { fixture, routerMock } +} + +function openPanel(fixture: ComponentFixture): void { + const trigger = fixture.nativeElement.querySelector('.sc-testing-fab__trigger') as HTMLButtonElement + trigger.click() + fixture.detectChanges() +} + +describe('TestingFabComponent', () => { + test('renders select widget and uses URL query parameter as selected value', () => { + const widgets: readonly TestingFabWidget[] = [ + { + id: 'env', + label: 'Backend Environment', + type: 'select-query-param', + queryParam: 'env', + options: [ + { label: 'QA', value: 'qa' }, + { label: 'Prod', value: 'prod' }, + ], + }, + ] + + const { fixture } = setup(widgets, '/?env=qa') + openPanel(fixture) + + const select = fixture.nativeElement.querySelector('select') as HTMLSelectElement + expect(select.value).toBe('qa') + }) + + test('updates query parameters when select widget value changes', () => { + const widgets: readonly TestingFabWidget[] = [ + { + id: 'env', + label: 'Backend Environment', + type: 'select-query-param', + queryParam: 'env', + options: [ + { label: 'QA', value: 'qa' }, + { label: 'Prod', value: 'prod' }, + ], + }, + ] + + const { fixture, routerMock } = setup(widgets, '/?env=qa') + openPanel(fixture) + + const select = fixture.nativeElement.querySelector('select') as HTMLSelectElement + select.value = 'prod' + select.dispatchEvent(new Event('change')) + + expect(routerMock.navigate).toHaveBeenCalledWith([], { + queryParams: { env: 'prod' }, + queryParamsHandling: 'merge', + }) + + select.value = '' + select.dispatchEvent(new Event('change')) + + expect(routerMock.navigate).toHaveBeenNthCalledWith(2, [], { + queryParams: { env: null }, + queryParamsHandling: 'merge', + }) + }) + + test('renders a custom component widget', () => { + const widgets: readonly TestingFabWidget[] = [ + { + id: 'custom', + label: 'Custom Widget', + type: 'custom-component', + component: TestCustomWidgetComponent, + }, + ] + + const { fixture } = setup(widgets) + openPanel(fixture) + + const customWidget = fixture.nativeElement.querySelector('[data-test-id="custom-widget"]') + expect(customWidget).not.toBeNull() + }) + + test('runs action widgets when clicked', () => { + const action = vi.fn() + const widgets: readonly TestingFabWidget[] = [ + { + id: 'toggle-i18n', + label: 'Show i18n Keys', + type: 'action', + action, + }, + ] + + const { fixture } = setup(widgets) + openPanel(fixture) + + const button = fixture.nativeElement.querySelector('.sc-testing-fab__button') as HTMLButtonElement + button.click() + + expect(action).toHaveBeenCalledTimes(1) + }) +}) diff --git a/libs/components/src/lib/testing-fab/testing-fab.component.ts b/libs/components/src/lib/testing-fab/testing-fab.component.ts new file mode 100644 index 0000000..f0d7376 --- /dev/null +++ b/libs/components/src/lib/testing-fab/testing-fab.component.ts @@ -0,0 +1,115 @@ +import { CommonModule, NgComponentOutlet } from '@angular/common' +import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core' +import { Router } from '@angular/router' + +import { TESTING_FAB_WIDGETS } from './testing-fab-config.token' +import { + TestingFabActionWidget, + TestingFabCustomComponentWidget, + TestingFabSelectQueryParamWidget, + TestingFabWidget, +} from './testing-fab-widget.type' + +@Component({ + selector: 'sc-testing-fab', + standalone: true, + imports: [CommonModule, NgComponentOutlet], + styleUrls: ['./testing-fab.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+ + +
+ +
+ + + + + + + + + + + + + +
+
+
+
+ `, +}) +export class TestingFabComponent { + protected readonly widgets = inject(TESTING_FAB_WIDGETS) + protected readonly isOpen = signal(false) + private readonly router = inject(Router) + + protected toggle() { + this.isOpen.update((v) => !v) + } + + protected runAction(widget: TestingFabActionWidget) { + widget.action() + } + + protected getSelectedValue(widget: TestingFabSelectQueryParamWidget): string { + const queryParams = this.router.parseUrl(this.router.url).queryParams + const value = queryParams[widget.queryParam] + return typeof value === 'string' ? value : '' + } + + protected setQueryParam(widget: TestingFabSelectQueryParamWidget, event: Event) { + const nextValue = (event.target as HTMLSelectElement).value + void this.router.navigate([], { + queryParams: { + [widget.queryParam]: nextValue || null, + }, + queryParamsHandling: 'merge', + }) + } + + protected trackById(_index: number, widget: TestingFabWidget): string { + return widget.id + } + + protected asActionWidget(widget: TestingFabWidget): TestingFabActionWidget | null { + return widget.type === 'action' ? widget : null + } + + protected asSelectWidget(widget: TestingFabWidget): TestingFabSelectQueryParamWidget | null { + return widget.type === 'select-query-param' ? widget : null + } + + protected asCustomComponentWidget(widget: TestingFabWidget): TestingFabCustomComponentWidget | null { + return widget.type === 'custom-component' ? widget : null + } +} diff --git a/libs/components/src/public-api.ts b/libs/components/src/public-api.ts index 27d7064..feac234 100644 --- a/libs/components/src/public-api.ts +++ b/libs/components/src/public-api.ts @@ -40,6 +40,12 @@ export * from './lib/insert-view-ref/insert-view-ref.directive' // navigation-class-handler export * from './lib/navigation-class-handler/navigation-class-handler' +// testing fab +export * from './lib/testing-fab/testing-fab.component' +export * from './lib/testing-fab/provide-testing-fab' +export * from './lib/testing-fab/testing-fab-config.token' +export * from './lib/testing-fab/testing-fab-widget.type' + // rx export * from './lib/rx/provide-defaults' export * from './lib/rx/rx-if.directive' From 51137d0abfed8099c5bb6c628c6cf3aa178227e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Jul 2026 20:13:01 +0000 Subject: [PATCH 4/7] feat(components): apply testing-fab PR feedback --- .../testing-fab/provide-testing-fab.spec.ts | 51 +++++++ .../lib/testing-fab/provide-testing-fab.ts | 43 +++++- .../testing-fab/testing-fab-widget.type.ts | 2 +- .../testing-fab/testing-fab.component.scss | 63 ++++++--- .../testing-fab/testing-fab.component.spec.ts | 42 ++++-- .../lib/testing-fab/testing-fab.component.ts | 125 +++++++----------- 6 files changed, 218 insertions(+), 108 deletions(-) create mode 100644 libs/components/src/lib/testing-fab/provide-testing-fab.spec.ts diff --git a/libs/components/src/lib/testing-fab/provide-testing-fab.spec.ts b/libs/components/src/lib/testing-fab/provide-testing-fab.spec.ts new file mode 100644 index 0000000..78b92f0 --- /dev/null +++ b/libs/components/src/lib/testing-fab/provide-testing-fab.spec.ts @@ -0,0 +1,51 @@ +import { TestBed } from '@angular/core/testing' +import { DefaultUrlSerializer, Router } from '@angular/router' +import { afterEach, describe, expect, test, vi } from 'vitest' + +import { provideTestingFab } from './provide-testing-fab' +import { TESTING_FAB_WIDGETS } from './testing-fab-config.token' +import { TestingFabWidget } from './testing-fab-widget.type' + +const serializer = new DefaultUrlSerializer() + +function createRouterMock(): Router { + return { + url: '/', + parseUrl: vi.fn((value) => serializer.parse(value)), + navigate: vi.fn().mockResolvedValue(true), + } as unknown as Router +} + +describe('provideTestingFab', () => { + afterEach(() => { + TestBed.resetTestingModule() + document.body.querySelectorAll('sc-testing-fab').forEach((el) => el.remove()) + }) + + test('supports static widgets config and renders the testing fab', () => { + const widgets: readonly TestingFabWidget[] = [ + { id: 'a1', label: 'Action', type: 'action', action: vi.fn() }, + ] + + TestBed.configureTestingModule({ + providers: [provideTestingFab(widgets), { provide: Router, useValue: createRouterMock() }], + }) + + expect(TestBed.inject(TESTING_FAB_WIDGETS)).toEqual(widgets) + expect(document.body.querySelectorAll('sc-testing-fab')).toHaveLength(1) + }) + + test('supports factory widgets config', () => { + const widgets: readonly TestingFabWidget[] = [ + { id: 'a1', label: 'Action', type: 'action', action: vi.fn() }, + ] + const factory = vi.fn(() => widgets) + + TestBed.configureTestingModule({ + providers: [provideTestingFab(factory), { provide: Router, useValue: createRouterMock() }], + }) + + expect(TestBed.inject(TESTING_FAB_WIDGETS)).toEqual(widgets) + expect(factory).toHaveBeenCalled() + }) +}) diff --git a/libs/components/src/lib/testing-fab/provide-testing-fab.ts b/libs/components/src/lib/testing-fab/provide-testing-fab.ts index 4dc5a52..cadb9fd 100644 --- a/libs/components/src/lib/testing-fab/provide-testing-fab.ts +++ b/libs/components/src/lib/testing-fab/provide-testing-fab.ts @@ -1,8 +1,45 @@ -import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core' +import { DOCUMENT } from '@angular/common' +import { + ApplicationRef, + createComponent, + EnvironmentInjector, + EnvironmentProviders, + inject, + makeEnvironmentProviders, + provideEnvironmentInitializer, +} from '@angular/core' import { TESTING_FAB_WIDGETS } from './testing-fab-config.token' import { TestingFabWidget } from './testing-fab-widget.type' +import { TestingFabComponent } from './testing-fab.component' -export function provideTestingFab(widgets: readonly TestingFabWidget[]): EnvironmentProviders { - return makeEnvironmentProviders([{ provide: TESTING_FAB_WIDGETS, useValue: widgets }]) +type ValueOrFactory = T | (() => T) + +export function provideTestingFab(widgets: ValueOrFactory): EnvironmentProviders { + return makeEnvironmentProviders([ + { + provide: TESTING_FAB_WIDGETS, + ...(typeof widgets === 'function' ? { useFactory: widgets } : { useValue: widgets }), + }, + provideEnvironmentInitializer(() => initializeTestingFab()), + ]) +} + +export function initializeTestingFab() { + const doc = inject(DOCUMENT) + + if (!doc.body || doc.body.querySelector('sc-testing-fab')) { + return + } + + const appRef = inject(ApplicationRef) + const environmentInjector = inject(EnvironmentInjector) + const host = doc.createElement('sc-testing-fab') + const cmpRef = createComponent(TestingFabComponent, { + hostElement: host, + environmentInjector, + }) + + appRef.attachView(cmpRef.hostView) + doc.body.appendChild(host) } diff --git a/libs/components/src/lib/testing-fab/testing-fab-widget.type.ts b/libs/components/src/lib/testing-fab/testing-fab-widget.type.ts index b93c4b3..7e7db59 100644 --- a/libs/components/src/lib/testing-fab/testing-fab-widget.type.ts +++ b/libs/components/src/lib/testing-fab/testing-fab-widget.type.ts @@ -17,6 +17,7 @@ export interface TestingFabSelectQueryParamWidget extends TestingFabWidgetBase { type: 'select-query-param' queryParam: string options: readonly TestingFabSelectOption[] + hardReload?: boolean } export interface TestingFabSelectOption { @@ -27,5 +28,4 @@ export interface TestingFabSelectOption { export interface TestingFabCustomComponentWidget extends TestingFabWidgetBase { type: 'custom-component' component: Type - inputs?: Record } diff --git a/libs/components/src/lib/testing-fab/testing-fab.component.scss b/libs/components/src/lib/testing-fab/testing-fab.component.scss index fe9669c..a4b4d12 100644 --- a/libs/components/src/lib/testing-fab/testing-fab.component.scss +++ b/libs/components/src/lib/testing-fab/testing-fab.component.scss @@ -1,30 +1,36 @@ :host { position: fixed; - right: 16px; - bottom: 16px; + right: var(--sc-testing-fab-inset, 16px); + bottom: var(--sc-testing-fab-inset, 16px); + margin: var(--sc-testing-fab-margin, 0); z-index: 2147483647; -} - -.sc-testing-fab { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 8px; + --sc-testing-fab-size: 48px; + --sc-testing-fab-border-radius: 50%; + --sc-testing-fab-background-color: #1d4ed8; + --sc-testing-fab-foreground-color: #fff; + --sc-testing-fab-icon: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PCFET0NUWVBFIHN2ZyBQVUJMSUMgIi0vL1czQy8vRFREIFNWRyAxLjEvL0VOIiAiaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkIj48c3ZnIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIHZpZXdCb3g9IjAgMCA0NDIgNTc5IiB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHhtbDpzcGFjZT0icHJlc2VydmUiIHhtbG5zOnNlcmlmPSJodHRwOi8vd3d3LnNlcmlmLmNvbS8iIHN0eWxlPSJmaWxsLXJ1bGU6ZXZlbm9kZDtjbGlwLXJ1bGU6ZXZlbm9kZDtzdHJva2UtbGluZWpvaW46cm91bmQ7c3Ryb2tlLW1pdGVybGltaXQ6MjsiPjxwYXRoIGQ9Ik0wLDE5Mi45MTdsMTkyLjkxNywtMTkyLjkxN2w5Ni4yNSwwbC0xOTIuOTE3LDE5Mi45MTdsMTkyLjkxNywxOTIuNWwtMTkyLjkxNywxOTIuOTE2bC05Ni4yNSwwbDE5Mi45MTcsLTE5Mi45MTZsLTE5Mi45MTcsLTE5Mi41WiIvPjxwYXRoIGQ9Ik0xNTIuNSwxOTIuOTE3bDE5Mi41LC0xOTIuOTE3bDk2LjY2NywwbC0xOTIuOTE3LDE5Mi45MTdsMTkyLjkxNywxOTIuNWwtOTYuNjY3LDBsLTE5Mi41LC0xOTIuNVoiLz48L3N2Zz4='); } .sc-testing-fab__trigger { - width: 48px; - height: 48px; - border-radius: 50%; + width: var(--sc-testing-fab-size); + height: var(--sc-testing-fab-size); + border-radius: var(--sc-testing-fab-border-radius); border: 0; - background: #1d4ed8; - color: #fff; + background: var(--sc-testing-fab-background-color); + color: var(--sc-testing-fab-foreground-color); cursor: pointer; - font-size: 22px; + display: inline-flex; + align-items: center; + justify-content: center; box-shadow: 0 2px 8px rgb(0 0 0 / 40%); } .sc-testing-fab__panel { + position: fixed; + right: var(--sc-testing-fab-inset, 16px); + bottom: calc(var(--sc-testing-fab-inset, 16px) + var(--sc-testing-fab-size) + 8px); + margin: var(--sc-testing-fab-margin, 0); + inset: auto; min-width: 240px; max-width: min(320px, calc(100vw - 32px)); max-height: min(60vh, 560px); @@ -35,6 +41,23 @@ color: #111827; padding: 12px; box-shadow: 0 8px 24px rgb(0 0 0 / 25%); + opacity: 0; + transform: translateY(8px) scale(0.98); + transition: + opacity 140ms ease, + transform 140ms ease; +} + +.sc-testing-fab__panel:popover-open { + opacity: 1; + transform: translateY(0) scale(1); +} + +@starting-style { + .sc-testing-fab__panel:popover-open { + opacity: 0; + transform: translateY(8px) scale(0.98); + } } .sc-testing-fab__widget { @@ -66,3 +89,13 @@ .sc-testing-fab__button { cursor: pointer; } + +.sc-testing-fab__trigger::before { + content: ''; + width: 62%; + height: 62%; + background-image: var(--sc-testing-fab-icon); + background-size: contain; + background-repeat: no-repeat; + background-position: center; +} diff --git a/libs/components/src/lib/testing-fab/testing-fab.component.spec.ts b/libs/components/src/lib/testing-fab/testing-fab.component.spec.ts index 4cb145c..cb41416 100644 --- a/libs/components/src/lib/testing-fab/testing-fab.component.spec.ts +++ b/libs/components/src/lib/testing-fab/testing-fab.component.spec.ts @@ -37,12 +37,6 @@ function setup(widgets: readonly TestingFabWidget[], url = '/'): { fixture: Comp return { fixture, routerMock } } -function openPanel(fixture: ComponentFixture): void { - const trigger = fixture.nativeElement.querySelector('.sc-testing-fab__trigger') as HTMLButtonElement - trigger.click() - fixture.detectChanges() -} - describe('TestingFabComponent', () => { test('renders select widget and uses URL query parameter as selected value', () => { const widgets: readonly TestingFabWidget[] = [ @@ -59,8 +53,6 @@ describe('TestingFabComponent', () => { ] const { fixture } = setup(widgets, '/?env=qa') - openPanel(fixture) - const select = fixture.nativeElement.querySelector('select') as HTMLSelectElement expect(select.value).toBe('qa') }) @@ -80,8 +72,6 @@ describe('TestingFabComponent', () => { ] const { fixture, routerMock } = setup(widgets, '/?env=qa') - openPanel(fixture) - const select = fixture.nativeElement.querySelector('select') as HTMLSelectElement select.value = 'prod' select.dispatchEvent(new Event('change')) @@ -100,6 +90,34 @@ describe('TestingFabComponent', () => { }) }) + test('forces hard reload when configured on select widget', () => { + const widgets: readonly TestingFabWidget[] = [ + { + id: 'env', + label: 'Backend Environment', + type: 'select-query-param', + queryParam: 'env', + hardReload: true, + options: [ + { label: 'QA', value: 'qa' }, + { label: 'Prod', value: 'prod' }, + ], + }, + ] + + const locationAssignSpy = vi.spyOn(window.location, 'assign').mockImplementation(() => undefined) + const { fixture, routerMock } = setup(widgets, '/?env=qa') + const select = fixture.nativeElement.querySelector('select') as HTMLSelectElement + select.value = 'prod' + select.dispatchEvent(new Event('change')) + + expect(locationAssignSpy).toHaveBeenCalledTimes(1) + expect(locationAssignSpy).toHaveBeenCalledWith(expect.stringContaining('env=prod')) + expect(routerMock.navigate).not.toHaveBeenCalled() + + locationAssignSpy.mockRestore() + }) + test('renders a custom component widget', () => { const widgets: readonly TestingFabWidget[] = [ { @@ -111,8 +129,6 @@ describe('TestingFabComponent', () => { ] const { fixture } = setup(widgets) - openPanel(fixture) - const customWidget = fixture.nativeElement.querySelector('[data-test-id="custom-widget"]') expect(customWidget).not.toBeNull() }) @@ -129,8 +145,6 @@ describe('TestingFabComponent', () => { ] const { fixture } = setup(widgets) - openPanel(fixture) - const button = fixture.nativeElement.querySelector('.sc-testing-fab__button') as HTMLButtonElement button.click() diff --git a/libs/components/src/lib/testing-fab/testing-fab.component.ts b/libs/components/src/lib/testing-fab/testing-fab.component.ts index f0d7376..f4ddff8 100644 --- a/libs/components/src/lib/testing-fab/testing-fab.component.ts +++ b/libs/components/src/lib/testing-fab/testing-fab.component.ts @@ -1,86 +1,64 @@ -import { CommonModule, NgComponentOutlet } from '@angular/common' -import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core' +import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { Router } from '@angular/router' +import { NgComponentOutlet } from '@angular/common' import { TESTING_FAB_WIDGETS } from './testing-fab-config.token' -import { - TestingFabActionWidget, - TestingFabCustomComponentWidget, - TestingFabSelectQueryParamWidget, - TestingFabWidget, -} from './testing-fab-widget.type' +import { TestingFabSelectQueryParamWidget } from './testing-fab-widget.type' @Component({ selector: 'sc-testing-fab', standalone: true, - imports: [CommonModule, NgComponentOutlet], + imports: [NgComponentOutlet], styleUrls: ['./testing-fab.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, template: ` -
- + @let panelId = 'sc-testing-fab-panel'; + -
- -
- +
+ @for (widget of widgets; track widget.id) { + @let widgetId = 'sc-testing-fab-' + widget.id; +
+ - - - - - + } + @case ('select-query-param') { - - - - - -
- -
-
+ } + @case ('custom-component') { + + } + } +
+ } + `, }) export class TestingFabComponent { protected readonly widgets = inject(TESTING_FAB_WIDGETS) - protected readonly isOpen = signal(false) private readonly router = inject(Router) - protected toggle() { - this.isOpen.update((v) => !v) - } - - protected runAction(widget: TestingFabActionWidget) { - widget.action() - } - protected getSelectedValue(widget: TestingFabSelectQueryParamWidget): string { const queryParams = this.router.parseUrl(this.router.url).queryParams const value = queryParams[widget.queryParam] @@ -89,6 +67,19 @@ export class TestingFabComponent { protected setQueryParam(widget: TestingFabSelectQueryParamWidget, event: Event) { const nextValue = (event.target as HTMLSelectElement).value + + if (widget.hardReload) { + const url = new URL(window.location.href) + if (nextValue) { + url.searchParams.set(widget.queryParam, nextValue) + } else { + url.searchParams.delete(widget.queryParam) + } + + window.location.assign(url.toString()) + return + } + void this.router.navigate([], { queryParams: { [widget.queryParam]: nextValue || null, @@ -96,20 +87,4 @@ export class TestingFabComponent { queryParamsHandling: 'merge', }) } - - protected trackById(_index: number, widget: TestingFabWidget): string { - return widget.id - } - - protected asActionWidget(widget: TestingFabWidget): TestingFabActionWidget | null { - return widget.type === 'action' ? widget : null - } - - protected asSelectWidget(widget: TestingFabWidget): TestingFabSelectQueryParamWidget | null { - return widget.type === 'select-query-param' ? widget : null - } - - protected asCustomComponentWidget(widget: TestingFabWidget): TestingFabCustomComponentWidget | null { - return widget.type === 'custom-component' ? widget : null - } } From 3b41721d048d63d1f9d07e4ba12acff7dce6a9ba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Jul 2026 20:40:13 +0000 Subject: [PATCH 5/7] refactor(components): split testing-fab template and initializer --- .../lib/testing-fab/initialize-testing-fab.ts | 23 ++++++++++ .../lib/testing-fab/provide-testing-fab.ts | 32 +------------- .../testing-fab/testing-fab.component.html | 36 +++++++++++++++ .../lib/testing-fab/testing-fab.component.ts | 44 +------------------ 4 files changed, 62 insertions(+), 73 deletions(-) create mode 100644 libs/components/src/lib/testing-fab/initialize-testing-fab.ts create mode 100644 libs/components/src/lib/testing-fab/testing-fab.component.html diff --git a/libs/components/src/lib/testing-fab/initialize-testing-fab.ts b/libs/components/src/lib/testing-fab/initialize-testing-fab.ts new file mode 100644 index 0000000..6ffff1f --- /dev/null +++ b/libs/components/src/lib/testing-fab/initialize-testing-fab.ts @@ -0,0 +1,23 @@ +import { DOCUMENT } from '@angular/common' +import { ApplicationRef, createComponent, EnvironmentInjector, inject } from '@angular/core' + +import { TestingFabComponent } from './testing-fab.component' + +export function initializeTestingFab() { + const doc = inject(DOCUMENT) + + if (!doc.body || doc.body.querySelector('sc-testing-fab')) { + return + } + + const appRef = inject(ApplicationRef) + const environmentInjector = inject(EnvironmentInjector) + const host = doc.createElement('sc-testing-fab') + const cmpRef = createComponent(TestingFabComponent, { + hostElement: host, + environmentInjector, + }) + + appRef.attachView(cmpRef.hostView) + doc.body.appendChild(host) +} diff --git a/libs/components/src/lib/testing-fab/provide-testing-fab.ts b/libs/components/src/lib/testing-fab/provide-testing-fab.ts index cadb9fd..d589eff 100644 --- a/libs/components/src/lib/testing-fab/provide-testing-fab.ts +++ b/libs/components/src/lib/testing-fab/provide-testing-fab.ts @@ -1,17 +1,8 @@ -import { DOCUMENT } from '@angular/common' -import { - ApplicationRef, - createComponent, - EnvironmentInjector, - EnvironmentProviders, - inject, - makeEnvironmentProviders, - provideEnvironmentInitializer, -} from '@angular/core' +import { EnvironmentProviders, makeEnvironmentProviders, provideEnvironmentInitializer } from '@angular/core' +import { initializeTestingFab } from './initialize-testing-fab' import { TESTING_FAB_WIDGETS } from './testing-fab-config.token' import { TestingFabWidget } from './testing-fab-widget.type' -import { TestingFabComponent } from './testing-fab.component' type ValueOrFactory = T | (() => T) @@ -24,22 +15,3 @@ export function provideTestingFab(widgets: ValueOrFactory initializeTestingFab()), ]) } - -export function initializeTestingFab() { - const doc = inject(DOCUMENT) - - if (!doc.body || doc.body.querySelector('sc-testing-fab')) { - return - } - - const appRef = inject(ApplicationRef) - const environmentInjector = inject(EnvironmentInjector) - const host = doc.createElement('sc-testing-fab') - const cmpRef = createComponent(TestingFabComponent, { - hostElement: host, - environmentInjector, - }) - - appRef.attachView(cmpRef.hostView) - doc.body.appendChild(host) -} diff --git a/libs/components/src/lib/testing-fab/testing-fab.component.html b/libs/components/src/lib/testing-fab/testing-fab.component.html new file mode 100644 index 0000000..54fac75 --- /dev/null +++ b/libs/components/src/lib/testing-fab/testing-fab.component.html @@ -0,0 +1,36 @@ +@let panelId = 'sc-testing-fab-panel'; + + +
+ @for (widget of widgets; track widget.id) { + @let widgetId = 'sc-testing-fab-' + widget.id; +
+ + + @switch (widget.type) { + @case ('action') { + + } + @case ('select-query-param') { + + } + @case ('custom-component') { + + } + } +
+ } +
diff --git a/libs/components/src/lib/testing-fab/testing-fab.component.ts b/libs/components/src/lib/testing-fab/testing-fab.component.ts index f4ddff8..cd4a9c2 100644 --- a/libs/components/src/lib/testing-fab/testing-fab.component.ts +++ b/libs/components/src/lib/testing-fab/testing-fab.component.ts @@ -11,49 +11,7 @@ import { TestingFabSelectQueryParamWidget } from './testing-fab-widget.type' imports: [NgComponentOutlet], styleUrls: ['./testing-fab.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - template: ` - @let panelId = 'sc-testing-fab-panel'; - - -
- @for (widget of widgets; track widget.id) { - @let widgetId = 'sc-testing-fab-' + widget.id; -
- - - @switch (widget.type) { - @case ('action') { - - } - @case ('select-query-param') { - - } - @case ('custom-component') { - - } - } -
- } -
- `, + templateUrl: './testing-fab.component.html', }) export class TestingFabComponent { protected readonly widgets = inject(TESTING_FAB_WIDGETS) From d9d98acf2608375e9a4b6af1f74183b466c42bb5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Jul 2026 21:06:50 +0000 Subject: [PATCH 6/7] style(components): format testing-fab files with prettier --- .../src/lib/testing-fab/provide-testing-fab.spec.ts | 8 ++------ .../src/lib/testing-fab/testing-fab-widget.type.ts | 5 ++++- .../src/lib/testing-fab/testing-fab.component.html | 7 ++++++- .../src/lib/testing-fab/testing-fab.component.spec.ts | 5 ++++- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/libs/components/src/lib/testing-fab/provide-testing-fab.spec.ts b/libs/components/src/lib/testing-fab/provide-testing-fab.spec.ts index 78b92f0..e5c764b 100644 --- a/libs/components/src/lib/testing-fab/provide-testing-fab.spec.ts +++ b/libs/components/src/lib/testing-fab/provide-testing-fab.spec.ts @@ -23,9 +23,7 @@ describe('provideTestingFab', () => { }) test('supports static widgets config and renders the testing fab', () => { - const widgets: readonly TestingFabWidget[] = [ - { id: 'a1', label: 'Action', type: 'action', action: vi.fn() }, - ] + const widgets: readonly TestingFabWidget[] = [{ id: 'a1', label: 'Action', type: 'action', action: vi.fn() }] TestBed.configureTestingModule({ providers: [provideTestingFab(widgets), { provide: Router, useValue: createRouterMock() }], @@ -36,9 +34,7 @@ describe('provideTestingFab', () => { }) test('supports factory widgets config', () => { - const widgets: readonly TestingFabWidget[] = [ - { id: 'a1', label: 'Action', type: 'action', action: vi.fn() }, - ] + const widgets: readonly TestingFabWidget[] = [{ id: 'a1', label: 'Action', type: 'action', action: vi.fn() }] const factory = vi.fn(() => widgets) TestBed.configureTestingModule({ diff --git a/libs/components/src/lib/testing-fab/testing-fab-widget.type.ts b/libs/components/src/lib/testing-fab/testing-fab-widget.type.ts index 7e7db59..3fb430c 100644 --- a/libs/components/src/lib/testing-fab/testing-fab-widget.type.ts +++ b/libs/components/src/lib/testing-fab/testing-fab-widget.type.ts @@ -1,6 +1,9 @@ import { Type } from '@angular/core' -export type TestingFabWidget = TestingFabActionWidget | TestingFabSelectQueryParamWidget | TestingFabCustomComponentWidget +export type TestingFabWidget = + | TestingFabActionWidget + | TestingFabSelectQueryParamWidget + | TestingFabCustomComponentWidget interface TestingFabWidgetBase { id: string diff --git a/libs/components/src/lib/testing-fab/testing-fab.component.html b/libs/components/src/lib/testing-fab/testing-fab.component.html index 54fac75..cedfbcd 100644 --- a/libs/components/src/lib/testing-fab/testing-fab.component.html +++ b/libs/components/src/lib/testing-fab/testing-fab.component.html @@ -20,7 +20,12 @@ } @case ('select-query-param') { - @for (option of widget.options; track option.value) { diff --git a/libs/components/src/lib/testing-fab/testing-fab.component.spec.ts b/libs/components/src/lib/testing-fab/testing-fab.component.spec.ts index cb41416..88e2fa2 100644 --- a/libs/components/src/lib/testing-fab/testing-fab.component.spec.ts +++ b/libs/components/src/lib/testing-fab/testing-fab.component.spec.ts @@ -16,7 +16,10 @@ class TestCustomWidgetComponent {} const serializer = new DefaultUrlSerializer() -function setup(widgets: readonly TestingFabWidget[], url = '/'): { fixture: ComponentFixture; routerMock: Router } { +function setup( + widgets: readonly TestingFabWidget[], + url = '/', +): { fixture: ComponentFixture; routerMock: Router } { const routerMock = { url, parseUrl: vi.fn((value) => serializer.parse(value)), From d24394254e760a7fedf997463c8f8eb9904e5c52 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Jul 2026 21:23:44 +0000 Subject: [PATCH 7/7] fix(components): resolve testing-fab lint violations --- .../testing-fab/testing-fab.component.html | 2 +- .../testing-fab/testing-fab.component.spec.ts | 25 +++++++++++++------ libs/components/src/public-api.ts | 2 +- 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/libs/components/src/lib/testing-fab/testing-fab.component.html b/libs/components/src/lib/testing-fab/testing-fab.component.html index cedfbcd..86bfe5a 100644 --- a/libs/components/src/lib/testing-fab/testing-fab.component.html +++ b/libs/components/src/lib/testing-fab/testing-fab.component.html @@ -33,7 +33,7 @@ } @case ('custom-component') { - + } } diff --git a/libs/components/src/lib/testing-fab/testing-fab.component.spec.ts b/libs/components/src/lib/testing-fab/testing-fab.component.spec.ts index 88e2fa2..7228a89 100644 --- a/libs/components/src/lib/testing-fab/testing-fab.component.spec.ts +++ b/libs/components/src/lib/testing-fab/testing-fab.component.spec.ts @@ -3,9 +3,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing' import { DefaultUrlSerializer, Router } from '@angular/router' import { describe, expect, test, vi } from 'vitest' -import { TESTING_FAB_WIDGETS } from './testing-fab-config.token' import { TestingFabComponent } from './testing-fab.component' -import { TestingFabWidget } from './testing-fab-widget.type' +import { TESTING_FAB_WIDGETS } from './testing-fab-config.token' +import type { TestingFabWidget } from './testing-fab-widget.type' @Component({ selector: 'sc-test-custom-widget', @@ -15,6 +15,17 @@ import { TestingFabWidget } from './testing-fab-widget.type' class TestCustomWidgetComponent {} const serializer = new DefaultUrlSerializer() +function queryRequired( + fixture: ComponentFixture, + selector: string, +): TElement { + const element = (fixture.nativeElement as HTMLElement).querySelector(selector) + if (!element) { + throw new Error(`Expected element for selector "${selector}"`) + } + + return element +} function setup( widgets: readonly TestingFabWidget[], @@ -56,7 +67,7 @@ describe('TestingFabComponent', () => { ] const { fixture } = setup(widgets, '/?env=qa') - const select = fixture.nativeElement.querySelector('select') as HTMLSelectElement + const select = queryRequired(fixture, 'select') expect(select.value).toBe('qa') }) @@ -75,7 +86,7 @@ describe('TestingFabComponent', () => { ] const { fixture, routerMock } = setup(widgets, '/?env=qa') - const select = fixture.nativeElement.querySelector('select') as HTMLSelectElement + const select = queryRequired(fixture, 'select') select.value = 'prod' select.dispatchEvent(new Event('change')) @@ -110,7 +121,7 @@ describe('TestingFabComponent', () => { const locationAssignSpy = vi.spyOn(window.location, 'assign').mockImplementation(() => undefined) const { fixture, routerMock } = setup(widgets, '/?env=qa') - const select = fixture.nativeElement.querySelector('select') as HTMLSelectElement + const select = queryRequired(fixture, 'select') select.value = 'prod' select.dispatchEvent(new Event('change')) @@ -132,7 +143,7 @@ describe('TestingFabComponent', () => { ] const { fixture } = setup(widgets) - const customWidget = fixture.nativeElement.querySelector('[data-test-id="custom-widget"]') + const customWidget = queryRequired(fixture, '[data-test-id="custom-widget"]') expect(customWidget).not.toBeNull() }) @@ -148,7 +159,7 @@ describe('TestingFabComponent', () => { ] const { fixture } = setup(widgets) - const button = fixture.nativeElement.querySelector('.sc-testing-fab__button') as HTMLButtonElement + const button = queryRequired(fixture, '.sc-testing-fab__button') button.click() expect(action).toHaveBeenCalledTimes(1) diff --git a/libs/components/src/public-api.ts b/libs/components/src/public-api.ts index feac234..337f50f 100644 --- a/libs/components/src/public-api.ts +++ b/libs/components/src/public-api.ts @@ -41,9 +41,9 @@ export * from './lib/insert-view-ref/insert-view-ref.directive' export * from './lib/navigation-class-handler/navigation-class-handler' // testing fab -export * from './lib/testing-fab/testing-fab.component' export * from './lib/testing-fab/provide-testing-fab' export * from './lib/testing-fab/testing-fab-config.token' +export * from './lib/testing-fab/testing-fab.component' export * from './lib/testing-fab/testing-fab-widget.type' // rx