+ @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.scss b/libs/components/src/lib/testing-fab/testing-fab.component.scss
new file mode 100644
index 0000000..a4b4d12
--- /dev/null
+++ b/libs/components/src/lib/testing-fab/testing-fab.component.scss
@@ -0,0 +1,101 @@
+:host {
+ position: fixed;
+ 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-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: var(--sc-testing-fab-size);
+ height: var(--sc-testing-fab-size);
+ border-radius: var(--sc-testing-fab-border-radius);
+ border: 0;
+ background: var(--sc-testing-fab-background-color);
+ color: var(--sc-testing-fab-foreground-color);
+ cursor: pointer;
+ 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);
+ 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%);
+ 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 {
+ 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;
+}
+
+.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
new file mode 100644
index 0000000..7228a89
--- /dev/null
+++ b/libs/components/src/lib/testing-fab/testing-fab.component.spec.ts
@@ -0,0 +1,167 @@
+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 { TestingFabComponent } from './testing-fab.component'
+import { TESTING_FAB_WIDGETS } from './testing-fab-config.token'
+import type { TestingFabWidget } from './testing-fab-widget.type'
+
+@Component({
+ selector: 'sc-test-custom-widget',
+ template: `