@marsaude/devtools-shell
v0.4.0
Published
Self-contained draggable DevTools FAB for QA (admin login, CPF/OTP user login, user generator, session switch). Isolated HttpClient/auth, gated out of production.
Maintainers
Readme
@marsaude/devtools-shell
A content-agnostic, draggable floating-action-button shell for dev-only tools.
It ships only the invólucro: a FAB you can drag anywhere on screen (mouse + touch, position persisted across reloads, clamped to the viewport) that opens a container. The panel content is yours — plugged in from the outside. The shell has zero knowledge of auth, APIs, or any business domain.
- Angular standalone + Signals +
@if/@forcontrol flow. No NgModules. - Drag/animations ported verbatim from the
DevTools FABprototype — plain Pointer Events, no external drag library. - Auto-mounts itself (
createComponent + ApplicationRef.attachViewfrom anAPP_BOOTSTRAP_LISTENER) — no tag to place in a template.
Shell behaviour (all domain-free): drag + snap-to-edge + persisted position; idle → collapse into a thin edge grip (tap to restore); tap → radial speed-dial of the registered actions (or open directly when there's a single action); pick an action → its content renders in a side-drawer / bottom-sheet.
Toast: inject DevtoolsToastService anywhere and call show('…') to flash a
message inside the shell layer (no Material dependency):
import { DevtoolsToastService } from '@marsaude/devtools-shell';
private readonly toast = inject(DevtoolsToastService);
this.toast.show('Usuário gerado');Install
npm i @marsaude/devtools-shell
# peers (already present in this workspace):
npm i @angular/core @angular/commonMaterial Symbols are used for glyphs. Load them once in the host app if you want the icons to render:
<link
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined"
rel="stylesheet"
/>Mount the shell
Add provideDevtools() to your application providers.
Standalone bootstrap
import { bootstrapApplication } from '@angular/platform-browser';
import { provideDevtools } from '@marsaude/devtools-shell';
import { environment } from './environments/environment';
bootstrapApplication(AppComponent, {
providers: [
provideDevtools({
enabled: !environment.production, // Layer-1 gate (see below)
title: 'DevTools',
actions: [],
}),
],
});NgModule app (this repo)
provideDevtools() returns EnvironmentProviders, valid in @NgModule.providers:
@NgModule({
// ...
providers: [
provideDevtools({ enabled: !environment.production }),
],
})
export class AppModule {}That's all — a draggable FAB appears, and clicking it opens an (empty) panel.
Pass the interface later (the extension point)
The panel content is supplied as actions. Each action is a standalone
component (or a TemplateRef) — the shell just renders it.
import { provideDevtools } from '@marsaude/devtools-shell';
import { MyUserGeneratorPanel } from './devtools/user-generator-panel';
provideDevtools({
enabled: !environment.production,
actions: [
{ id: 'gen', label: 'Gerador', icon: 'groups', content: MyUserGeneratorPanel },
{ id: 'login', label: 'Logar', icon: 'login', content: MyLoginPanel },
],
});- One action → it opens directly in the container.
- Multiple actions → the shell renders a tab switcher in the panel header.
- Your panel component is rendered with
NgComponentOutlet; it can inject its own services normally.
Two doors for content
Action registry (primary). Shown above. This is the recommended path because the shell auto-mounts onto
document.body— it has no place in your template, so there is nowhere to project into.Content projection (secondary). Only when you place the component yourself instead of using
provideDevtoolsauto-mount:<devtools-shell> <my-panel /> </devtools-shell><ng-content>is rendered when no actions are registered.
Why the registry is primary: auto-mounting via
createComponent(the pattern reused from the original DevTools boot flow) means the shell lives outside any consumer template.<ng-content>requires the consumer to host the tag, which contradicts auto-mount. The token-based registry decouples where the shell lives from who supplies the content.
Re-skinning
The shell ships the dark theme from the original mockup, exposed as CSS custom properties on the host element. Override them anywhere:
[data-devtools-shell-host] devtools-shell {
--dts-bg: #14151b;
--dts-accent: #ffc454;
--dts-fg: #eceef3;
--dts-font: system-ui, sans-serif;
}Production gating
The shell must never reach a production bundle. Two layers:
Layer 1 — runtime flag (always on)
Pass enabled: !environment.production. When false, provideDevtools()
returns no providers: nothing mounts, no actions register.
Layer 2 — build-time elimination (recommended)
Layer 1 still leaves the shell imported. To drop it from the bundle entirely,
isolate the wiring in one file and swap it via fileReplacements.
src/app/devtools/devtools.providers.ts (dev):
import { EnvironmentProviders } from '@angular/core';
import { provideDevtools } from '@marsaude/devtools-shell';
import { UserGeneratorPanel } from './user-generator-panel';
export function devtoolsProviders(): EnvironmentProviders[] {
return [
provideDevtools({
enabled: true,
actions: [{ id: 'gen', label: 'Gerador', icon: 'groups', content: UserGeneratorPanel }],
}),
];
}src/app/devtools/devtools.providers.prod.ts (prod no-op — imports nothing):
export function devtoolsProviders() {
return [];
}angular.json (production configuration):
"fileReplacements": [
{ "replace": "src/environments/environment.ts", "with": "src/environments/environment.prod.ts" },
{ "replace": "src/app/devtools/devtools.providers.ts", "with": "src/app/devtools/devtools.providers.prod.ts" }
]Use it in bootstrap:
import { devtoolsProviders } from './app/devtools/devtools.providers';
// providers: [ ...devtoolsProviders() ]Because the prod file imports neither @marsaude/devtools-shell nor your panel
components, the bundler tree-shakes the whole shell out of the production build.
Verification checklist
- [ ]
provideDevtools({ enabled: !environment.production })— Layer 1 in place. - [ ] Wiring isolated in
devtools.providers.tswith a.prod.tsno-op twin. - [ ]
fileReplacementsentry for the providers file added to the production configuration inangular.json. - [ ]
environment.prod.tsactually hasproduction: truefor the real prod env. - [ ] Run a prod build and confirm the shell is gone:
ng build --configuration productionthengrep -r "devtools-shell\|data-devtools-shell-host" dist/returns nothing. - [ ] Load the prod bundle: no FAB on screen, no
[data-devtools-shell-host]element in the DOM.
Build & publish (public npm — scope @marsaude)
Published as a public scoped package on npmjs (free). You must be logged in
as a user that owns the @marsaude scope.
# one-time: authenticate against npmjs
npm login # npm whoami → marsaude
# build + publish in one step (from the workspace root)
npm run publish:lib
# → runs `ng build devtools-shell` then
# `npm publish ./dist/devtools-shell --access public`Manual equivalent:
npm run build:lib # → dist/devtools-shell
npm publish ./dist/devtools-shell --access public # note the ./ prefixBump the version in projects/devtools-shell/package.json before each release
(0.1.0 → 0.1.1 …). peerDependencies: @angular/core, @angular/common
(^21).
--access publicis required for the first publish of a scoped package on the free npm plan (private/restricted needs a paid npm plan →E402).
