@polymorfa/devtools 0.1.0-dev.20260922094209

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Polymorfa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # `@polymorfa/devtools`
2
+
3
+ An explicit development assistant for testing Polymorfa appearance, direction,
4
+ motion, viewport presets, network profiles, and redacted request diagnostics.
5
+
6
+ The assistant only enables when a trusted build environment and the client-token
7
+ environment match and both are non-production. URL query parameters are never
8
+ consulted. The `@polymorfa/devtools/production` entry point is inert.
9
+
10
+ ```ts
11
+ import { DevAssistant, mountDevAssistant } from "@polymorfa/devtools";
12
+
13
+ const assistant = new DevAssistant({
14
+ environment: "development",
15
+ tokenEnvironment: "development",
16
+ });
17
+
18
+ mountDevAssistant(assistant);
19
+ ```
20
+
21
+ The assistant starts as a 40px launcher button in the bottom-left corner, so
22
+ it stays clear of a chat composer. Selecting it opens the panel beside it;
23
+ Escape or the close button closes the panel and returns focus to the
24
+ launcher. The open state is kept in `sessionStorage` for the tab when
25
+ storage is available. The launcher and panel render in a shadow root, follow
26
+ the system light or dark color scheme, and skip their transitions when
27
+ reduced motion is preferred.
28
+
29
+ ```ts
30
+ const mounted = mountDevAssistant(assistant, {
31
+ position: "top-right", // "bottom-left" (default), "bottom-right", "top-left"
32
+ defaultOpen: false,
33
+ offset: { x: 16, y: 16 },
34
+ parent: document.body,
35
+ });
36
+ mounted?.open();
37
+ mounted?.dispose();
38
+ ```
39
+
40
+ Passing a parent element as the second argument still works. The returned
41
+ `element` is the host; the controls are inside its open `shadowRoot`.
@@ -0,0 +1,4 @@
1
+ export { DevAssistant, type DevAssistantOptions, type DevAssistantSettings, type DevAssistantSnapshot, type NetworkProfile, type RuntimeEnvironment, type ViewportPreset, } from "./model.js";
2
+ export { createSimulatedFetch, type SimulatedFetchOptions } from "./network.js";
3
+ export { mountDevAssistant, type DevAssistantPosition, type MountDevAssistantOptions, type MountedDevAssistant, } from "./panel.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACvB,KAAK,cAAc,GACpB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,oBAAoB,EAAE,KAAK,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAChF,OAAO,EACL,iBAAiB,EACjB,KAAK,oBAAoB,EACzB,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,GACzB,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { DevAssistant, } from "./model.js";
2
+ export { createSimulatedFetch } from "./network.js";
3
+ export { mountDevAssistant, } from "./panel.js";
@@ -0,0 +1,44 @@
1
+ import type { BrowserDiagnosticEvent } from "@polymorfa/browser";
2
+ import { type Appearance, type AppearanceInput, type Direction, type MotionPreference, type Theme } from "@polymorfa/ui";
3
+ export type RuntimeEnvironment = "development" | "test" | "preview" | "production";
4
+ export type NetworkProfile = "online" | "slow" | "offline";
5
+ export type ViewportPreset = "responsive" | "mobile" | "tablet" | "desktop";
6
+ export interface DevAssistantSettings {
7
+ readonly appearance: Appearance;
8
+ readonly locale: string;
9
+ readonly direction: Direction;
10
+ readonly motion: MotionPreference;
11
+ readonly viewport: ViewportPreset;
12
+ readonly network: NetworkProfile;
13
+ }
14
+ export interface DevAssistantOptions {
15
+ /** Must come from trusted build or server configuration, never the URL. */
16
+ readonly environment: RuntimeEnvironment;
17
+ /** Environment asserted by the client-token minting service. */
18
+ readonly tokenEnvironment: RuntimeEnvironment;
19
+ readonly appearance?: AppearanceInput;
20
+ readonly locale?: string;
21
+ readonly maxEvents?: number;
22
+ }
23
+ export interface DevAssistantSnapshot extends DevAssistantSettings {
24
+ readonly enabled: boolean;
25
+ readonly events: readonly unknown[];
26
+ }
27
+ export declare class DevAssistant {
28
+ #private;
29
+ constructor(options: DevAssistantOptions);
30
+ getSnapshot: () => DevAssistantSnapshot;
31
+ subscribe: (listener: () => void) => (() => void);
32
+ diagnosticSink: (event: BrowserDiagnosticEvent) => void;
33
+ configure(input: {
34
+ readonly theme?: Theme;
35
+ readonly locale?: string;
36
+ readonly direction?: Direction;
37
+ readonly motion?: MotionPreference;
38
+ readonly viewport?: ViewportPreset;
39
+ readonly network?: NetworkProfile;
40
+ }): void;
41
+ copyableConfig(): string;
42
+ clearEvents(): void;
43
+ }
44
+ //# sourceMappingURL=model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAGL,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,SAAS,EACd,KAAK,gBAAgB,EACrB,KAAK,KAAK,EACX,MAAM,eAAe,CAAC;AAEvB,MAAM,MAAM,kBAAkB,GAC5B,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,YAAY,CAAC;AACpD,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC;AAC3D,MAAM,MAAM,cAAc,GAAG,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE5E,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;CAClC;AAED,MAAM,WAAW,mBAAmB;IAClC,2EAA2E;IAC3E,QAAQ,CAAC,WAAW,EAAE,kBAAkB,CAAC;IACzC,gEAAgE;IAChE,QAAQ,CAAC,gBAAgB,EAAE,kBAAkB,CAAC;IAC9C,QAAQ,CAAC,UAAU,CAAC,EAAE,eAAe,CAAC;IACtC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,oBAAqB,SAAQ,oBAAoB;IAChE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,SAAS,OAAO,EAAE,CAAC;CACrC;AAED,qBAAa,YAAY;;gBAOX,OAAO,EAAE,mBAAmB;IAiBxC,WAAW,QAAO,oBAAoB,CAKjC;IAEL,SAAS,GAAI,UAAU,MAAM,IAAI,KAAG,CAAC,MAAM,IAAI,CAAC,CAG9C;IAEF,cAAc,GAAI,OAAO,sBAAsB,KAAG,IAAI,CAMpD;IAEF,SAAS,CAAC,KAAK,EAAE;QACf,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC;QACvB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QACzB,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,CAAC;QAC/B,QAAQ,CAAC,MAAM,CAAC,EAAE,gBAAgB,CAAC;QACnC,QAAQ,CAAC,QAAQ,CAAC,EAAE,cAAc,CAAC;QACnC,QAAQ,CAAC,OAAO,CAAC,EAAE,cAAc,CAAC;KACnC,GAAG,IAAI;IA2BR,cAAc,IAAI,MAAM;IAIxB,WAAW,IAAI,IAAI;CASpB"}
package/dist/model.js ADDED
@@ -0,0 +1,79 @@
1
+ import { defineAppearance, redactDiagnostic, } from "@polymorfa/ui";
2
+ export class DevAssistant {
3
+ #enabled;
4
+ #maxEvents;
5
+ #listeners = new Set();
6
+ #settings;
7
+ #events = [];
8
+ constructor(options) {
9
+ this.#enabled =
10
+ options.environment !== "production" &&
11
+ options.tokenEnvironment !== "production" &&
12
+ options.environment === options.tokenEnvironment;
13
+ this.#maxEvents = Math.max(1, Math.floor(options.maxEvents ?? 50));
14
+ const appearance = defineAppearance(options.appearance);
15
+ this.#settings = Object.freeze({
16
+ appearance,
17
+ locale: options.locale ?? "en",
18
+ direction: appearance.layout.direction,
19
+ motion: appearance.variables.motion,
20
+ viewport: "responsive",
21
+ network: "online",
22
+ });
23
+ }
24
+ getSnapshot = () => Object.freeze({
25
+ ...this.#settings,
26
+ enabled: this.#enabled,
27
+ events: Object.freeze([...this.#events]),
28
+ });
29
+ subscribe = (listener) => {
30
+ this.#listeners.add(listener);
31
+ return () => this.#listeners.delete(listener);
32
+ };
33
+ diagnosticSink = (event) => {
34
+ if (!this.#enabled)
35
+ return;
36
+ this.#events = [...this.#events, redactDiagnostic(event)].slice(-this.#maxEvents);
37
+ this.#emit();
38
+ };
39
+ configure(input) {
40
+ if (!this.#enabled)
41
+ return;
42
+ const appearance = defineAppearance({
43
+ ...this.#settings.appearance,
44
+ ...(input.theme === undefined ? {} : { theme: input.theme }),
45
+ variables: {
46
+ ...this.#settings.appearance.variables,
47
+ ...(input.motion === undefined ? {} : { motion: input.motion }),
48
+ },
49
+ layout: {
50
+ ...this.#settings.appearance.layout,
51
+ ...(input.direction === undefined
52
+ ? {}
53
+ : { direction: input.direction }),
54
+ },
55
+ });
56
+ this.#settings = Object.freeze({
57
+ appearance,
58
+ locale: input.locale ?? this.#settings.locale,
59
+ direction: input.direction ?? this.#settings.direction,
60
+ motion: input.motion ?? this.#settings.motion,
61
+ viewport: input.viewport ?? this.#settings.viewport,
62
+ network: input.network ?? this.#settings.network,
63
+ });
64
+ this.#emit();
65
+ }
66
+ copyableConfig() {
67
+ return JSON.stringify(this.#settings, null, 2);
68
+ }
69
+ clearEvents() {
70
+ if (!this.#enabled)
71
+ return;
72
+ this.#events = [];
73
+ this.#emit();
74
+ }
75
+ #emit() {
76
+ for (const listener of this.#listeners)
77
+ listener();
78
+ }
79
+ }
@@ -0,0 +1,9 @@
1
+ import type { NetworkProfile } from "./model.js";
2
+ export interface SimulatedFetchOptions {
3
+ readonly profile: () => NetworkProfile;
4
+ readonly delayMs?: number;
5
+ readonly fetch?: typeof globalThis.fetch;
6
+ }
7
+ /** Explicitly wraps fetch for development; no simulation is installed globally. */
8
+ export declare function createSimulatedFetch(options: SimulatedFetchOptions): typeof fetch;
9
+ //# sourceMappingURL=network.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../src/network.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,cAAc,CAAC;IACvC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CAC1C;AAED,mFAAmF;AACnF,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,KAAK,CAWd"}
@@ -0,0 +1,26 @@
1
+ /** Explicitly wraps fetch for development; no simulation is installed globally. */
2
+ export function createSimulatedFetch(options) {
3
+ const fetchImplementation = options.fetch ?? globalThis.fetch;
4
+ return async (input, init) => {
5
+ const profile = options.profile();
6
+ if (profile === "offline")
7
+ throw new TypeError("Simulated offline network.");
8
+ if (profile === "slow") {
9
+ await abortableDelay(options.delayMs ?? 800, init?.signal);
10
+ }
11
+ return fetchImplementation(input, init);
12
+ };
13
+ }
14
+ function abortableDelay(delayMs, signal) {
15
+ return new Promise((resolve, reject) => {
16
+ if (signal?.aborted === true) {
17
+ reject(signal.reason);
18
+ return;
19
+ }
20
+ const timeout = setTimeout(resolve, delayMs);
21
+ signal?.addEventListener("abort", () => {
22
+ clearTimeout(timeout);
23
+ reject(signal.reason);
24
+ }, { once: true });
25
+ });
26
+ }
@@ -0,0 +1,31 @@
1
+ import type { DevAssistant } from "./model.js";
2
+ export type DevAssistantPosition = "bottom-left" | "bottom-right" | "top-left" | "top-right";
3
+ export interface MountDevAssistantOptions {
4
+ /** Corner for the launcher. Defaults to `"bottom-left"`. */
5
+ readonly position?: DevAssistantPosition;
6
+ /** Open the panel on first mount. Defaults to `false`. */
7
+ readonly defaultOpen?: boolean;
8
+ /** Distance from the viewport edges in pixels. Defaults to 16 each. */
9
+ readonly offset?: {
10
+ readonly x?: number;
11
+ readonly y?: number;
12
+ };
13
+ /** Element to mount into. Defaults to `document.body`. */
14
+ readonly parent?: HTMLElement;
15
+ }
16
+ export interface MountedDevAssistant {
17
+ /** Host element; the launcher and panel live in its open shadow root. */
18
+ readonly element: HTMLElement;
19
+ readonly isOpen: boolean;
20
+ open(): void;
21
+ close(): void;
22
+ toggle(): void;
23
+ dispose(): void;
24
+ }
25
+ /**
26
+ * Mount the development panel: a small launcher in a corner that opens the
27
+ * panel next to it. Returns `null` when the assistant is disabled. The
28
+ * second argument may also be the parent element, as in earlier versions.
29
+ */
30
+ export declare function mountDevAssistant(assistant: DevAssistant, options?: MountDevAssistantOptions | HTMLElement): MountedDevAssistant | null;
31
+ //# sourceMappingURL=panel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../src/panel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAwB,MAAM,YAAY,CAAC;AAErE,MAAM,MAAM,oBAAoB,GAC9B,aAAa,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,CAAC;AAE5D,MAAM,WAAW,wBAAwB;IACvC,4DAA4D;IAC5D,QAAQ,CAAC,QAAQ,CAAC,EAAE,oBAAoB,CAAC;IACzC,0DAA0D;IAC1D,QAAQ,CAAC,WAAW,CAAC,EAAE,OAAO,CAAC;IAC/B,uEAAuE;IACvE,QAAQ,CAAC,MAAM,CAAC,EAAE;QAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/D,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAClC,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,IAAI,IAAI,IAAI,CAAC;IACb,KAAK,IAAI,IAAI,CAAC;IACd,MAAM,IAAI,IAAI,CAAC;IACf,OAAO,IAAI,IAAI,CAAC;CACjB;AAkND;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,YAAY,EACvB,OAAO,GAAE,wBAAwB,GAAG,WAAgB,GACnD,mBAAmB,GAAG,IAAI,CA+K5B"}
package/dist/panel.js ADDED
@@ -0,0 +1,407 @@
1
+ const STORAGE_KEY = "polymorfa:devtools:open";
2
+ function readOpen() {
3
+ try {
4
+ const value = globalThis.sessionStorage?.getItem(STORAGE_KEY);
5
+ return value === "1" ? true : value === "0" ? false : undefined;
6
+ }
7
+ catch {
8
+ return undefined;
9
+ }
10
+ }
11
+ function writeOpen(open) {
12
+ try {
13
+ globalThis.sessionStorage?.setItem(STORAGE_KEY, open ? "1" : "0");
14
+ }
15
+ catch {
16
+ // Storage can be blocked; the state then lasts for this page only.
17
+ }
18
+ }
19
+ const STYLES = `
20
+ :host {
21
+ all: initial;
22
+ --bg: #ffffff;
23
+ --fg: #17151f;
24
+ --muted: #6d6878;
25
+ --border: #e4e1ea;
26
+ --surface: #f5f4f8;
27
+ --accent: #5b4bf7;
28
+ --on-accent: #ffffff;
29
+ --shadow: 0 24px 60px rgb(23 21 31 / 0.18), 0 2px 8px rgb(23 21 31 / 0.08);
30
+ position: fixed;
31
+ z-index: 2147483647;
32
+ font: 13px/1.45 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
33
+ color: var(--fg);
34
+ color-scheme: light;
35
+ }
36
+ @media (prefers-color-scheme: dark) {
37
+ :host {
38
+ --bg: #1c1b22;
39
+ --fg: #f1eff6;
40
+ --muted: #a19cae;
41
+ --border: #34313e;
42
+ --surface: #26242e;
43
+ --accent: #8b80ff;
44
+ --on-accent: #120f2b;
45
+ --shadow: 0 24px 60px rgb(0 0 0 / 0.5), 0 2px 8px rgb(0 0 0 / 0.3);
46
+ color-scheme: dark;
47
+ }
48
+ }
49
+ *, *::before, *::after { box-sizing: border-box; }
50
+ button, select { font: inherit; color: inherit; }
51
+ .launcher {
52
+ display: grid;
53
+ place-items: center;
54
+ width: 40px;
55
+ height: 40px;
56
+ padding: 0;
57
+ border: 1px solid var(--border);
58
+ border-radius: 50%;
59
+ background: var(--bg);
60
+ color: var(--accent);
61
+ box-shadow: 0 4px 14px rgb(0 0 0 / 0.16);
62
+ cursor: pointer;
63
+ opacity: 0.85;
64
+ transition: opacity 120ms ease, transform 120ms ease, background-color 120ms ease;
65
+ }
66
+ .launcher:hover, .launcher[aria-expanded="true"] { opacity: 1; }
67
+ .launcher[aria-expanded="true"] { background: var(--accent); color: var(--on-accent); border-color: transparent; }
68
+ .launcher:active { transform: scale(0.94); }
69
+ .launcher svg { width: 20px; height: 20px; }
70
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
71
+ .panel {
72
+ position: absolute;
73
+ width: min(320px, calc(100vw - 32px));
74
+ max-height: min(560px, calc(100vh - 88px));
75
+ overflow: auto;
76
+ border: 1px solid var(--border);
77
+ border-radius: 14px;
78
+ background: var(--bg);
79
+ box-shadow: var(--shadow);
80
+ transform-origin: var(--origin);
81
+ transition: opacity 140ms ease, transform 140ms ease, visibility 0s linear 140ms;
82
+ }
83
+ .panel[hidden] { display: block; visibility: hidden; opacity: 0; transform: scale(0.96); pointer-events: none; }
84
+ .panel:not([hidden]) { transition-delay: 0s; }
85
+ :host([data-position^="bottom"]) .panel { bottom: 48px; }
86
+ :host([data-position^="top"]) .panel { top: 48px; }
87
+ :host([data-position$="left"]) .panel { left: 0; }
88
+ :host([data-position$="right"]) .panel { right: 0; }
89
+ header {
90
+ display: flex;
91
+ align-items: center;
92
+ gap: 8px;
93
+ padding: 10px 8px 10px 14px;
94
+ border-bottom: 1px solid var(--border);
95
+ }
96
+ h2 { flex: 1; margin: 0; font-size: 13px; font-weight: 650; }
97
+ .icon-button {
98
+ display: grid;
99
+ place-items: center;
100
+ width: 32px;
101
+ height: 32px;
102
+ padding: 0;
103
+ border: 0;
104
+ border-radius: 8px;
105
+ background: none;
106
+ color: var(--muted);
107
+ cursor: pointer;
108
+ }
109
+ .icon-button:hover { background: var(--surface); color: var(--fg); }
110
+ .icon-button svg { width: 16px; height: 16px; }
111
+ .body { display: flex; flex-direction: column; gap: 12px; padding: 12px 14px 14px; }
112
+ fieldset { margin: 0; padding: 0; border: 0; min-width: 0; }
113
+ legend, .label { display: block; margin-bottom: 6px; padding: 0; color: var(--muted); font-size: 11px; font-weight: 600; letter-spacing: 0.02em; text-transform: uppercase; }
114
+ .segmented {
115
+ display: flex;
116
+ gap: 2px;
117
+ padding: 2px;
118
+ border-radius: 9px;
119
+ background: var(--surface);
120
+ }
121
+ .segmented label { flex: 1; position: relative; }
122
+ .segmented input { position: absolute; inset: 0; margin: 0; opacity: 0; cursor: pointer; }
123
+ .segmented span {
124
+ display: block;
125
+ padding: 5px 4px;
126
+ border-radius: 7px;
127
+ color: var(--muted);
128
+ font-weight: 550;
129
+ text-align: center;
130
+ text-transform: capitalize;
131
+ white-space: nowrap;
132
+ transition: background-color 120ms ease, color 120ms ease;
133
+ }
134
+ .segmented input:checked + span { background: var(--bg); color: var(--fg); box-shadow: 0 1px 2px rgb(0 0 0 / 0.12); }
135
+ .segmented input:focus-visible + span { outline: 2px solid var(--accent); outline-offset: -2px; }
136
+ .select { position: relative; }
137
+ .select select {
138
+ appearance: none;
139
+ width: 100%;
140
+ min-height: 32px;
141
+ padding: 5px 30px 5px 10px;
142
+ border: 1px solid var(--border);
143
+ border-radius: 8px;
144
+ background: var(--surface);
145
+ text-transform: capitalize;
146
+ cursor: pointer;
147
+ }
148
+ .select svg { position: absolute; right: 10px; top: 50%; width: 14px; height: 14px; transform: translateY(-50%); color: var(--muted); pointer-events: none; }
149
+ footer {
150
+ display: flex;
151
+ align-items: center;
152
+ gap: 8px;
153
+ padding: 10px 14px;
154
+ border-top: 1px solid var(--border);
155
+ background: var(--surface);
156
+ }
157
+ .events { flex: 1; color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
158
+ .button {
159
+ min-height: 32px;
160
+ padding: 0 12px;
161
+ border: 0;
162
+ border-radius: 8px;
163
+ background: var(--accent);
164
+ color: var(--on-accent);
165
+ font-weight: 600;
166
+ cursor: pointer;
167
+ }
168
+ .button:hover { filter: brightness(1.08); }
169
+ @media (prefers-reduced-motion: reduce) {
170
+ .launcher, .panel, .segmented span { transition: none; }
171
+ }
172
+ `;
173
+ const SVG = "http://www.w3.org/2000/svg";
174
+ function svgIcon(d) {
175
+ const svg = document.createElementNS(SVG, "svg");
176
+ svg.setAttribute("viewBox", "0 0 24 24");
177
+ svg.setAttribute("fill", "none");
178
+ svg.setAttribute("stroke", "currentColor");
179
+ svg.setAttribute("stroke-width", "2");
180
+ svg.setAttribute("stroke-linecap", "round");
181
+ svg.setAttribute("stroke-linejoin", "round");
182
+ svg.setAttribute("aria-hidden", "true");
183
+ const path = document.createElementNS(SVG, "path");
184
+ path.setAttribute("d", d);
185
+ svg.append(path);
186
+ return svg;
187
+ }
188
+ const ICONS = {
189
+ wrench: "M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76Z",
190
+ close: "M18 6 6 18M6 6l12 12",
191
+ chevron: "m6 9 6 6 6-6",
192
+ };
193
+ let instance = 0;
194
+ function isOptions(value) {
195
+ return (value !== undefined &&
196
+ !(typeof HTMLElement !== "undefined" && value instanceof HTMLElement));
197
+ }
198
+ /**
199
+ * Mount the development panel: a small launcher in a corner that opens the
200
+ * panel next to it. Returns `null` when the assistant is disabled. The
201
+ * second argument may also be the parent element, as in earlier versions.
202
+ */
203
+ export function mountDevAssistant(assistant, options = {}) {
204
+ if (!assistant.getSnapshot().enabled)
205
+ return null;
206
+ const settings = isOptions(options)
207
+ ? options
208
+ : { parent: options };
209
+ const parent = settings.parent ?? document.body;
210
+ const position = settings.position ?? "bottom-left";
211
+ const x = settings.offset?.x ?? 16;
212
+ const y = settings.offset?.y ?? 16;
213
+ const id = `pmfa-devtools-${++instance}`;
214
+ const host = document.createElement("div");
215
+ host.dataset.polymorfaDevtools = "";
216
+ host.dataset.position = position;
217
+ const [vertical, horizontal] = position.split("-");
218
+ host.style.setProperty(vertical, `${y}px`);
219
+ host.style.setProperty(horizontal, `${x}px`);
220
+ const shadow = host.attachShadow({ mode: "open" });
221
+ const style = document.createElement("style");
222
+ style.textContent = STYLES;
223
+ const launcher = document.createElement("button");
224
+ launcher.type = "button";
225
+ launcher.className = "launcher";
226
+ launcher.setAttribute("aria-label", "Polymorfa dev mode");
227
+ launcher.setAttribute("aria-controls", `${id}-panel`);
228
+ launcher.title = "Polymorfa dev mode";
229
+ launcher.append(svgIcon(ICONS.wrench));
230
+ const panel = document.createElement("section");
231
+ panel.className = "panel";
232
+ panel.id = `${id}-panel`;
233
+ panel.setAttribute("role", "dialog");
234
+ panel.setAttribute("aria-label", "Polymorfa developer assistant");
235
+ panel.style.setProperty("--origin", `${vertical} ${horizontal}`);
236
+ const header = document.createElement("header");
237
+ const title = document.createElement("h2");
238
+ title.textContent = "Polymorfa dev mode";
239
+ const close = document.createElement("button");
240
+ close.type = "button";
241
+ close.className = "icon-button";
242
+ close.setAttribute("aria-label", "Close dev mode");
243
+ close.append(svgIcon(ICONS.close));
244
+ header.append(title, close);
245
+ const body = document.createElement("div");
246
+ body.className = "body";
247
+ const controls = [
248
+ segmented(`${id}-theme`, "Theme", ["system", "light", "dark"], (value) => assistant.configure({ theme: value })),
249
+ segmented(`${id}-direction`, "Direction", ["auto", "ltr", "rtl"], (value) => assistant.configure({ direction: value })),
250
+ segmented(`${id}-motion`, "Motion", ["system", "full", "reduced"], (value) => assistant.configure({
251
+ motion: value,
252
+ })),
253
+ select(`${id}-viewport`, "Viewport", ["responsive", "mobile", "tablet", "desktop"], (value) => assistant.configure({
254
+ viewport: value,
255
+ })),
256
+ segmented(`${id}-network`, "Network", ["online", "slow", "offline"], (value) => assistant.configure({
257
+ network: value,
258
+ })),
259
+ ];
260
+ body.append(...controls.map((control) => control.node));
261
+ const footer = document.createElement("footer");
262
+ const events = document.createElement("span");
263
+ events.className = "events";
264
+ events.setAttribute("aria-live", "polite");
265
+ const copy = document.createElement("button");
266
+ copy.type = "button";
267
+ copy.className = "button";
268
+ copy.textContent = "Copy configuration";
269
+ let copyTimer;
270
+ copy.addEventListener("click", () => {
271
+ void navigator.clipboard
272
+ ?.writeText(assistant.copyableConfig())
273
+ .then(() => {
274
+ copy.textContent = "Copied";
275
+ clearTimeout(copyTimer);
276
+ copyTimer = setTimeout(() => {
277
+ copy.textContent = "Copy configuration";
278
+ }, 1500);
279
+ })
280
+ .catch(() => undefined);
281
+ });
282
+ footer.append(events, copy);
283
+ panel.append(header, body, footer);
284
+ shadow.append(style, panel, launcher);
285
+ parent.append(host);
286
+ let isOpen = readOpen() ?? settings.defaultOpen ?? false;
287
+ const apply = () => {
288
+ panel.hidden = !isOpen;
289
+ launcher.setAttribute("aria-expanded", String(isOpen));
290
+ };
291
+ const setOpen = (next, focus) => {
292
+ if (next === isOpen)
293
+ return;
294
+ isOpen = next;
295
+ writeOpen(next);
296
+ apply();
297
+ if (!focus)
298
+ return;
299
+ if (next)
300
+ panel
301
+ .querySelector("input:checked, select, button")
302
+ ?.focus();
303
+ else
304
+ launcher.focus();
305
+ };
306
+ apply();
307
+ // Opening from the launcher moves focus into the panel, which precedes the
308
+ // launcher in tab order; closing leaves focus on the launcher itself.
309
+ launcher.addEventListener("click", () => setOpen(!isOpen, true));
310
+ close.addEventListener("click", () => setOpen(false, true));
311
+ shadow.addEventListener("keydown", (event) => {
312
+ const key = event;
313
+ if (key.key !== "Escape" || !isOpen || key.isComposing)
314
+ return;
315
+ key.preventDefault();
316
+ key.stopPropagation();
317
+ setOpen(false, true);
318
+ });
319
+ const render = () => {
320
+ const snapshot = assistant.getSnapshot();
321
+ panel.dir = snapshot.direction === "rtl" ? "rtl" : "ltr";
322
+ const values = [
323
+ snapshot.appearance.theme,
324
+ snapshot.direction,
325
+ snapshot.motion,
326
+ snapshot.viewport,
327
+ snapshot.network,
328
+ ];
329
+ controls.forEach((control, index) => control.set(values[index] ?? ""));
330
+ const count = snapshot.events.length;
331
+ events.textContent = `${count} redacted request event${count === 1 ? "" : "s"}`;
332
+ };
333
+ const unsubscribe = assistant.subscribe(render);
334
+ render();
335
+ return {
336
+ element: host,
337
+ get isOpen() {
338
+ return isOpen;
339
+ },
340
+ open: () => setOpen(true, false),
341
+ close: () => setOpen(false, false),
342
+ toggle: () => setOpen(!isOpen, false),
343
+ dispose: () => {
344
+ unsubscribe();
345
+ clearTimeout(copyTimer);
346
+ host.remove();
347
+ },
348
+ };
349
+ }
350
+ function segmented(name, label, options, onChange) {
351
+ const fieldset = document.createElement("fieldset");
352
+ const legend = document.createElement("legend");
353
+ legend.textContent = label;
354
+ const group = document.createElement("div");
355
+ group.className = "segmented";
356
+ const inputs = options.map((option) => {
357
+ const wrapper = document.createElement("label");
358
+ const input = document.createElement("input");
359
+ input.type = "radio";
360
+ input.name = name;
361
+ input.value = option;
362
+ input.addEventListener("change", () => {
363
+ if (input.checked)
364
+ onChange(option);
365
+ });
366
+ const text = document.createElement("span");
367
+ text.textContent = option;
368
+ wrapper.append(input, text);
369
+ group.append(wrapper);
370
+ return input;
371
+ });
372
+ fieldset.append(legend, group);
373
+ return {
374
+ node: fieldset,
375
+ set: (value) => {
376
+ for (const input of inputs)
377
+ input.checked = input.value === value;
378
+ },
379
+ };
380
+ }
381
+ function select(id, label, options, onChange) {
382
+ const wrapper = document.createElement("div");
383
+ const caption = document.createElement("label");
384
+ caption.className = "label";
385
+ caption.htmlFor = id;
386
+ caption.textContent = label;
387
+ const box = document.createElement("div");
388
+ box.className = "select";
389
+ const control = document.createElement("select");
390
+ control.id = id;
391
+ for (const option of options) {
392
+ const item = document.createElement("option");
393
+ item.value = option;
394
+ item.textContent = option;
395
+ control.add(item);
396
+ }
397
+ control.addEventListener("change", () => onChange(control.value));
398
+ box.append(control, svgIcon(ICONS.chevron));
399
+ wrapper.append(caption, box);
400
+ return {
401
+ node: wrapper,
402
+ set: (value) => {
403
+ if (control.value !== value)
404
+ control.value = value;
405
+ },
406
+ };
407
+ }
@@ -0,0 +1,3 @@
1
+ /** Production entry point. Deliberately inert and free of DOM side effects. */
2
+ export declare function mountDevAssistant(...args: readonly unknown[]): null;
3
+ //# sourceMappingURL=production.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"production.d.ts","sourceRoot":"","sources":["../src/production.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAAC,GAAG,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,IAAI,CAGnE"}
@@ -0,0 +1,5 @@
1
+ /** Production entry point. Deliberately inert and free of DOM side effects. */
2
+ export function mountDevAssistant(...args) {
3
+ void args;
4
+ return null;
5
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@polymorfa/devtools",
3
+ "version": "0.1.0-dev.20260922094209",
4
+ "description": "Production-gated configuration, theming, and diagnostics assistant for Polymorfa UI",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/polymorfa/sdks.git",
10
+ "directory": "packages/devtools"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/polymorfa/sdks/issues"
14
+ },
15
+ "homepage": "https://github.com/polymorfa/sdks#readme",
16
+ "sideEffects": false,
17
+ "main": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ },
24
+ "./production": {
25
+ "types": "./dist/production.d.ts",
26
+ "import": "./dist/production.js"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "README.md",
32
+ "LICENSE"
33
+ ],
34
+ "scripts": {
35
+ "build": "tsc -p tsconfig.build.json",
36
+ "typecheck": "tsc -p tsconfig.json --noEmit"
37
+ },
38
+ "dependencies": {
39
+ "@polymorfa/ui": "0.1.0-dev.20260922094209"
40
+ },
41
+ "peerDependencies": {
42
+ "@polymorfa/browser": "0.1.0-dev.20260922094209"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public",
46
+ "provenance": true
47
+ }
48
+ }