@flighthq/statusbar 0.1.0

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.
@@ -0,0 +1,2 @@
1
+ export * from './statusbar';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './statusbar';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC"}
@@ -0,0 +1,18 @@
1
+ import type { StatusBar, StatusBarAnimation, StatusBarBackend, StatusBarInfo, StatusBarStyle, StatusBarStyleEntry, StatusBarStyleEntryHandle } from '@flighthq/types';
2
+ export declare function attachStatusBar(bar: StatusBar): void;
3
+ export declare function createStatusBar(): StatusBar;
4
+ export declare function createStatusBarInfo(): StatusBarInfo;
5
+ export declare function createWebStatusBarBackend(): StatusBarBackend;
6
+ export declare function detachStatusBar(bar: StatusBar): void;
7
+ export declare function disposeStatusBar(bar: StatusBar): void;
8
+ export declare function getStatusBarBackend(): StatusBarBackend;
9
+ export declare function getStatusBarHeight(): number;
10
+ export declare function getStatusBarInfo(out: StatusBarInfo): StatusBarInfo;
11
+ export declare function popStatusBarStyleEntry(handle: StatusBarStyleEntryHandle): void;
12
+ export declare function pushStatusBarStyleEntry(entry: Readonly<StatusBarStyleEntry>): StatusBarStyleEntryHandle;
13
+ export declare function setStatusBarBackend(backend: StatusBarBackend | null): void;
14
+ export declare function setStatusBarColor(color: number, animated?: boolean): void;
15
+ export declare function setStatusBarOverlaysContent(overlay: boolean): void;
16
+ export declare function setStatusBarStyle(style: StatusBarStyle): void;
17
+ export declare function setStatusBarVisible(visible: boolean, animation?: StatusBarAnimation): void;
18
+ //# sourceMappingURL=statusbar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"statusbar.d.ts","sourceRoot":"","sources":["../src/statusbar.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,SAAS,EACT,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,cAAc,EACd,mBAAmB,EACnB,yBAAyB,EAC1B,MAAM,iBAAiB,CAAC;AAKzB,wBAAgB,eAAe,CAAC,GAAG,EAAE,SAAS,GAAG,IAAI,CAQpD;AAGD,wBAAgB,eAAe,IAAI,SAAS,CAI3C;AAID,wBAAgB,mBAAmB,IAAI,aAAa,CAQnD;AAMD,wBAAgB,yBAAyB,IAAI,gBAAgB,CAsC5D;AAGD,wBAAgB,eAAe,CAAC,GAAG,EAAE,SAAS,GAAG,IAAI,CAMpD;AAID,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,SAAS,GAAG,IAAI,CAErD;AAGD,wBAAgB,mBAAmB,IAAI,gBAAgB,CAGtD;AAOD,wBAAgB,kBAAkB,IAAI,MAAM,CAE3C;AAID,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,aAAa,CAElE;AAID,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,yBAAyB,GAAG,IAAI,CAM9E;AAKD,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,QAAQ,CAAC,mBAAmB,CAAC,GAAG,yBAAyB,CAKvG;AAGD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,GAAG,IAAI,CAE1E;AAKD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,OAAO,GAAG,IAAI,CAEzE;AAGD,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAElE;AAGD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI,CAE7D;AAGD,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAE1F"}
@@ -0,0 +1,213 @@
1
+ import { createSignal, emitSignal } from '@flighthq/signals';
2
+ // Begins delivering OS-driven status bar changes to `bar`'s signals by subscribing to the active
3
+ // backend. Idempotent: a prior subscription is torn down first. Pair with detachStatusBar /
4
+ // disposeStatusBar.
5
+ export function attachStatusBar(bar) {
6
+ detachStatusBar(bar);
7
+ const backend = getStatusBarBackend();
8
+ const unsubscribe = backend.subscribe(() => {
9
+ const info = backend.getInfo(_scratchInfo);
10
+ emitSignal(bar.onChange, info);
11
+ });
12
+ _subscriptions.set(bar, unsubscribe);
13
+ }
14
+ // Allocates a StatusBar event entity with inert signals; call attachStatusBar to start delivery.
15
+ export function createStatusBar() {
16
+ return {
17
+ onChange: createSignal(),
18
+ };
19
+ }
20
+ // Allocates a zeroed StatusBarInfo, suitable as the `out` for getStatusBarInfo.
21
+ // height defaults to -1 (unknown), color to 0 (transparent black), style to 'default'.
22
+ export function createStatusBarInfo() {
23
+ return {
24
+ color: 0,
25
+ height: -1,
26
+ overlaysContent: false,
27
+ style: 'default',
28
+ visible: true,
29
+ };
30
+ }
31
+ // Builds the default web backend. Web pages have no true status bar, so only setBackgroundColor does
32
+ // anything observable: it upserts a <meta name="theme-color"> hint (alpha dropped). The rest no-op
33
+ // until a native host registers a backend. getInfo reads from the current theme-color meta where
34
+ // present and returns safe defaults for fields the web cannot know (height = -1, visible = true).
35
+ export function createWebStatusBarBackend() {
36
+ return {
37
+ getInfo(out) {
38
+ // Read back the theme-color meta if present; otherwise 0 (no hint set).
39
+ out.color = _webReadThemeColor();
40
+ out.height = -1;
41
+ out.overlaysContent = false;
42
+ out.style = 'default';
43
+ out.visible = true;
44
+ return out;
45
+ },
46
+ setBackgroundColor(color, _animated) {
47
+ // Derive #rrggbb from the top 24 bits of the packed RGBA integer; alpha is dropped.
48
+ if (typeof document === 'undefined')
49
+ return;
50
+ const head = document.head;
51
+ if (head === null || head === undefined)
52
+ return;
53
+ let meta = head.querySelector('meta[name="theme-color"]');
54
+ if (meta === null) {
55
+ meta = document.createElement('meta');
56
+ meta.setAttribute('name', 'theme-color');
57
+ head.appendChild(meta);
58
+ }
59
+ meta.setAttribute('content', packedRgbaToHexColor(color));
60
+ },
61
+ setOverlaysContent(_overlay) {
62
+ // No web status bar; a native host is required to control content overlay.
63
+ },
64
+ setStyle(_style) {
65
+ // No web status bar; a native host (Capacitor/native shell) is required to honor style.
66
+ },
67
+ setVisible(_visible, _animation) {
68
+ // No web status bar; a native host is required to show/hide it.
69
+ },
70
+ subscribe(_listener) {
71
+ // No OS-driven status bar events on web; return a no-op unsubscribe.
72
+ return () => { };
73
+ },
74
+ };
75
+ }
76
+ // Stops delivery to `bar` and forgets its subscription. Safe to call when not attached.
77
+ export function detachStatusBar(bar) {
78
+ const unsubscribe = _subscriptions.get(bar);
79
+ if (unsubscribe !== undefined) {
80
+ unsubscribe();
81
+ _subscriptions.delete(bar);
82
+ }
83
+ }
84
+ // Releases `bar` for garbage collection by detaching its backend subscription. The signals remain
85
+ // plain GC-managed memory afterward.
86
+ export function disposeStatusBar(bar) {
87
+ detachStatusBar(bar);
88
+ }
89
+ // The active status bar backend, or a lazily-created web default. There is always a backend.
90
+ export function getStatusBarBackend() {
91
+ if (_backend === null)
92
+ _backend = createWebStatusBarBackend();
93
+ return _backend;
94
+ }
95
+ // Returns the status bar height in CSS pixels, or -1 when the host does not report it (web,
96
+ // desktops). Convenience over getStatusBarInfo.
97
+ // NOTE: On notched/island devices, the safe-area top inset (owned by @flighthq/device) may differ
98
+ // from the status bar height. Use device.getSafeAreaInsets().top for layout-safe top padding;
99
+ // use getStatusBarHeight() when you specifically need the status bar element's intrinsic height.
100
+ export function getStatusBarHeight() {
101
+ return getStatusBarBackend().getInfo(_scratchInfo).height;
102
+ }
103
+ // Fills `out` with the current status bar state snapshot and returns it. Alias-safe: `out` may
104
+ // be the same object as any internal scratch.
105
+ export function getStatusBarInfo(out) {
106
+ return getStatusBarBackend().getInfo(out);
107
+ }
108
+ // Removes the style stack entry identified by `handle`. If the handle is unknown or invalid,
109
+ // this is a no-op. The top entry (or baseline) is re-applied after removal.
110
+ export function popStatusBarStyleEntry(handle) {
111
+ if (handle === INVALID_HANDLE)
112
+ return;
113
+ const idx = _styleStack.findIndex((e) => e.handle === handle);
114
+ if (idx === -1)
115
+ return;
116
+ _styleStack.splice(idx, 1);
117
+ _applyTopStyleEntry();
118
+ }
119
+ // Pushes a style stack entry, returns an opaque handle for later pop. Nested components can push
120
+ // entries and restore the previous state on unmount without global last-write-wins clashes.
121
+ // Unset fields fall through to the next entry down the stack (last pushed wins per field).
122
+ export function pushStatusBarStyleEntry(entry) {
123
+ const handle = _nextHandle++;
124
+ _styleStack.push({ handle, entry });
125
+ _applyTopStyleEntry();
126
+ return handle;
127
+ }
128
+ // Installs a native host status bar backend; pass null to fall back to the web default.
129
+ export function setStatusBarBackend(backend) {
130
+ _backend = backend;
131
+ }
132
+ // Sets the status bar background color from a packed RGBA integer (0xRRGGBBAA). On web this
133
+ // updates the theme-color hint; alpha is ignored. Set animated to true for a smooth transition
134
+ // (native hosts only; no-op on web).
135
+ export function setStatusBarColor(color, animated) {
136
+ getStatusBarBackend().setBackgroundColor(color, animated);
137
+ }
138
+ // Controls whether content draws under the status bar. No-op on web.
139
+ export function setStatusBarOverlaysContent(overlay) {
140
+ getStatusBarBackend().setOverlaysContent(overlay);
141
+ }
142
+ // Sets the status bar foreground style ('light' | 'dark' | 'default'). No-op on web.
143
+ export function setStatusBarStyle(style) {
144
+ getStatusBarBackend().setStyle(style);
145
+ }
146
+ // Shows or hides the status bar. animation controls the transition; defaults to 'none'. No-op on web.
147
+ export function setStatusBarVisible(visible, animation) {
148
+ getStatusBarBackend().setVisible(visible, animation);
149
+ }
150
+ // ---- module-level state ----
151
+ let _backend = null;
152
+ let _nextHandle = 1;
153
+ const _scratchInfo = createStatusBarInfo();
154
+ const _styleStack = [];
155
+ const _subscriptions = new WeakMap();
156
+ const INVALID_HANDLE = -1;
157
+ // Merges the style stack top-down (last pushed = highest priority per field) and applies the
158
+ // merged result to the active backend. Falls through to no-ops where no entry sets a field.
159
+ function _applyTopStyleEntry() {
160
+ const backend = getStatusBarBackend();
161
+ let style;
162
+ let visible;
163
+ let color;
164
+ let overlaysContent;
165
+ let animation;
166
+ // Stack is in push order; iterate from last pushed (top) to earliest (bottom).
167
+ for (let i = _styleStack.length - 1; i >= 0; i--) {
168
+ const e = _styleStack[i].entry;
169
+ if (style === undefined && e.style !== undefined)
170
+ style = e.style;
171
+ if (visible === undefined && e.visible !== undefined)
172
+ visible = e.visible;
173
+ if (color === undefined && e.color !== undefined)
174
+ color = e.color;
175
+ if (overlaysContent === undefined && e.overlaysContent !== undefined)
176
+ overlaysContent = e.overlaysContent;
177
+ if (animation === undefined && e.animation !== undefined)
178
+ animation = e.animation;
179
+ }
180
+ if (style !== undefined)
181
+ backend.setStyle(style);
182
+ if (visible !== undefined)
183
+ backend.setVisible(visible, animation ?? 'none');
184
+ if (color !== undefined)
185
+ backend.setBackgroundColor(color, false);
186
+ if (overlaysContent !== undefined)
187
+ backend.setOverlaysContent(overlaysContent);
188
+ }
189
+ function packedRgbaToHexColor(color) {
190
+ const rgb = (color >>> 8) & 0xffffff;
191
+ return '#' + rgb.toString(16).padStart(6, '0');
192
+ }
193
+ // Reads the current theme-color meta content and parses it back to a packed RGBA integer.
194
+ // Returns 0 if no meta is present or the value cannot be parsed.
195
+ function _webReadThemeColor() {
196
+ if (typeof document === 'undefined')
197
+ return 0;
198
+ const meta = document.head?.querySelector('meta[name="theme-color"]');
199
+ if (meta === null || meta === undefined)
200
+ return 0;
201
+ const content = meta.getAttribute('content');
202
+ if (content === null || !content.startsWith('#'))
203
+ return 0;
204
+ const hex = content.slice(1);
205
+ if (hex.length !== 6)
206
+ return 0;
207
+ const rgb = parseInt(hex, 16);
208
+ if (isNaN(rgb))
209
+ return 0;
210
+ // Reconstruct as 0xRRGGBBFF (alpha fully opaque since web drops alpha).
211
+ return ((rgb << 8) | 0xff) >>> 0;
212
+ }
213
+ //# sourceMappingURL=statusbar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"statusbar.js","sourceRoot":"","sources":["../src/statusbar.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@flighthq/statusbar",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src/**/*.test.ts",
16
+ "!dist/**/*.test.js",
17
+ "!dist/**/*.test.d.ts",
18
+ "!dist/**/*.test.js.map",
19
+ "!dist/**/*.test.d.ts.map"
20
+ ],
21
+ "scripts": {
22
+ "build": "tsc -b",
23
+ "clean": "tsc -b --clean",
24
+ "test": "vitest run --config vitest.config.ts",
25
+ "test:watch": "vitest --watch --config vitest.config.ts",
26
+ "prepack": "npm run clean && npm run clean:dist && npm run build",
27
+ "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
28
+ },
29
+ "dependencies": {
30
+ "@flighthq/signals": "0.1.0",
31
+ "@flighthq/types": "0.1.0"
32
+ },
33
+ "devDependencies": {
34
+ "typescript": "^5.3.0"
35
+ },
36
+ "description": "Mobile status bar style, visibility, and color over a swappable web/native backend",
37
+ "sideEffects": false
38
+ }
@@ -0,0 +1,379 @@
1
+ import type { StatusBar, StatusBarBackend, StatusBarInfo, StatusBarStyle } from '@flighthq/types';
2
+
3
+ import {
4
+ attachStatusBar,
5
+ createStatusBar,
6
+ createStatusBarInfo,
7
+ createWebStatusBarBackend,
8
+ detachStatusBar,
9
+ disposeStatusBar,
10
+ getStatusBarBackend,
11
+ getStatusBarHeight,
12
+ getStatusBarInfo,
13
+ popStatusBarStyleEntry,
14
+ pushStatusBarStyleEntry,
15
+ setStatusBarBackend,
16
+ setStatusBarColor,
17
+ setStatusBarOverlaysContent,
18
+ setStatusBarStyle,
19
+ setStatusBarVisible,
20
+ } from './statusbar';
21
+
22
+ function fakeBackend(): StatusBarBackend & {
23
+ _emit(): void;
24
+ animatedColor: boolean | undefined;
25
+ animation: string | undefined;
26
+ color: number;
27
+ infoHeight: number;
28
+ overlay: boolean;
29
+ style: StatusBarStyle;
30
+ subscribeCallCount: number;
31
+ visible: boolean;
32
+ } {
33
+ let listener: (() => void) | null = null;
34
+ return {
35
+ animatedColor: undefined,
36
+ animation: undefined,
37
+ color: 0,
38
+ infoHeight: 42,
39
+ overlay: false,
40
+ style: 'default',
41
+ subscribeCallCount: 0,
42
+ visible: true,
43
+ getInfo(out: StatusBarInfo): StatusBarInfo {
44
+ out.color = this.color;
45
+ out.height = this.infoHeight;
46
+ out.overlaysContent = this.overlay;
47
+ out.style = this.style;
48
+ out.visible = this.visible;
49
+ return out;
50
+ },
51
+ setBackgroundColor(color: number, animated?: boolean): void {
52
+ this.color = color;
53
+ this.animatedColor = animated;
54
+ },
55
+ setOverlaysContent(overlay: boolean): void {
56
+ this.overlay = overlay;
57
+ },
58
+ setStyle(style: StatusBarStyle): void {
59
+ this.style = style;
60
+ },
61
+ setVisible(visible: boolean, animation?: string): void {
62
+ this.visible = visible;
63
+ this.animation = animation;
64
+ },
65
+ subscribe(l: () => void): () => void {
66
+ this.subscribeCallCount++;
67
+ listener = l;
68
+ return () => {
69
+ listener = null;
70
+ };
71
+ },
72
+ // Trigger for tests: fire the subscription listener externally.
73
+ _emit(): void {
74
+ if (listener !== null) listener();
75
+ },
76
+ } as ReturnType<typeof fakeBackend>;
77
+ }
78
+
79
+ afterEach(() => {
80
+ setStatusBarBackend(null);
81
+ // Reset style stack state between tests by popping all entries (pop returns no-op on invalid)
82
+ for (let i = 0; i < 100; i++) popStatusBarStyleEntry(i);
83
+ });
84
+
85
+ describe('attachStatusBar', () => {
86
+ it('subscribes to the backend and emits onChange on change', () => {
87
+ const backend = fakeBackend();
88
+ setStatusBarBackend(backend);
89
+ const bar = createStatusBar();
90
+ const received: StatusBarInfo[] = [];
91
+ bar.onChange.emit = (info) => {
92
+ received.push({ ...info });
93
+ };
94
+ attachStatusBar(bar);
95
+ (backend as ReturnType<typeof fakeBackend>)._emit();
96
+ expect(received.length).toBe(1);
97
+ expect(received[0].height).toBe(42);
98
+ disposeStatusBar(bar);
99
+ });
100
+
101
+ it('is idempotent: re-attaching replaces the subscription', () => {
102
+ const backend = fakeBackend();
103
+ setStatusBarBackend(backend);
104
+ const bar = createStatusBar();
105
+ attachStatusBar(bar);
106
+ attachStatusBar(bar);
107
+ expect(backend.subscribeCallCount).toBe(2);
108
+ disposeStatusBar(bar);
109
+ });
110
+ });
111
+
112
+ describe('createStatusBar', () => {
113
+ it('returns a StatusBar with an inert onChange signal', () => {
114
+ const bar = createStatusBar();
115
+ expect(bar.onChange).not.toBeNull();
116
+ // Signals are inert (no listeners) until attachStatusBar is called.
117
+ });
118
+ });
119
+
120
+ describe('createStatusBarInfo', () => {
121
+ it('returns defaults with height -1 and style default', () => {
122
+ const info = createStatusBarInfo();
123
+ expect(info.height).toBe(-1);
124
+ expect(info.style).toBe('default');
125
+ expect(info.visible).toBe(true);
126
+ expect(info.overlaysContent).toBe(false);
127
+ expect(info.color).toBe(0);
128
+ });
129
+ });
130
+
131
+ describe('createWebStatusBarBackend', () => {
132
+ it('upserts a single theme-color meta with #rrggbb from the top 24 bits', () => {
133
+ document.head.querySelectorAll('meta[name="theme-color"]').forEach((m) => m.remove());
134
+ const backend = createWebStatusBarBackend();
135
+ backend.setBackgroundColor(0x11223344);
136
+ backend.setBackgroundColor(0xaabbccff);
137
+ const metas = document.head.querySelectorAll('meta[name="theme-color"]');
138
+ expect(metas.length).toBe(1);
139
+ expect(metas[0].getAttribute('content')).toBe('#aabbcc');
140
+ });
141
+
142
+ it('no-ops style/visible/overlay/subscribe without throwing', () => {
143
+ const backend = createWebStatusBarBackend();
144
+ expect(() => {
145
+ backend.setStyle('light');
146
+ backend.setVisible(false, 'slide');
147
+ backend.setOverlaysContent(true);
148
+ const unsub = backend.subscribe(() => {});
149
+ unsub();
150
+ }).not.toThrow();
151
+ });
152
+
153
+ it('getInfo returns -1 height and defaults when no theme-color meta', () => {
154
+ document.head.querySelectorAll('meta[name="theme-color"]').forEach((m) => m.remove());
155
+ const backend = createWebStatusBarBackend();
156
+ const info = createStatusBarInfo();
157
+ backend.getInfo(info);
158
+ expect(info.height).toBe(-1);
159
+ expect(info.visible).toBe(true);
160
+ expect(info.color).toBe(0);
161
+ });
162
+
163
+ it('getInfo reads back a previously set theme-color meta', () => {
164
+ document.head.querySelectorAll('meta[name="theme-color"]').forEach((m) => m.remove());
165
+ const backend = createWebStatusBarBackend();
166
+ backend.setBackgroundColor(0xff000000); // red, no alpha
167
+ const info = createStatusBarInfo();
168
+ backend.getInfo(info);
169
+ // Color should be reconstructed as 0xff0000ff (alpha forced to 0xff on web read-back)
170
+ expect(info.color).toBe(0xff0000ff);
171
+ });
172
+
173
+ it('subscribe returns a no-op unsubscribe function', () => {
174
+ const backend = createWebStatusBarBackend();
175
+ const unsub = backend.subscribe(() => {});
176
+ expect(() => unsub()).not.toThrow();
177
+ });
178
+ });
179
+
180
+ describe('detachStatusBar', () => {
181
+ it('stops subscription and is safe to call when not attached', () => {
182
+ const backend = fakeBackend();
183
+ setStatusBarBackend(backend);
184
+ const bar = createStatusBar();
185
+ expect(() => detachStatusBar(bar)).not.toThrow();
186
+ attachStatusBar(bar);
187
+ detachStatusBar(bar);
188
+ let emitCount = 0;
189
+ bar.onChange.emit = () => {
190
+ emitCount++;
191
+ };
192
+ (backend as ReturnType<typeof fakeBackend>)._emit();
193
+ expect(emitCount).toBe(0);
194
+ });
195
+ });
196
+
197
+ describe('disposeStatusBar', () => {
198
+ it('detaches subscription and releases the entity', () => {
199
+ const backend = fakeBackend();
200
+ setStatusBarBackend(backend);
201
+ const bar = createStatusBar();
202
+ attachStatusBar(bar);
203
+ disposeStatusBar(bar);
204
+ let emitCount = 0;
205
+ bar.onChange.emit = () => {
206
+ emitCount++;
207
+ };
208
+ (backend as ReturnType<typeof fakeBackend>)._emit();
209
+ expect(emitCount).toBe(0);
210
+ });
211
+ });
212
+
213
+ describe('getStatusBarBackend', () => {
214
+ it('falls back to a web backend', () => {
215
+ expect(getStatusBarBackend()).not.toBeNull();
216
+ });
217
+
218
+ it('returns the registered backend', () => {
219
+ const backend = fakeBackend();
220
+ setStatusBarBackend(backend);
221
+ expect(getStatusBarBackend()).toBe(backend);
222
+ });
223
+ });
224
+
225
+ describe('getStatusBarHeight', () => {
226
+ it('returns the height from the active backend', () => {
227
+ const backend = fakeBackend();
228
+ setStatusBarBackend(backend);
229
+ expect(getStatusBarHeight()).toBe(42);
230
+ });
231
+
232
+ it('returns -1 when the web backend reports unknown height', () => {
233
+ document.head.querySelectorAll('meta[name="theme-color"]').forEach((m) => m.remove());
234
+ // Web backend always returns -1 for height.
235
+ expect(getStatusBarHeight()).toBe(-1);
236
+ });
237
+ });
238
+
239
+ describe('getStatusBarInfo', () => {
240
+ it('fills the out parameter from the active backend', () => {
241
+ const backend = fakeBackend();
242
+ backend.color = 0x112233ff;
243
+ backend.style = 'light';
244
+ backend.visible = false;
245
+ backend.overlay = true;
246
+ setStatusBarBackend(backend);
247
+ const out = createStatusBarInfo();
248
+ const result = getStatusBarInfo(out);
249
+ expect(result).toBe(out); // returns `out`
250
+ expect(out.color).toBe(0x112233ff);
251
+ expect(out.style).toBe('light');
252
+ expect(out.visible).toBe(false);
253
+ expect(out.overlaysContent).toBe(true);
254
+ expect(out.height).toBe(42);
255
+ });
256
+
257
+ it('is alias-safe: out may be reused across calls', () => {
258
+ const backend = fakeBackend();
259
+ setStatusBarBackend(backend);
260
+ const out = createStatusBarInfo();
261
+ getStatusBarInfo(out);
262
+ backend.style = 'dark';
263
+ getStatusBarInfo(out);
264
+ expect(out.style).toBe('dark');
265
+ });
266
+ });
267
+
268
+ describe('popStatusBarStyleEntry', () => {
269
+ it('no-ops for unknown or invalid handles', () => {
270
+ expect(() => popStatusBarStyleEntry(-1)).not.toThrow();
271
+ expect(() => popStatusBarStyleEntry(99999)).not.toThrow();
272
+ });
273
+
274
+ it('removes the entry and re-applies the stack', () => {
275
+ const backend = fakeBackend();
276
+ setStatusBarBackend(backend);
277
+ const handle = pushStatusBarStyleEntry({ style: 'dark' });
278
+ expect(backend.style).toBe('dark');
279
+ popStatusBarStyleEntry(handle);
280
+ // After pop the backend should have been re-applied with the empty stack (no-op for style).
281
+ // The key invariant: the push was applied, the pop does not throw.
282
+ });
283
+ });
284
+
285
+ describe('pushStatusBarStyleEntry', () => {
286
+ it('applies the entry to the active backend', () => {
287
+ const backend = fakeBackend();
288
+ setStatusBarBackend(backend);
289
+ pushStatusBarStyleEntry({ style: 'light', visible: false });
290
+ expect(backend.style).toBe('light');
291
+ expect(backend.visible).toBe(false);
292
+ });
293
+
294
+ it('later entries win per-field over earlier entries', () => {
295
+ const backend = fakeBackend();
296
+ setStatusBarBackend(backend);
297
+ pushStatusBarStyleEntry({ style: 'dark' });
298
+ pushStatusBarStyleEntry({ style: 'light' });
299
+ expect(backend.style).toBe('light');
300
+ });
301
+
302
+ it('returns unique handles', () => {
303
+ const backend = fakeBackend();
304
+ setStatusBarBackend(backend);
305
+ const h1 = pushStatusBarStyleEntry({ style: 'dark' });
306
+ const h2 = pushStatusBarStyleEntry({ style: 'light' });
307
+ expect(h1).not.toBe(h2);
308
+ popStatusBarStyleEntry(h1);
309
+ popStatusBarStyleEntry(h2);
310
+ });
311
+
312
+ it('fields not set fall through to lower entries', () => {
313
+ const backend = fakeBackend();
314
+ setStatusBarBackend(backend);
315
+ pushStatusBarStyleEntry({ style: 'dark' });
316
+ pushStatusBarStyleEntry({ visible: false }); // no style set → falls through
317
+ expect(backend.style).toBe('dark');
318
+ expect(backend.visible).toBe(false);
319
+ });
320
+ });
321
+
322
+ describe('setStatusBarBackend', () => {
323
+ it('clears back to the web fallback when passed null', () => {
324
+ setStatusBarBackend(fakeBackend());
325
+ setStatusBarBackend(null);
326
+ expect(getStatusBarBackend()).not.toBeNull();
327
+ });
328
+ });
329
+
330
+ describe('setStatusBarColor', () => {
331
+ it('forwards the packed color to the active backend', () => {
332
+ const backend = fakeBackend();
333
+ setStatusBarBackend(backend);
334
+ setStatusBarColor(0x123456ff);
335
+ expect(backend.color).toBe(0x123456ff);
336
+ expect(backend.animatedColor).toBeUndefined();
337
+ });
338
+
339
+ it('forwards the animated flag when provided', () => {
340
+ const backend = fakeBackend();
341
+ setStatusBarBackend(backend);
342
+ setStatusBarColor(0x123456ff, true);
343
+ expect(backend.animatedColor).toBe(true);
344
+ });
345
+ });
346
+
347
+ describe('setStatusBarOverlaysContent', () => {
348
+ it('forwards overlay to the active backend', () => {
349
+ const backend = fakeBackend();
350
+ setStatusBarBackend(backend);
351
+ setStatusBarOverlaysContent(true);
352
+ expect(backend.overlay).toBe(true);
353
+ });
354
+ });
355
+
356
+ describe('setStatusBarStyle', () => {
357
+ it('forwards style to the active backend', () => {
358
+ const backend = fakeBackend();
359
+ setStatusBarBackend(backend);
360
+ setStatusBarStyle('dark');
361
+ expect(backend.style).toBe('dark');
362
+ });
363
+ });
364
+
365
+ describe('setStatusBarVisible', () => {
366
+ it('forwards visibility to the active backend', () => {
367
+ const backend = fakeBackend();
368
+ setStatusBarBackend(backend);
369
+ setStatusBarVisible(false);
370
+ expect(backend.visible).toBe(false);
371
+ });
372
+
373
+ it('forwards animation parameter to the active backend', () => {
374
+ const backend = fakeBackend();
375
+ setStatusBarBackend(backend);
376
+ setStatusBarVisible(false, 'fade');
377
+ expect(backend.animation).toBe('fade');
378
+ });
379
+ });