@livx.cc/native-kit 0.20.0 → 0.22.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livx.cc/native-kit",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "Isomorphic native-capabilities kit for PWAs — same API in browser and in an appwrap native shell. Zero dependencies.",
5
5
  "license": "MIT",
6
6
  "author": "Elya Livshitz",
@@ -12,6 +12,7 @@ import { DeviceModule } from '../modules/device';
12
12
  import { GeoModule } from '../modules/geo';
13
13
  import { HapticsModule } from '../modules/haptics';
14
14
  import { HealthModule } from '../modules/health';
15
+ import { KeyboardModule } from '../modules/keyboard';
15
16
  import { LifecycleModule } from '../modules/lifecycle';
16
17
  import { MediaModule } from '../modules/media';
17
18
  import { MotionModule } from '../modules/motion';
@@ -70,6 +71,7 @@ export class NativeKit {
70
71
  public readonly haptics = new HapticsModule(this);
71
72
  public readonly share = new ShareModule(this);
72
73
  public readonly screen = new ScreenModule(this);
74
+ public readonly keyboard = new KeyboardModule(this);
73
75
  public readonly storage = new StorageModule(this);
74
76
  public readonly toast = new ToastModule(this);
75
77
  public readonly ui = new UiModule(this);
@@ -49,11 +49,13 @@ export class WebAdapter implements NativeKitAdapter {
49
49
  device: 'web',
50
50
  clipboard: navigator.clipboard ? 'web' : 'none',
51
51
  notifications: 'Notification' in window ? 'web' : 'none',
52
+ badge: typeof (navigator as any).setAppBadge === 'function' ? 'web' : 'none', // Badging API (PWA icon badge)
52
53
  biometrics: 'none',
53
54
  geo: 'geolocation' in navigator ? 'web' : 'none',
54
55
  photos: 'web',
55
56
  network: 'web',
56
57
  screen: 'wakeLock' in n ? 'web' : 'none',
58
+ keyboard: (window as any).visualViewport ? 'web' : 'none', // VisualViewport resize ≈ keyboard show/hide
57
59
  dialogs: 'web',
58
60
  reviews: 'none',
59
61
  themeColor: 'web', // the browser honors <meta name="theme-color"> itself
@@ -180,6 +182,11 @@ export class WebAdapter implements NativeKitAdapter {
180
182
  case 'screen.orientation.current':
181
183
  return (((screen as any)?.orientation?.type ?? '').startsWith('landscape') ? 'landscape' : 'portrait') as T;
182
184
 
185
+ case 'keyboard.hide':
186
+ // No programmatic IME on web — blurring the focused field dismisses it.
187
+ (document.activeElement as HTMLElement | null)?.blur?.();
188
+ return undefined as T;
189
+
183
190
  case 'reviews.requestReview':
184
191
  throw new KitError('UNSUPPORTED', 'No in-app review prompt on web');
185
192
 
@@ -371,6 +378,22 @@ export class WebAdapter implements NativeKitAdapter {
371
378
  so.addEventListener('change', fire);
372
379
  return () => so.removeEventListener('change', fire);
373
380
  }
381
+ if (event === 'keyboard.show' || event === 'keyboard.hide') {
382
+ const vv = (window as any).visualViewport;
383
+ if (!vv) return () => {};
384
+ let shown = false;
385
+ const onResize = () => {
386
+ // The keyboard shrinks the visual viewport; the hidden band ≈ its height.
387
+ const height = Math.max(0, window.innerHeight - vv.height - vv.offsetTop);
388
+ const nowShown = height > 120; // threshold filters URL-bar/toolbar resizes
389
+ if (nowShown === shown) return;
390
+ shown = nowShown;
391
+ if (event === 'keyboard.show' && nowShown) cb({ height: Math.round(height) });
392
+ if (event === 'keyboard.hide' && !nowShown) cb(undefined);
393
+ };
394
+ vv.addEventListener('resize', onResize);
395
+ return () => vv.removeEventListener('resize', onResize);
396
+ }
374
397
  if (event === 'app.pause' || event === 'app.resume') {
375
398
  const handler = () => {
376
399
  const paused = document.visibilityState === 'hidden';
package/src/index.ts CHANGED
@@ -22,6 +22,7 @@ export type {
22
22
  export type { ImpactStyle, NotifyType } from './modules/haptics';
23
23
  export type { SharePayload, ShareFile } from './modules/share';
24
24
  export type { Orientation, OrientationLock } from './modules/screen';
25
+ export type { KeyboardInfo } from './modules/keyboard';
25
26
  export type { DeviceInfo } from './modules/device';
26
27
  export type { ScheduleOptions } from './modules/notifications';
27
28
  export type { PushMessage, PushPlatform, PushToken } from './modules/push';
@@ -31,6 +31,22 @@ export class AppModule {
31
31
  return this.kit.capability('app');
32
32
  }
33
33
 
34
+ /** 'native' where the OS exposes an app-icon badge (iOS always; Android launcher-dependent →
35
+ * honest no-op) · 'web' where the Badging API exists · else 'none'. Branch on this, not try/catch. */
36
+ get badgeCapability() {
37
+ return this.kit.capability('badge');
38
+ }
39
+
40
+ /**
41
+ * Set (or clear, with `0`) the app-icon badge count — the small number on the home-screen icon.
42
+ * Convenience over the shared native badge path: iOS sets the springboard badge; Android is an
43
+ * honest no-op (launchers own badges); web uses the Badging API where present.
44
+ * Branch on {@link badgeCapability}.
45
+ */
46
+ badge(count: number): Promise<void> {
47
+ return this.kit.invoke('notifications.setBadge', { count });
48
+ }
49
+
34
50
  /** Install-level environment facts for analytics/identification (source, install id, timestamps). */
35
51
  environment(): Promise<AppEnvironment> {
36
52
  return this.kit.invoke('app.environment');
@@ -0,0 +1,44 @@
1
+ import type { NativeKit } from '../core/NativeKit';
2
+ import type { Unsubscribe } from '../core/types';
3
+
4
+ /** Payload of {@link KeyboardModule.onShow}: the on-screen keyboard's height in CSS pixels,
5
+ * so the page can pad/scroll content above it. */
6
+ export interface KeyboardInfo {
7
+ /** Keyboard height in CSS px (0 when unknown). */
8
+ height: number;
9
+ }
10
+
11
+ /**
12
+ * Software-keyboard control + observation — ONE API across platforms.
13
+ *
14
+ * The classic webview pain: the keyboard slides up and covers the focused input.
15
+ * Native emits `show` with the keyboard height (CSS px) and `hide`, so the page can
16
+ * lift content above it; `hide()` dismisses the keyboard programmatically.
17
+ *
18
+ * Native (iOS `UIKeyboardWillShow/Hide`, Android global-layout/IME insets) reports
19
+ * `capability === 'native'`. Web maps to the VisualViewport API where present
20
+ * (`'web'`), else `'none'`. Branch on {@link capability}, not try/catch.
21
+ */
22
+ export class KeyboardModule {
23
+ constructor(private kit: NativeKit) {}
24
+
25
+ /** 'native' on a shell · 'web' where VisualViewport exists · else 'none'. */
26
+ get capability() {
27
+ return this.kit.capability('keyboard');
28
+ }
29
+
30
+ /** Dismiss the software keyboard (resign first responder / hide the IME). */
31
+ hide(): Promise<void> {
32
+ return this.kit.invoke('keyboard.hide');
33
+ }
34
+
35
+ /** Keyboard shown — payload carries its height in CSS pixels. */
36
+ onShow(cb: (info: KeyboardInfo) => void): Unsubscribe {
37
+ return this.kit.on('keyboard.show', (p) => cb(p as KeyboardInfo));
38
+ }
39
+
40
+ /** Keyboard dismissed. */
41
+ onHide(cb: () => void): Unsubscribe {
42
+ return this.kit.on('keyboard.hide', () => cb());
43
+ }
44
+ }