@sigx/lynx-appearance 0.4.1

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) 2025-2026 Andreas Ekdahl
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.
@@ -0,0 +1,160 @@
1
+ package com.sigx.appearance
2
+
3
+ import android.app.Activity
4
+ import android.content.Context
5
+ import android.content.ContextWrapper
6
+ import android.content.res.Configuration
7
+ import android.graphics.Color
8
+ import android.os.Build
9
+ import android.util.Log
10
+ import androidx.core.view.WindowCompat
11
+ import androidx.core.view.WindowInsetsControllerCompat
12
+ import com.lynx.jsbridge.LynxMethod
13
+ import com.lynx.jsbridge.LynxModule
14
+ import com.lynx.react.bridge.Callback
15
+ import com.lynx.react.bridge.JavaOnlyMap
16
+ import com.lynx.react.bridge.ReadableMap
17
+
18
+ /**
19
+ * System-bar appearance setters + a sync getter for the current color scheme.
20
+ * JS usage:
21
+ * NativeModules.Appearance.setStatusBarStyle({ "style": "light" }, callback)
22
+ * NativeModules.Appearance.getColorScheme(callback)
23
+ *
24
+ * Status-bar / nav-bar style maps to `WindowInsetsControllerCompat`:
25
+ * - "light" style ⇒ light icons on a dark background ⇒
26
+ * isAppearanceLightStatusBars = false (the FLAG_LIGHT_STATUS_BAR meaning
27
+ * is inverse of its name — it controls whether the *background* is light,
28
+ * i.e. icons go dark).
29
+ * - "dark" style ⇒ dark icons ⇒ isAppearanceLightStatusBars = true.
30
+ */
31
+ class AppearanceModule(context: Context) : LynxModule(context) {
32
+
33
+ private companion object {
34
+ const val TAG = "AppearanceModule"
35
+ }
36
+
37
+ @LynxMethod
38
+ fun setStatusBarStyle(params: ReadableMap?, callback: Callback?) {
39
+ val style = params?.getString("style") ?: "default"
40
+ val activity = findActivity(mContext)
41
+ if (activity == null) {
42
+ callback?.invoke(errorMap("No hosting Activity found"))
43
+ return
44
+ }
45
+ activity.runOnUiThread {
46
+ try {
47
+ val controller = controller(activity)
48
+ controller.isAppearanceLightStatusBars = style == "dark"
49
+ callback?.invoke(okMap())
50
+ } catch (e: Throwable) {
51
+ Log.w(TAG, "setStatusBarStyle failed: ${e.message}")
52
+ callback?.invoke(errorMap(e.message ?: "setStatusBarStyle failed"))
53
+ }
54
+ }
55
+ }
56
+
57
+ @LynxMethod
58
+ fun setStatusBarBackgroundColor(params: ReadableMap?, callback: Callback?) {
59
+ val color = params?.getString("color")
60
+ val activity = findActivity(mContext)
61
+ if (activity == null) {
62
+ callback?.invoke(errorMap("No hosting Activity found"))
63
+ return
64
+ }
65
+ activity.runOnUiThread {
66
+ try {
67
+ // Setting statusBarColor on API 35+ (Android 15) does nothing —
68
+ // edge-to-edge is enforced and the system bar background is
69
+ // always transparent. Callers should overlay their own
70
+ // background view instead.
71
+ @Suppress("DEPRECATION")
72
+ activity.window.statusBarColor = parseColorOrTransparent(color)
73
+ callback?.invoke(okMap())
74
+ } catch (e: Throwable) {
75
+ Log.w(TAG, "setStatusBarBackgroundColor failed: ${e.message}")
76
+ callback?.invoke(errorMap(e.message ?: "setStatusBarBackgroundColor failed"))
77
+ }
78
+ }
79
+ }
80
+
81
+ @LynxMethod
82
+ fun setNavigationBarStyle(params: ReadableMap?, callback: Callback?) {
83
+ val style = params?.getString("style") ?: "default"
84
+ val color = if (params?.hasKey("color") == true) params.getString("color") else null
85
+ val activity = findActivity(mContext)
86
+ if (activity == null) {
87
+ callback?.invoke(errorMap("No hosting Activity found"))
88
+ return
89
+ }
90
+ activity.runOnUiThread {
91
+ try {
92
+ val controller = controller(activity)
93
+ controller.isAppearanceLightNavigationBars = style == "dark"
94
+ if (color != null) {
95
+ @Suppress("DEPRECATION")
96
+ activity.window.navigationBarColor = parseColorOrTransparent(color)
97
+ }
98
+ callback?.invoke(okMap())
99
+ } catch (e: Throwable) {
100
+ Log.w(TAG, "setNavigationBarStyle failed: ${e.message}")
101
+ callback?.invoke(errorMap(e.message ?: "setNavigationBarStyle failed"))
102
+ }
103
+ }
104
+ }
105
+
106
+ @LynxMethod
107
+ fun getColorScheme(callback: Callback?) {
108
+ val night = mContext.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK
109
+ val scheme = if (night == Configuration.UI_MODE_NIGHT_YES) "dark" else "light"
110
+ val map = JavaOnlyMap()
111
+ map.putString("colorScheme", scheme)
112
+ callback?.invoke(map)
113
+ }
114
+
115
+ private fun controller(activity: Activity): WindowInsetsControllerCompat {
116
+ // WindowCompat.getInsetsController is the public-API-version-agnostic
117
+ // path; under the hood it uses WindowInsetsControllerCompat which
118
+ // delegates to WindowInsetsController on R+ and the deprecated
119
+ // SystemUiVisibility flags below.
120
+ return WindowCompat.getInsetsController(activity.window, activity.window.decorView)
121
+ }
122
+
123
+ private fun parseColorOrTransparent(hex: String?): Int {
124
+ if (hex.isNullOrBlank()) return Color.TRANSPARENT
125
+ return try {
126
+ Color.parseColor(hex)
127
+ } catch (_: IllegalArgumentException) {
128
+ Color.TRANSPARENT
129
+ }
130
+ }
131
+
132
+ private fun findActivity(ctx: Context?): Activity? {
133
+ var cur: Context? = ctx
134
+ while (cur is ContextWrapper) {
135
+ if (cur is Activity) return cur
136
+ cur = cur.baseContext
137
+ }
138
+ return null
139
+ }
140
+
141
+ private fun okMap(): JavaOnlyMap {
142
+ val m = JavaOnlyMap()
143
+ m.putBoolean("ok", true)
144
+ return m
145
+ }
146
+
147
+ private fun errorMap(message: String): JavaOnlyMap {
148
+ val m = JavaOnlyMap()
149
+ m.putBoolean("ok", false)
150
+ m.putString("reason", message)
151
+ return m
152
+ }
153
+
154
+ // Keep API 35+ awareness from triggering a "field never used" warning on
155
+ // older toolchains.
156
+ @Suppress("unused")
157
+ private fun targetsApi35Plus(activity: Activity): Boolean =
158
+ Build.VERSION.SDK_INT >= 35 &&
159
+ activity.applicationInfo.targetSdkVersion >= 35
160
+ }
@@ -0,0 +1,113 @@
1
+ package com.sigx.appearance
2
+
3
+ import android.content.ComponentCallbacks
4
+ import android.content.ComponentCallbacks2
5
+ import android.content.Context
6
+ import android.content.res.Configuration
7
+ import android.util.Log
8
+ import android.view.View
9
+ import com.lynx.react.bridge.JavaOnlyArray
10
+ import com.lynx.react.bridge.JavaOnlyMap
11
+ import com.lynx.tasm.LynxView
12
+ import com.lynx.tasm.TemplateData
13
+
14
+ /**
15
+ * Publishes the host's current system color scheme (`light` / `dark`) to JS via:
16
+ *
17
+ * 1. **`lynx.__globalProps.appearance`** — populated synchronously via
18
+ * [LynxView.updateGlobalProps] before MT first paint, so apps render the
19
+ * correct theme on cold start with no flash.
20
+ *
21
+ * 2. **`appearanceChanged` global event** — fired via [LynxView.sendGlobalEvent]
22
+ * after each republish. The JS `<AppearanceProvider>` subscribes via
23
+ * `lynx.getJSModule("GlobalEventEmitter").addListener` and updates its
24
+ * reactive signal so consumers (`<ThemeProvider followSystem>`) swap themes
25
+ * live when the user toggles dark mode in system settings.
26
+ *
27
+ * Lifecycle: one publisher per LynxView. [attach] is called by the autolinker
28
+ * via `GeneratedLifecyclePublishers.attachAll(lynxView)`. We register a
29
+ * `ComponentCallbacks2` on the application context to catch
30
+ * `onConfigurationChanged` (the only reliable signal for runtime dark-mode
31
+ * flips) and unregister on view detach.
32
+ */
33
+ class AppearancePublisher(private val lynxView: LynxView) {
34
+
35
+ private companion object {
36
+ const val TAG = "AppearancePublisher"
37
+ const val EVENT_NAME = "appearanceChanged"
38
+ }
39
+
40
+ private var lastScheme: String = ""
41
+ private var callbacks: ComponentCallbacks? = null
42
+
43
+ fun attach() {
44
+ // Seed synchronously so __globalProps is populated before MT first
45
+ // paint reads it. Reading the LynxView's own resources gives the host
46
+ // window's effective configuration (accounts for app-level
47
+ // android:configChanges and overrideConfiguration on the activity).
48
+ publish(lynxView.resources.configuration)
49
+
50
+ // Register ComponentCallbacks2 on the application so we get
51
+ // onConfigurationChanged when the user flips system dark mode in
52
+ // Settings → Display while the app is foregrounded. View-level
53
+ // dispatch is unreliable: onConfigurationChanged on a View only fires
54
+ // if the Activity declares android:configChanges, which most apps
55
+ // don't (and shouldn't).
56
+ val appCtx = lynxView.context.applicationContext
57
+ val cb = object : ComponentCallbacks2 {
58
+ override fun onConfigurationChanged(newConfig: Configuration) {
59
+ publish(newConfig)
60
+ }
61
+ override fun onLowMemory() {}
62
+ override fun onTrimMemory(level: Int) {}
63
+ }
64
+ appCtx.registerComponentCallbacks(cb)
65
+ callbacks = cb
66
+
67
+ // Detach safely when the LynxView leaves the window — covers
68
+ // activity recreation, single-LynxView teardown during HMR, etc.
69
+ lynxView.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
70
+ override fun onViewAttachedToWindow(v: View) {}
71
+ override fun onViewDetachedFromWindow(v: View) {
72
+ detach(appCtx)
73
+ v.removeOnAttachStateChangeListener(this)
74
+ }
75
+ })
76
+ }
77
+
78
+ private fun detach(appCtx: Context) {
79
+ callbacks?.let {
80
+ try {
81
+ appCtx.unregisterComponentCallbacks(it)
82
+ } catch (_: Throwable) {
83
+ // unregister can throw if the callback was never registered;
84
+ // ignore — there's nothing the host can do about it.
85
+ }
86
+ callbacks = null
87
+ }
88
+ }
89
+
90
+ private fun publish(config: Configuration) {
91
+ val night = config.uiMode and Configuration.UI_MODE_NIGHT_MASK
92
+ val scheme = if (night == Configuration.UI_MODE_NIGHT_YES) "dark" else "light"
93
+ if (scheme == lastScheme) return
94
+ lastScheme = scheme
95
+
96
+ val map: Map<String, Any> = mapOf("colorScheme" to scheme)
97
+
98
+ try {
99
+ // Channel 1: __globalProps — sync MT read at first paint.
100
+ lynxView.updateGlobalProps(TemplateData.fromMap(mapOf("appearance" to map)))
101
+
102
+ // Channel 2: appearanceChanged event — live BG update.
103
+ val payload = JavaOnlyMap().apply { putString("colorScheme", scheme) }
104
+ val params = JavaOnlyArray().apply { pushMap(payload) }
105
+ lynxView.sendGlobalEvent(EVENT_NAME, params)
106
+ } catch (e: Throwable) {
107
+ // Defensive — bridge calls during teardown can throw. The next
108
+ // configuration change will republish, so a dropped publish here
109
+ // is non-fatal.
110
+ Log.w(TAG, "publish failed: ${e.message}")
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,21 @@
1
+ import type { ColorScheme } from './types.js';
2
+ /**
3
+ * Key under `lynx.__globalProps` where the native publisher writes the
4
+ * appearance map. Single string shared by iOS / Android publishers, the JS
5
+ * reader, and tests.
6
+ */
7
+ export declare const GLOBAL_PROPS_KEY = "appearance";
8
+ export interface RawAppearanceProps {
9
+ colorScheme?: ColorScheme;
10
+ }
11
+ /**
12
+ * Synchronously read the current system color scheme from
13
+ * `lynx.__globalProps`. Returns `null` when the publisher hasn't populated
14
+ * yet (early cold start) or when running outside a Lynx host (web preview,
15
+ * SSR, tests). Callers should treat `null` as "unknown — fall back to your
16
+ * default theme".
17
+ *
18
+ * Safe on both BG and MT threads — `__globalProps` is mirrored across both.
19
+ */
20
+ export declare function readGlobalColorScheme(): ColorScheme | null;
21
+ //# sourceMappingURL=globals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"globals.d.ts","sourceRoot":"","sources":["../src/globals.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,eAAe,CAAC;AAE7C,MAAM,WAAW,kBAAkB;IACjC,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAYD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,IAAI,WAAW,GAAG,IAAI,CAO1D"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Key under `lynx.__globalProps` where the native publisher writes the
3
+ * appearance map. Single string shared by iOS / Android publishers, the JS
4
+ * reader, and tests.
5
+ */
6
+ export const GLOBAL_PROPS_KEY = 'appearance';
7
+ /**
8
+ * Synchronously read the current system color scheme from
9
+ * `lynx.__globalProps`. Returns `null` when the publisher hasn't populated
10
+ * yet (early cold start) or when running outside a Lynx host (web preview,
11
+ * SSR, tests). Callers should treat `null` as "unknown — fall back to your
12
+ * default theme".
13
+ *
14
+ * Safe on both BG and MT threads — `__globalProps` is mirrored across both.
15
+ */
16
+ export function readGlobalColorScheme() {
17
+ const lynxObj = typeof lynx !== 'undefined'
18
+ ? lynx
19
+ : undefined;
20
+ const raw = lynxObj?.__globalProps?.[GLOBAL_PROPS_KEY];
21
+ if (!raw || typeof raw !== 'object')
22
+ return null;
23
+ return raw.colorScheme === 'dark' ? 'dark' : raw.colorScheme === 'light' ? 'light' : null;
24
+ }
25
+ //# sourceMappingURL=globals.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"globals.js","sourceRoot":"","sources":["../src/globals.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,YAAY,CAAC;AAgB7C;;;;;;;;GAQG;AACH,MAAM,UAAU,qBAAqB;IACnC,MAAM,OAAO,GAA+B,OAAO,IAAI,KAAK,WAAW;QACrE,CAAC,CAAE,IAAkC;QACrC,CAAC,CAAC,SAAS,CAAC;IACd,MAAM,GAAG,GAAG,OAAO,EAAE,aAAa,EAAE,CAAC,gBAAgB,CAAmC,CAAC;IACzF,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACjD,OAAO,GAAG,CAAC,WAAW,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AAC5F,CAAC"}
@@ -0,0 +1,25 @@
1
+ import { type Computed, type PrimitiveSignal } from '@sigx/reactivity';
2
+ import type { ColorScheme } from './types.js';
3
+ type ColorSchemeRead = PrimitiveSignal<ColorScheme> | Computed<ColorScheme>;
4
+ /**
5
+ * BG-side reactive read of the current system color scheme. Returns the
6
+ * live signal — components calling this re-render when the user flips dark
7
+ * mode in system settings.
8
+ *
9
+ * If no `<AppearanceProvider>` is in scope, returns a fallback signal seeded
10
+ * from `lynx.__globalProps` once at first call (still gives the correct
11
+ * value on cold start; just doesn't update live). This lets ThemeProvider
12
+ * use the hook even when its consumer hasn't mounted `<AppearanceProvider>`.
13
+ */
14
+ export declare function useSystemColorScheme(): ColorSchemeRead;
15
+ /**
16
+ * MT-thread synchronous read of the current system color scheme. For use
17
+ * inside `'main thread'`-marked worklet bodies. Reads `lynx.__globalProps`
18
+ * directly — no subscription, callers re-evaluate per worklet invocation.
19
+ *
20
+ * Returns `'light'` when the publisher hasn't populated yet (cold start before
21
+ * first publish, or non-Lynx hosts).
22
+ */
23
+ export declare function useSystemColorSchemeMT(): ColorScheme;
24
+ export type { PrimitiveSignal, Computed } from '@sigx/reactivity';
25
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,QAAQ,EAAE,KAAK,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAG/E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,KAAK,eAAe,GAAG,eAAe,CAAC,WAAW,CAAC,GAAG,QAAQ,CAAC,WAAW,CAAC,CAAC;AAE5E;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,IAAI,eAAe,CAItD;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,IAAI,WAAW,CAEpD;AAYD,YAAY,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC"}
package/dist/hooks.js ADDED
@@ -0,0 +1,38 @@
1
+ import { signal } from '@sigx/reactivity';
2
+ import { useAppearanceContext } from './injectable.js';
3
+ import { readGlobalColorScheme } from './globals.js';
4
+ /**
5
+ * BG-side reactive read of the current system color scheme. Returns the
6
+ * live signal — components calling this re-render when the user flips dark
7
+ * mode in system settings.
8
+ *
9
+ * If no `<AppearanceProvider>` is in scope, returns a fallback signal seeded
10
+ * from `lynx.__globalProps` once at first call (still gives the correct
11
+ * value on cold start; just doesn't update live). This lets ThemeProvider
12
+ * use the hook even when its consumer hasn't mounted `<AppearanceProvider>`.
13
+ */
14
+ export function useSystemColorScheme() {
15
+ const ctx = useAppearanceContext();
16
+ if (ctx)
17
+ return ctx.colorScheme;
18
+ return fallbackSignal();
19
+ }
20
+ /**
21
+ * MT-thread synchronous read of the current system color scheme. For use
22
+ * inside `'main thread'`-marked worklet bodies. Reads `lynx.__globalProps`
23
+ * directly — no subscription, callers re-evaluate per worklet invocation.
24
+ *
25
+ * Returns `'light'` when the publisher hasn't populated yet (cold start before
26
+ * first publish, or non-Lynx hosts).
27
+ */
28
+ export function useSystemColorSchemeMT() {
29
+ return readGlobalColorScheme() ?? 'light';
30
+ }
31
+ let _fallback;
32
+ function fallbackSignal() {
33
+ if (!_fallback) {
34
+ _fallback = signal(readGlobalColorScheme() ?? 'light');
35
+ }
36
+ return _fallback;
37
+ }
38
+ //# sourceMappingURL=hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../src/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAuC,MAAM,kBAAkB,CAAC;AAC/E,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACvD,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAKrD;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,GAAG,GAAG,oBAAoB,EAAE,CAAC;IACnC,IAAI,GAAG;QAAE,OAAO,GAAG,CAAC,WAAW,CAAC;IAChC,OAAO,cAAc,EAAE,CAAC;AAC1B,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB;IACpC,OAAO,qBAAqB,EAAE,IAAI,OAAO,CAAC;AAC5C,CAAC;AAED,IAAI,SAAmD,CAAC;AACxD,SAAS,cAAc;IACrB,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,SAAS,GAAG,MAAM,CAAc,qBAAqB,EAAE,IAAI,OAAO,CAAC,CAAC;IACtE,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC"}
@@ -0,0 +1,10 @@
1
+ export { AppearanceProvider, APPEARANCE_EVENT } from './provider.js';
2
+ export type { AppearanceProviderProps } from './provider.js';
3
+ export { useAppearanceContext } from './injectable.js';
4
+ export type { AppearanceContextValue } from './injectable.js';
5
+ export { useSystemColorScheme, useSystemColorSchemeMT, } from './hooks.js';
6
+ export { setStatusBarStyle, setStatusBarBackgroundColor, setNavigationBarStyle, setSystemBarsStyle, isAvailable, } from './setters.js';
7
+ export { readGlobalColorScheme, GLOBAL_PROPS_KEY, } from './globals.js';
8
+ export type { RawAppearanceProps } from './globals.js';
9
+ export type { ColorScheme, SystemBarStyle, SystemBarsStyleInput, SetterResult, } from './types.js';
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AACrE,YAAY,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAE7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AACvD,YAAY,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAE9D,OAAO,EACL,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,iBAAiB,EACjB,2BAA2B,EAC3B,qBAAqB,EACrB,kBAAkB,EAClB,WAAW,GACZ,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,qBAAqB,EACrB,gBAAgB,GACjB,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAEvD,YAAY,EACV,WAAW,EACX,cAAc,EACd,oBAAoB,EACpB,YAAY,GACb,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ // Public API for @sigx/lynx-appearance.
2
+ export { AppearanceProvider, APPEARANCE_EVENT } from './provider.js';
3
+ export { useAppearanceContext } from './injectable.js';
4
+ export { useSystemColorScheme, useSystemColorSchemeMT, } from './hooks.js';
5
+ export { setStatusBarStyle, setStatusBarBackgroundColor, setNavigationBarStyle, setSystemBarsStyle, isAvailable, } from './setters.js';
6
+ export { readGlobalColorScheme, GLOBAL_PROPS_KEY, } from './globals.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,wCAAwC;AAExC,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAGrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAGvD,OAAO,EACL,oBAAoB,EACpB,sBAAsB,GACvB,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,iBAAiB,EACjB,2BAA2B,EAC3B,qBAAqB,EACrB,kBAAkB,EAClB,WAAW,GACZ,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,qBAAqB,EACrB,gBAAgB,GACjB,MAAM,cAAc,CAAC"}
@@ -0,0 +1,16 @@
1
+ import type { PrimitiveSignal } from '@sigx/reactivity';
2
+ import type { ColorScheme } from './types.js';
3
+ /** DI shape exposed by `<AppearanceProvider>`. */
4
+ export interface AppearanceContextValue {
5
+ /** BG-side reactive color scheme. Re-renders consumers on system flip. */
6
+ readonly colorScheme: PrimitiveSignal<ColorScheme>;
7
+ }
8
+ /**
9
+ * The DI handle for the appearance context.
10
+ *
11
+ * Factory returns `null` so consumers outside a provider get a clear signal
12
+ * (vs. a phantom `'light'` signal that silently never updates). Hooks in
13
+ * `./hooks.ts` wrap this with the null-check + fallback signal.
14
+ */
15
+ export declare const useAppearanceContext: import("@sigx/runtime-core").InjectableFunction<AppearanceContextValue | null>;
16
+ //# sourceMappingURL=injectable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"injectable.d.ts","sourceRoot":"","sources":["../src/injectable.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAE9C,kDAAkD;AAClD,MAAM,WAAW,sBAAsB;IACrC,0EAA0E;IAC1E,QAAQ,CAAC,WAAW,EAAE,eAAe,CAAC,WAAW,CAAC,CAAC;CACpD;AAED;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,gFAEhC,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { defineInjectable } from '@sigx/lynx';
2
+ /**
3
+ * The DI handle for the appearance context.
4
+ *
5
+ * Factory returns `null` so consumers outside a provider get a clear signal
6
+ * (vs. a phantom `'light'` signal that silently never updates). Hooks in
7
+ * `./hooks.ts` wrap this with the null-check + fallback signal.
8
+ */
9
+ export const useAppearanceContext = defineInjectable(() => null);
10
+ //# sourceMappingURL=injectable.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"injectable.js","sourceRoot":"","sources":["../src/injectable.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAU9C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,gBAAgB,CAClD,GAAG,EAAE,CAAC,IAAI,CACX,CAAC"}
@@ -0,0 +1,27 @@
1
+ import { type Define } from '@sigx/lynx';
2
+ /**
3
+ * Event name fired by the native publisher (iOS `AppearancePublisher.swift`,
4
+ * Android `AppearancePublisher.kt`) via `GlobalEventEmitter` every time the
5
+ * host's system color scheme flips. Payload mirrors the same map stored under
6
+ * `lynx.__globalProps.appearance`.
7
+ *
8
+ * Kept as a constant so iOS/Android publishers and the JS listener agree on
9
+ * a single string.
10
+ */
11
+ export declare const APPEARANCE_EVENT = "appearanceChanged";
12
+ export type AppearanceProviderProps = Define.Prop<'class', string, false> & Define.Prop<'style', Record<string, string | number>, false> & Define.Slot<'default'>;
13
+ /**
14
+ * Mount near the root of an app (above any consumer of `useSystemColorScheme`).
15
+ * Cheap — just one BG signal + one GlobalEventEmitter subscription. The
16
+ * native publisher writes `lynx.__globalProps.appearance` before MT first
17
+ * paint, so the initial value is correct on cold start with no flash.
18
+ *
19
+ * On platforms where the publisher isn't wired (web preview, tests),
20
+ * `readGlobalColorScheme()` returns `null` and we seed `'light'` as a safe
21
+ * default. Consumers can detect the unwired case via the live-update
22
+ * subscription never firing.
23
+ */
24
+ export declare const AppearanceProvider: import("@sigx/runtime-core").ComponentFactory<AppearanceProviderProps, void, {
25
+ default: () => import("@sigx/runtime-core").JSXElement | import("@sigx/runtime-core").JSXElement[] | null;
26
+ }>;
27
+ //# sourceMappingURL=provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAML,KAAK,MAAM,EACZ,MAAM,YAAY,CAAC;AAKpB;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,sBAAsB,CAAC;AAapD,MAAM,MAAM,uBAAuB,GAC/B,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,GACnC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,EAAE,KAAK,CAAC,GAC5D,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;AAE3B;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kBAAkB;;EAgC7B,CAAC"}
@@ -0,0 +1,59 @@
1
+ import { jsx as _jsx } from "@sigx/lynx/jsx-runtime";
2
+ import { component, defineProvide, signal, onMounted, onUnmounted, } from '@sigx/lynx';
3
+ import { useAppearanceContext } from './injectable.js';
4
+ import { readGlobalColorScheme } from './globals.js';
5
+ /**
6
+ * Event name fired by the native publisher (iOS `AppearancePublisher.swift`,
7
+ * Android `AppearancePublisher.kt`) via `GlobalEventEmitter` every time the
8
+ * host's system color scheme flips. Payload mirrors the same map stored under
9
+ * `lynx.__globalProps.appearance`.
10
+ *
11
+ * Kept as a constant so iOS/Android publishers and the JS listener agree on
12
+ * a single string.
13
+ */
14
+ export const APPEARANCE_EVENT = 'appearanceChanged';
15
+ /**
16
+ * Mount near the root of an app (above any consumer of `useSystemColorScheme`).
17
+ * Cheap — just one BG signal + one GlobalEventEmitter subscription. The
18
+ * native publisher writes `lynx.__globalProps.appearance` before MT first
19
+ * paint, so the initial value is correct on cold start with no flash.
20
+ *
21
+ * On platforms where the publisher isn't wired (web preview, tests),
22
+ * `readGlobalColorScheme()` returns `null` and we seed `'light'` as a safe
23
+ * default. Consumers can detect the unwired case via the live-update
24
+ * subscription never firing.
25
+ */
26
+ export const AppearanceProvider = component(({ props, slots }) => {
27
+ const initial = readGlobalColorScheme() ?? 'light';
28
+ const colorScheme = signal(initial);
29
+ const ctx = { colorScheme };
30
+ defineProvide(useAppearanceContext, () => ctx);
31
+ let listener;
32
+ let emitter;
33
+ onMounted(() => {
34
+ const lynxObj = typeof lynx !== 'undefined'
35
+ ? lynx
36
+ : undefined;
37
+ emitter = lynxObj?.getJSModule?.('GlobalEventEmitter');
38
+ if (!emitter)
39
+ return;
40
+ listener = (raw) => {
41
+ const next = normaliseScheme(raw);
42
+ if (next && next !== colorScheme.value)
43
+ colorScheme.value = next;
44
+ };
45
+ emitter.addListener(APPEARANCE_EVENT, listener);
46
+ });
47
+ onUnmounted(() => {
48
+ if (emitter && listener)
49
+ emitter.removeListener(APPEARANCE_EVENT, listener);
50
+ });
51
+ return () => (_jsx("view", { class: props.class, style: props.style, children: slots.default?.() }));
52
+ });
53
+ function normaliseScheme(raw) {
54
+ if (!raw || typeof raw !== 'object')
55
+ return null;
56
+ const v = raw['colorScheme'];
57
+ return v === 'dark' ? 'dark' : v === 'light' ? 'light' : null;
58
+ }
59
+ //# sourceMappingURL=provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":";AAAA,OAAO,EACL,SAAS,EACT,aAAa,EACb,MAAM,EACN,SAAS,EACT,WAAW,GAEZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,oBAAoB,EAA+B,MAAM,iBAAiB,CAAC;AACpF,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AAGrD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AAkBpD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,SAAS,CAA0B,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE;IACxF,MAAM,OAAO,GAAgB,qBAAqB,EAAE,IAAI,OAAO,CAAC;IAChE,MAAM,WAAW,GAAG,MAAM,CAAc,OAAO,CAAC,CAAC;IAEjD,MAAM,GAAG,GAA2B,EAAE,WAAW,EAAE,CAAC;IACpD,aAAa,CAAC,oBAAoB,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;IAE/C,IAAI,QAAiD,CAAC;IACtD,IAAI,OAA2C,CAAC;IAEhD,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,OAAO,GAAyB,OAAO,IAAI,KAAK,WAAW;YAC/D,CAAC,CAAE,IAA4B;YAC/B,CAAC,CAAC,SAAS,CAAC;QACd,OAAO,GAAG,OAAO,EAAE,WAAW,EAAE,CAAC,oBAAoB,CAAC,CAAC;QACvD,IAAI,CAAC,OAAO;YAAE,OAAO;QACrB,QAAQ,GAAG,CAAC,GAAY,EAAE,EAAE;YAC1B,MAAM,IAAI,GAAG,eAAe,CAAC,GAAG,CAAC,CAAC;YAClC,IAAI,IAAI,IAAI,IAAI,KAAK,WAAW,CAAC,KAAK;gBAAE,WAAW,CAAC,KAAK,GAAG,IAAI,CAAC;QACnE,CAAC,CAAC;QACF,OAAO,CAAC,WAAW,CAAC,gBAAgB,EAAE,QAAQ,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,WAAW,CAAC,GAAG,EAAE;QACf,IAAI,OAAO,IAAI,QAAQ;YAAE,OAAO,CAAC,cAAc,CAAC,gBAAgB,EAAE,QAAQ,CAAC,CAAC;IAC9E,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,EAAE,CAAC,CACX,eAAM,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,YACzC,KAAK,CAAC,OAAO,EAAE,EAAE,GACb,CACR,CAAC;AACJ,CAAC,CAAC,CAAC;AAEH,SAAS,eAAe,CAAC,GAAY;IACnC,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACjD,MAAM,CAAC,GAAI,GAA+B,CAAC,aAAa,CAAC,CAAC;IAC1D,OAAO,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;AAChE,CAAC"}
@@ -0,0 +1,58 @@
1
+ import type { SetterResult, SystemBarStyle, SystemBarsStyleInput } from './types.js';
2
+ /**
3
+ * Set the status-bar *content* tint (clock + icons).
4
+ *
5
+ * - `'light'` = light content (white-ish icons) — use when behind a dark theme.
6
+ * - `'dark'` = dark content — use when behind a light theme.
7
+ *
8
+ * On iOS the host VC must forward `preferredStatusBarStyle` to
9
+ * `AppearanceModule.preferredStatusBarStyle` for this to take effect — the
10
+ * lynx-cli iOS template wires that automatically.
11
+ *
12
+ * Resolves `{ ok: false, reason: 'unsupported' }` (never rejects) when the
13
+ * native module isn't registered — typical in web preview, SSR, tests, or
14
+ * apps that don't link the module. Fire-and-forget callers can therefore
15
+ * `void setStatusBarStyle(...)` without risking unhandled rejections.
16
+ */
17
+ export declare function setStatusBarStyle(style: SystemBarStyle): Promise<SetterResult>;
18
+ /**
19
+ * Android only — set the status-bar background color. Pass `null` (or
20
+ * omit / pass `'transparent'`) to clear. iOS resolves `{ ok: false,
21
+ * reason: 'unsupported' }` since iOS has no separate status-bar background.
22
+ *
23
+ * On Android 15+ (API 35) edge-to-edge is enforced and this call is a no-op
24
+ * at the system level — callers should overlay their own background view
25
+ * inside the safe-area top padding.
26
+ *
27
+ * Resolves `{ ok: false, reason: 'unsupported' }` (never rejects) when the
28
+ * native module isn't registered. See `setStatusBarStyle` for the rationale.
29
+ */
30
+ export declare function setStatusBarBackgroundColor(color: string | null): Promise<SetterResult>;
31
+ /**
32
+ * Android only — set the navigation-bar content tint + optional background.
33
+ * iOS resolves `{ ok: false, reason: 'unsupported' }` since there's no
34
+ * separate navigation bar.
35
+ *
36
+ * Resolves `{ ok: false, reason: 'unsupported' }` (never rejects) when the
37
+ * native module isn't registered. See `setStatusBarStyle` for the rationale.
38
+ */
39
+ export declare function setNavigationBarStyle(opts: {
40
+ style: SystemBarStyle;
41
+ color?: string;
42
+ }): Promise<SetterResult>;
43
+ /**
44
+ * Convenience: apply status-bar tint + (optionally) status-bar background +
45
+ * nav-bar tint in one call. Resolves to the aggregate result — `ok: false`
46
+ * if any leg returned a non-`unsupported` failure, with the first such
47
+ * failure's reason. `unsupported` legs (e.g. status-bar background on iOS)
48
+ * are intentionally ignored so a partially-supported platform can still
49
+ * report success for the legs that do apply.
50
+ *
51
+ * Fields are optional; omitting any leaves that surface untouched. Order is
52
+ * deterministic (statusBar → statusBarBackground → navigationBar) so the
53
+ * resolved reason on partial failure is unambiguous.
54
+ */
55
+ export declare function setSystemBarsStyle(opts: SystemBarsStyleInput): Promise<SetterResult>;
56
+ /** Quick check whether the native Appearance module is registered. */
57
+ export declare function isAvailable(): boolean;
58
+ //# sourceMappingURL=setters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"setters.d.ts","sourceRoot":"","sources":["../src/setters.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAMrF;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAG9E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,2BAA2B,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,OAAO,CAAC,YAAY,CAAC,CAGvF;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE;IAAE,KAAK,EAAE,cAAc,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,YAAY,CAAC,CAG5G;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,YAAY,CAAC,CAY1F;AAED,sEAAsE;AACtE,wBAAgB,WAAW,IAAI,OAAO,CAErC"}