@mostajs/browser-kit 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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # Changelog — @mostajs/browser-kit
2
+
3
+ ## [0.1.0] — 2026-06-23
4
+ ### Ajouté
5
+ - **Lot 1** : wake-lock (extrait du plateau VoxStudio/host), fullscreen, visibility, network, web-share, clipboard (repli execCommand), vibration, screen-orientation.
6
+ - **Lot 2** : geolocation, notifications, permissions, battery, idle-detection, media-devices (getUserMedia/getDisplayMedia/enumerate), sensors (orientation/motion + permission iOS), audio (beep/playSequence Web Audio), badging, file-system-access (repli input/download), contact-picker.
7
+ - `capabilities()` : carte de support 21 clés. Exports par sous-chemin (tree-shaking). 6 tests (dégradé Node + navigateur simulé). README exhaustif + how2use.
package/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # @mostajs/browser-kit
2
+
3
+ **Auteur** : Dr Hamid MADANI <drmdh@msn.com>
4
+
5
+ Façade unifiée des **Web APIs du navigateur** : une API cohérente, sûre et testable par-dessus une vingtaine de capacités natives. Membre de `mosta-browser-stack`.
6
+
7
+ **Principes (chaque capacité respecte le même contrat) :**
8
+ - **Feature-detection** — `isXSupported()` ou `capabilities()` avant tout appel.
9
+ - **Promesses** — pas de callbacks bruts ; les abonnements renvoient une **fonction de désabonnement**.
10
+ - **Dégradation gracieuse** — sur navigateur non compatible (ou sous **Node** : SSR/tests), rien ne jette : `false` / `null` / no-op.
11
+ - **Tree-shaking** — import global *ou* par sous-chemin : `@mostajs/browser-kit/wake-lock`.
12
+ - **Repli** quand pertinent — `clipboard` (execCommand), `file-system-access` (`<input type=file>` / `<a download>`).
13
+
14
+ ```bash
15
+ npm i @mostajs/browser-kit
16
+ ```
17
+
18
+ ---
19
+
20
+ ## Démarrage rapide
21
+
22
+ ```js
23
+ import { capabilities, createWakeLock, share, vibrate } from '@mostajs/browser-kit';
24
+
25
+ // 1) Que sait faire ce navigateur ?
26
+ console.log(capabilities());
27
+ // { wakeLock:true, fullscreen:true, share:true, clipboard:true, geolocation:true, … }
28
+
29
+ // 2) Empêcher la veille pendant une diffusion / lecture vidéo
30
+ const wl = createWakeLock({ onChange: (on) => console.log('écran maintenu allumé:', on) });
31
+ await wl.acquire();
32
+ wl.bindAutoReacquire(); // ré-acquiert tout seul au retour au premier plan
33
+ // … à la fin : await wl.dispose();
34
+
35
+ // 3) Partager + vibrer (mobile)
36
+ if (await share({ title: 'VoxStudio', url: location.href })) vibrate(50);
37
+ ```
38
+
39
+ > Sous Node, `capabilities()` renvoie tout à `false` et chaque appel dégrade sans erreur — le module est **SSR-safe** et **testable**.
40
+
41
+ ---
42
+
43
+ ## How to use — recettes par cas d'usage
44
+
45
+ ### Garder l'écran allumé pendant un live (cas VoxStudio)
46
+ ```js
47
+ import { createWakeLock } from '@mostajs/browser-kit/wake-lock';
48
+ const wl = createWakeLock();
49
+ startPublishBtn.onclick = async () => { /* … getUserMedia + WHIP … */ await wl.acquire(); wl.bindAutoReacquire(); };
50
+ stopBtn.onclick = async () => { await wl.dispose(); };
51
+ ```
52
+
53
+ ### Bouton plein écran (avec icône qui suit l'état)
54
+ ```js
55
+ import { toggleFullscreen, onFullscreenChange } from '@mostajs/browser-kit/fullscreen';
56
+ btn.onclick = () => toggleFullscreen(document.getElementById('player'));
57
+ onFullscreenChange((on) => btn.textContent = on ? '⤡ Quitter' : '⤢ Plein écran');
58
+ ```
59
+
60
+ ### Mettre l'app en pause quand l'onglet est caché (économie CPU/batterie)
61
+ ```js
62
+ import { onHidden, onVisible } from '@mostajs/browser-kit/visibility';
63
+ onHidden(() => player.pause());
64
+ onVisible(() => player.play());
65
+ ```
66
+
67
+ ### Bandeau « hors-ligne » + adaptation à la qualité réseau
68
+ ```js
69
+ import { onOffline, onOnline, connectionInfo } from '@mostajs/browser-kit/network';
70
+ onOffline(() => banner.show('Hors-ligne — reconnexion…'));
71
+ onOnline(() => banner.hide());
72
+ const net = connectionInfo(); // { effectiveType:'4g', downlink:10, saveData:false }
73
+ if (net?.saveData || net?.effectiveType === '2g') useLowQuality();
74
+ ```
75
+
76
+ ### Copier un lien d'invitation (repli automatique en HTTP)
77
+ ```js
78
+ import { writeText } from '@mostajs/browser-kit/clipboard';
79
+ copyBtn.onclick = async () => { if (await writeText(inviteUrl)) toast('Lien copié ✓'); };
80
+ ```
81
+
82
+ ### Notifier + pastille d'icône (PWA) à l'arrivée d'une question Q&A
83
+ ```js
84
+ import { notify } from '@mostajs/browser-kit/notifications';
85
+ import { setBadge } from '@mostajs/browser-kit/badging';
86
+ await notify('Nouvelle question', { body: q.text, icon: '/logo.png' });
87
+ await setBadge(unread);
88
+ ```
89
+
90
+ ### Position de l'utilisateur (carte d'un événement)
91
+ ```js
92
+ import { getCurrentPosition } from '@mostajs/browser-kit/geolocation';
93
+ const pos = await getCurrentPosition(); // { lat, lng, accuracy } ou null si refusé
94
+ if (pos) centerMap(pos.lat, pos.lng);
95
+ ```
96
+
97
+ ### Choisir caméra/micro + partage d'écran (plateau)
98
+ ```js
99
+ import { enumerateDevices, getUserMedia, getDisplayMedia, stopStream } from '@mostajs/browser-kit/media-devices';
100
+ const { cameras, microphones } = await enumerateDevices();
101
+ const cam = await getUserMedia({ video: { deviceId: cameras[0]?.deviceId }, audio: true });
102
+ const screen = await getDisplayMedia(); // partage d'écran
103
+ // … stopStream(cam) à l'arrêt
104
+ ```
105
+
106
+ ### Bip de notification synthétisé (sans fichier audio)
107
+ ```js
108
+ import { beep, playSequence } from '@mostajs/browser-kit/audio';
109
+ beep({ freq: 880, duration: 120 }); // après un geste utilisateur
110
+ playSequence([{ freq: 660, duration: 100 }, { freq: 990, duration: 140 }]);
111
+ ```
112
+
113
+ ---
114
+
115
+ ## Référence des capacités
116
+
117
+ `capabilities()` → carte booléenne de tout ce qui suit (**21 clés**).
118
+
119
+ ### Lot 1
120
+ | Module / sous-chemin | Exports |
121
+ |---|---|
122
+ | **`wake-lock`** | `isWakeLockSupported()` · `createWakeLock({onChange})` → `{ isSupported, acquire(), release(), isActive(), bindAutoReacquire()→off, dispose() }` |
123
+ | **`fullscreen`** | `isFullscreenSupported()` · `isFullscreen()` · `requestFullscreen(el?)` · `exitFullscreen()` · `toggleFullscreen(el?)` · `onFullscreenChange(cb)→off` |
124
+ | **`visibility`** | `isVisibilitySupported()` · `isVisible()` · `onVisibilityChange(cb)→off` · `onVisible(cb)` · `onHidden(cb)` |
125
+ | **`network`** | `isOnline()` · `isNetworkInfoSupported()` · `connectionInfo()` → `{effectiveType,downlink,rtt,saveData,type}` · `onNetworkChange(cb)` · `onOnline(cb)` · `onOffline(cb)` · `onConnectionChange(cb)` |
126
+ | **`web-share`** | `isShareSupported()` · `canShareFiles()` · `canShare(data)` · `share({title,text,url,files})` → bool |
127
+ | **`clipboard`** | `isClipboardSupported()` · `writeText(s)` → bool *(repli execCommand)* · `readText()` → string\|null |
128
+ | **`vibration`** | `isVibrationSupported()` · `vibrate(pattern)` · `stopVibration()` |
129
+ | **`screen-orientation`** | `isOrientationSupported()` · `currentOrientation()` → `{type,angle}` · `lockOrientation(type)` · `unlockOrientation()` · `onOrientationChange(cb)→off` |
130
+
131
+ ### Lot 2
132
+ | Module / sous-chemin | Exports |
133
+ |---|---|
134
+ | **`geolocation`** | `isGeolocationSupported()` · `getCurrentPosition(opts)` → `{lat,lng,accuracy,altitude,heading,speed,timestamp}`\|null · `watchPosition(cb,opts)`→off |
135
+ | **`notifications`** | `isNotificationSupported()` · `notificationPermission()` · `requestNotificationPermission()` → état · `notify(title,opts)` → Notification\|null |
136
+ | **`permissions`** | `isPermissionsSupported()` · `queryPermission(name)` → `'granted'\|'denied'\|'prompt'`\|null · `onPermissionChange(name,cb)`→off |
137
+ | **`battery`** | `isBatterySupported()` · `getBattery()` → `{level,charging,chargingTime,dischargingTime}`\|null · `onBatteryChange(cb)`→off |
138
+ | **`idle-detection`** | `isIdleDetectionSupported()` · `requestIdlePermission()` · `createIdleDetector({threshold,onChange})` → `{start(),stop()}` |
139
+ | **`media-devices`** | `isMediaDevicesSupported()` · `isDisplayMediaSupported()` · `enumerateDevices()` → `{cameras,microphones,speakers}` · `getUserMedia(c)` · `getDisplayMedia(c)` · `stopStream(s)` · `onDeviceChange(cb)`→off |
140
+ | **`sensors`** | `isOrientationSensorSupported()` · `isMotionSensorSupported()` · `requestSensorPermission()` *(iOS 13+)* · `onDeviceOrientation(cb)`→off · `onDeviceMotion(cb)`→off |
141
+ | **`audio`** | `isAudioSupported()` · `beep({freq,duration,type,volume})` → bool · `playSequence([{freq,duration,gap}])` *(Web Audio, sans fichier)* |
142
+ | **`badging`** | `isBadgingSupported()` · `setBadge(count?)` · `clearBadge()` *(pastille icône PWA)* |
143
+ | **`file-system-access`** | `isFileSystemAccessSupported()` · `openFile(opts)` → File\|null *(repli `<input file>`)* · `saveFile(blob,opts)` → bool *(repli `<a download>`)* |
144
+ | **`contact-picker`** | `isContactPickerSupported()` · `pickContacts(props,opts)` → contacts\|null · `availableContactProperties()` |
145
+
146
+ ---
147
+
148
+ ## Notes & limites
149
+ - **Contexte sécurisé (HTTPS)** requis par la plupart des API (Wake Lock, Clipboard async, getUserMedia, Geolocation…).
150
+ - **Geste utilisateur** requis pour : Fullscreen, Web Share, déblocage du contexte Audio, permission capteurs iOS.
151
+ - **Permissions** : Geolocation, Notifications, Camera/Micro, Idle Detection demandent l'accord de l'utilisateur — vérifier l'état via `permissions`.
152
+ - **Disponibilité variable** : `idle-detection`, `badging`, `contact-picker`, `file-system-access`, Network Information sont surtout sur Chromium. Toujours tester via `capabilities()` et prévoir le repli.
153
+
154
+ ## Tests
155
+ `npm test` — 6 tests (`@mostajs/mjs-unit`) : dégradé sous Node + navigateur simulé (`globalThis.navigator`/`Notification`).
156
+
157
+ Licence : AGPL-3.0-or-later.
package/llms.txt ADDED
@@ -0,0 +1,28 @@
1
+ # @mostajs/browser-kit
2
+ Façade des Web APIs du navigateur. ESM côté navigateur (dégrade sous Node : isSupported=false, SSR-safe). Chaque capacité : feature-detection (isXSupported) + API promise + cleanup (abonnement → fonction off). Exports par sous-chemin (tree-shaking).
3
+ capabilities() → carte booléenne 21 clés : wakeLock fullscreen visibility network share clipboard vibration orientation geolocation notifications permissions battery idleDetection mediaDevices displayMedia orientationSensor motionSensor audio badging fileSystemAccess contactPicker.
4
+
5
+ ## Lot 1
6
+ ./wake-lock isWakeLockSupported() ; createWakeLock({onChange}) → {isSupported,acquire(),release(),isActive(),bindAutoReacquire()→off,dispose()}
7
+ ./fullscreen isFullscreenSupported() isFullscreen() requestFullscreen(el?) exitFullscreen() toggleFullscreen(el?) onFullscreenChange(cb)→off
8
+ ./visibility isVisibilitySupported() isVisible() onVisibilityChange(cb)→off onVisible(cb) onHidden(cb)
9
+ ./network isOnline() isNetworkInfoSupported() connectionInfo()→{effectiveType,downlink,rtt,saveData,type} onNetworkChange/onOnline/onOffline/onConnectionChange(cb)→off
10
+ ./web-share isShareSupported() canShareFiles() canShare(data) share({title,text,url,files})→bool
11
+ ./clipboard isClipboardSupported() writeText(s)→bool (repli execCommand) readText()→string|null
12
+ ./vibration isVibrationSupported() vibrate(pattern) stopVibration()
13
+ ./screen-orientation isOrientationSupported() currentOrientation()→{type,angle} lockOrientation(type) unlockOrientation() onOrientationChange(cb)→off
14
+
15
+ ## Lot 2
16
+ ./geolocation isGeolocationSupported() getCurrentPosition(opts)→{lat,lng,accuracy,...}|null watchPosition(cb,opts)→off
17
+ ./notifications isNotificationSupported() notificationPermission() requestNotificationPermission()→état notify(title,opts)→Notification|null
18
+ ./permissions isPermissionsSupported() queryPermission(name)→state|null onPermissionChange(name,cb)→off
19
+ ./battery isBatterySupported() getBattery()→{level,charging,chargingTime,dischargingTime}|null onBatteryChange(cb)→off
20
+ ./idle-detection isIdleDetectionSupported() requestIdlePermission() createIdleDetector({threshold,onChange})→{start(),stop()}
21
+ ./media-devices isMediaDevicesSupported() isDisplayMediaSupported() enumerateDevices()→{cameras,microphones,speakers} getUserMedia(c) getDisplayMedia(c) stopStream(s) onDeviceChange(cb)→off
22
+ ./sensors isOrientationSensorSupported() isMotionSensorSupported() requestSensorPermission() (iOS13+) onDeviceOrientation(cb)→off onDeviceMotion(cb)→off
23
+ ./audio isAudioSupported() beep({freq,duration,type,volume})→bool playSequence([{freq,duration,gap}]) (Web Audio, sans fichier)
24
+ ./badging isBadgingSupported() setBadge(count?) clearBadge() (pastille PWA)
25
+ ./file-system-access isFileSystemAccessSupported() openFile(opts)→File|null (repli input) saveFile(blob,opts)→bool (repli a download)
26
+ ./contact-picker isContactPickerSupported() pickContacts(props,opts)→contacts|null availableContactProperties()
27
+
28
+ Notes : HTTPS requis (Wake Lock, Clipboard async, getUserMedia, Geolocation) ; geste utilisateur (Fullscreen, Share, Audio, capteurs iOS) ; idle/badging/contact/file-system/network-info surtout Chromium → tester via capabilities().
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@mostajs/browser-kit",
3
+ "version": "0.1.0",
4
+ "description": "Façade unifiée des Web APIs du navigateur (Wake Lock, Fullscreen, Visibility, Network, Web Share, Clipboard, Vibration, Screen Orientation…). Feature-detection + API promise + cleanup, dégradation gracieuse. ESM côté navigateur (se charge aussi sous Node : isSupported=false).",
5
+ "license": "AGPL-3.0-or-later",
6
+ "author": "Dr Hamid MADANI <drmdh@msn.com>",
7
+ "type": "module",
8
+ "main": "src/index.js",
9
+ "files": [
10
+ "src",
11
+ "llms.txt",
12
+ "README.md",
13
+ "CHANGELOG.md"
14
+ ],
15
+ "exports": {
16
+ ".": "./src/index.js",
17
+ "./wake-lock": "./src/wake-lock.js",
18
+ "./fullscreen": "./src/fullscreen.js",
19
+ "./visibility": "./src/visibility.js",
20
+ "./network": "./src/network.js",
21
+ "./web-share": "./src/web-share.js",
22
+ "./clipboard": "./src/clipboard.js",
23
+ "./vibration": "./src/vibration.js",
24
+ "./screen-orientation": "./src/screen-orientation.js",
25
+ "./geolocation": "./src/geolocation.js",
26
+ "./notifications": "./src/notifications.js",
27
+ "./permissions": "./src/permissions.js",
28
+ "./battery": "./src/battery.js",
29
+ "./idle-detection": "./src/idle-detection.js",
30
+ "./media-devices": "./src/media-devices.js",
31
+ "./sensors": "./src/sensors.js",
32
+ "./audio": "./src/audio.js",
33
+ "./badging": "./src/badging.js",
34
+ "./file-system-access": "./src/file-system-access.js",
35
+ "./contact-picker": "./src/contact-picker.js"
36
+ },
37
+ "keywords": [
38
+ "mostajs",
39
+ "browser",
40
+ "web-api",
41
+ "wake-lock",
42
+ "fullscreen",
43
+ "web-share",
44
+ "clipboard",
45
+ "pwa"
46
+ ],
47
+ "devDependencies": {
48
+ "@mostajs/mjs-unit": "^0.3.0"
49
+ },
50
+ "scripts": {
51
+ "test": "bash test-scripts/run-tests.sh"
52
+ }
53
+ }
package/src/audio.js ADDED
@@ -0,0 +1,36 @@
1
+ // @mostajs/browser-kit — Web Audio : bips/tonalités sans fichier. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ const AC = () => (typeof window !== 'undefined' ? window.AudioContext || window.webkitAudioContext : null);
3
+
4
+ export const isAudioSupported = () => !!AC();
5
+
6
+ let ctx = null;
7
+ function context() {
8
+ const Ctor = AC();
9
+ if (!Ctor) return null;
10
+ if (!ctx) ctx = new Ctor();
11
+ if (ctx.state === 'suspended') ctx.resume().catch(() => {}); // débloquer après un geste utilisateur
12
+ return ctx;
13
+ }
14
+
15
+ /** Joue une tonalité synthétisée. { freq=880, duration=150 (ms), type='sine', volume=0.2 }. → true/false. */
16
+ export function beep({ freq = 880, duration = 150, type = 'sine', volume = 0.2 } = {}) {
17
+ const c = context();
18
+ if (!c) return false;
19
+ try {
20
+ const osc = c.createOscillator();
21
+ const gain = c.createGain();
22
+ osc.type = type; osc.frequency.value = freq; gain.gain.value = volume;
23
+ osc.connect(gain); gain.connect(c.destination);
24
+ const now = c.currentTime;
25
+ osc.start(now);
26
+ gain.gain.setValueAtTime(volume, now);
27
+ gain.gain.exponentialRampToValueAtTime(0.0001, now + duration / 1000); // fondu pour éviter le clic
28
+ osc.stop(now + duration / 1000);
29
+ return true;
30
+ } catch { return false; }
31
+ }
32
+
33
+ /** Joue une séquence de bips : [{freq,duration,gap?}, …]. */
34
+ export async function playSequence(notes = []) {
35
+ for (const n of notes) { beep(n); await new Promise((r) => setTimeout(r, (n.duration || 150) + (n.gap || 40))); }
36
+ }
package/src/badging.js ADDED
@@ -0,0 +1,15 @@
1
+ // @mostajs/browser-kit — App Badging API (pastille sur l'icône PWA). Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ const hasNavigator = () => typeof navigator !== 'undefined';
3
+
4
+ export const isBadgingSupported = () => hasNavigator() && typeof navigator.setAppBadge === 'function';
5
+
6
+ /** Pose une pastille (nombre, ou pastille simple si count omis). → true/false. */
7
+ export async function setBadge(count) {
8
+ if (!isBadgingSupported()) return false;
9
+ try { await (count === undefined ? navigator.setAppBadge() : navigator.setAppBadge(count)); return true; } catch { return false; }
10
+ }
11
+
12
+ export async function clearBadge() {
13
+ if (!hasNavigator() || typeof navigator.clearAppBadge !== 'function') return false;
14
+ try { await navigator.clearAppBadge(); return true; } catch { return false; }
15
+ }
package/src/battery.js ADDED
@@ -0,0 +1,24 @@
1
+ // @mostajs/browser-kit — Battery Status API. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ const hasNavigator = () => typeof navigator !== 'undefined';
3
+
4
+ export const isBatterySupported = () => hasNavigator() && typeof navigator.getBattery === 'function';
5
+
6
+ const snapshot = (b) => ({ level: b.level, charging: b.charging, chargingTime: b.chargingTime, dischargingTime: b.dischargingTime });
7
+
8
+ /** État courant de la batterie. → { level, charging, chargingTime, dischargingTime } ou null. */
9
+ export async function getBattery() {
10
+ if (!isBatterySupported()) return null;
11
+ try { return snapshot(await navigator.getBattery()); } catch { return null; }
12
+ }
13
+
14
+ /** Abonne aux changements de batterie (niveau/charge). cb(snapshot). → désabonnement. */
15
+ export async function onBatteryChange(cb) {
16
+ if (!isBatterySupported()) return () => {};
17
+ try {
18
+ const b = await navigator.getBattery();
19
+ const handler = () => cb(snapshot(b));
20
+ const events = ['levelchange', 'chargingchange', 'chargingtimechange', 'dischargingtimechange'];
21
+ events.forEach((e) => b.addEventListener(e, handler));
22
+ return () => events.forEach((e) => b.removeEventListener(e, handler));
23
+ } catch { return () => {}; }
24
+ }
@@ -0,0 +1,27 @@
1
+ // @mostajs/browser-kit — Clipboard API (avec repli execCommand). Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ const hasNavigator = () => typeof navigator !== 'undefined';
3
+ const hasDocument = () => typeof document !== 'undefined';
4
+
5
+ export const isClipboardSupported = () => hasNavigator() && !!navigator.clipboard;
6
+
7
+ /** Copie un texte. Repli sur un <textarea> + execCommand si l'API async indisponible (http, vieux navigateur). → true/false. */
8
+ export async function writeText(text) {
9
+ if (isClipboardSupported() && navigator.clipboard.writeText) {
10
+ try { await navigator.clipboard.writeText(text); return true; } catch { /* repli */ }
11
+ }
12
+ if (!hasDocument()) return false;
13
+ try {
14
+ const ta = document.createElement('textarea');
15
+ ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0';
16
+ document.body.appendChild(ta); ta.select();
17
+ const ok = document.execCommand('copy');
18
+ document.body.removeChild(ta);
19
+ return ok;
20
+ } catch { return false; }
21
+ }
22
+
23
+ /** Lit le presse-papiers (nécessite permission + contexte sécurisé). → texte ou null. */
24
+ export async function readText() {
25
+ if (!isClipboardSupported() || !navigator.clipboard.readText) return null;
26
+ try { return await navigator.clipboard.readText(); } catch { return null; }
27
+ }
@@ -0,0 +1,18 @@
1
+ // @mostajs/browser-kit — Contact Picker API (Android Chrome). Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ const cp = () => (typeof navigator !== 'undefined' ? navigator.contacts : null);
3
+
4
+ export const isContactPickerSupported = () => !!(cp() && cp().select);
5
+
6
+ /** Ouvre le sélecteur de contacts. props = ['name','email','tel','address','icon']. → tableau de contacts ou null. */
7
+ export async function pickContacts(props = ['name', 'email', 'tel'], opts = { multiple: true }) {
8
+ const c = cp();
9
+ if (!c || !c.select) return null;
10
+ try { return await c.select(props, opts); } catch { return null; }
11
+ }
12
+
13
+ /** Propriétés sélectionnables par ce navigateur. → tableau ou []. */
14
+ export async function availableContactProperties() {
15
+ const c = cp();
16
+ if (!c || !c.getProperties) return [];
17
+ try { return await c.getProperties(); } catch { return []; }
18
+ }
@@ -0,0 +1,38 @@
1
+ // @mostajs/browser-kit — File System Access API (avec replis). Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ const hasWindow = () => typeof window !== 'undefined';
3
+ const hasDocument = () => typeof document !== 'undefined';
4
+
5
+ export const isFileSystemAccessSupported = () => hasWindow() && 'showOpenFilePicker' in window;
6
+
7
+ /** Ouvre un sélecteur de fichier. → File ou null (repli <input type=file>). opts = { types?, multiple? }. */
8
+ export async function openFile(opts = {}) {
9
+ if (isFileSystemAccessSupported()) {
10
+ try { const [h] = await window.showOpenFilePicker(opts); return await h.getFile(); } catch { return null; }
11
+ }
12
+ if (!hasDocument()) return null;
13
+ return new Promise((resolve) => {
14
+ const input = document.createElement('input');
15
+ input.type = 'file';
16
+ if (opts.accept) input.accept = opts.accept;
17
+ input.onchange = () => resolve(input.files?.[0] || null);
18
+ input.click();
19
+ });
20
+ }
21
+
22
+ /** Enregistre un Blob. → true/false (repli téléchargement <a download>). opts = { suggestedName?, types? }. */
23
+ export async function saveFile(blob, opts = {}) {
24
+ if (hasWindow() && 'showSaveFilePicker' in window) {
25
+ try {
26
+ const h = await window.showSaveFilePicker({ suggestedName: opts.suggestedName, types: opts.types });
27
+ const w = await h.createWritable(); await w.write(blob); await w.close(); return true;
28
+ } catch { return false; }
29
+ }
30
+ if (!hasDocument()) return false;
31
+ try {
32
+ const url = URL.createObjectURL(blob);
33
+ const a = document.createElement('a');
34
+ a.href = url; a.download = opts.suggestedName || 'download';
35
+ a.click(); setTimeout(() => URL.revokeObjectURL(url), 1000);
36
+ return true;
37
+ } catch { return false; }
38
+ }
@@ -0,0 +1,37 @@
1
+ // @mostajs/browser-kit — Fullscreen API. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ const hasDocument = () => typeof document !== 'undefined';
3
+
4
+ export const isFullscreenSupported = () =>
5
+ hasDocument() && !!(document.fullscreenEnabled || document.webkitFullscreenEnabled);
6
+
7
+ export const isFullscreen = () =>
8
+ hasDocument() && !!(document.fullscreenElement || document.webkitFullscreenElement);
9
+
10
+ export async function requestFullscreen(el) {
11
+ if (!hasDocument()) return false;
12
+ const target = el || document.documentElement;
13
+ const fn = target.requestFullscreen || target.webkitRequestFullscreen;
14
+ if (!fn) return false;
15
+ try { await fn.call(target); return true; } catch { return false; }
16
+ }
17
+
18
+ export async function exitFullscreen() {
19
+ if (!hasDocument()) return false;
20
+ const fn = document.exitFullscreen || document.webkitExitFullscreen;
21
+ if (!fn) return false;
22
+ try { await fn.call(document); return true; } catch { return false; }
23
+ }
24
+
25
+ export const toggleFullscreen = (el) => (isFullscreen() ? exitFullscreen() : requestFullscreen(el));
26
+
27
+ /** Abonne aux changements de plein écran. → fonction de désabonnement. */
28
+ export function onFullscreenChange(cb) {
29
+ if (!hasDocument()) return () => {};
30
+ const handler = () => cb(isFullscreen());
31
+ document.addEventListener('fullscreenchange', handler);
32
+ document.addEventListener('webkitfullscreenchange', handler);
33
+ return () => {
34
+ document.removeEventListener('fullscreenchange', handler);
35
+ document.removeEventListener('webkitfullscreenchange', handler);
36
+ };
37
+ }
@@ -0,0 +1,24 @@
1
+ // @mostajs/browser-kit — Geolocation API. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ const geo = () => (typeof navigator !== 'undefined' ? navigator.geolocation : null);
3
+
4
+ export const isGeolocationSupported = () => !!geo();
5
+
6
+ const toPos = (p) => ({
7
+ lat: p.coords.latitude, lng: p.coords.longitude, accuracy: p.coords.accuracy,
8
+ altitude: p.coords.altitude, heading: p.coords.heading, speed: p.coords.speed, timestamp: p.timestamp,
9
+ });
10
+
11
+ /** Position courante. → { lat, lng, accuracy, … } ou null. */
12
+ export function getCurrentPosition(opts = { enableHighAccuracy: true, timeout: 10000 }) {
13
+ const g = geo();
14
+ if (!g) return Promise.resolve(null);
15
+ return new Promise((resolve) => g.getCurrentPosition((p) => resolve(toPos(p)), () => resolve(null), opts));
16
+ }
17
+
18
+ /** Suit la position. cb({lat,lng,…}). → fonction d'arrêt. */
19
+ export function watchPosition(cb, opts = { enableHighAccuracy: true }) {
20
+ const g = geo();
21
+ if (!g) return () => {};
22
+ const id = g.watchPosition((p) => cb(toPos(p)), () => {}, opts);
23
+ return () => g.clearWatch(id);
24
+ }
@@ -0,0 +1,31 @@
1
+ // @mostajs/browser-kit — Idle Detection API. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ const hasIdle = () => typeof globalThis !== 'undefined' && 'IdleDetector' in globalThis;
3
+
4
+ export const isIdleDetectionSupported = () => hasIdle();
5
+
6
+ /** Demande la permission idle-detection. → 'granted' | 'denied'. */
7
+ export async function requestIdlePermission() {
8
+ if (!hasIdle()) return 'denied';
9
+ try { return await globalThis.IdleDetector.requestPermission(); } catch { return 'denied'; }
10
+ }
11
+
12
+ /**
13
+ * Détecteur d'inactivité utilisateur/écran.
14
+ * threshold : ms (≥ 60000). onChange({ user:'active'|'idle', screen:'locked'|'unlocked' }).
15
+ * → { start(), stop() }.
16
+ */
17
+ export function createIdleDetector({ threshold = 60000, onChange } = {}) {
18
+ let detector = null, ctrl = null;
19
+ async function start() {
20
+ if (!hasIdle()) return false;
21
+ try {
22
+ ctrl = new AbortController();
23
+ detector = new globalThis.IdleDetector();
24
+ detector.addEventListener('change', () => onChange?.({ user: detector.userState, screen: detector.screenState }));
25
+ await detector.start({ threshold, signal: ctrl.signal });
26
+ return true;
27
+ } catch { return false; }
28
+ }
29
+ function stop() { try { ctrl?.abort(); } catch {} detector = null; ctrl = null; }
30
+ return { start, stop };
31
+ }
package/src/index.js ADDED
@@ -0,0 +1,70 @@
1
+ // @mostajs/browser-kit — façade des Web APIs du navigateur. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ // Chaque capacité : feature-detection (isXSupported) + API promise + cleanup. ESM navigateur (dégrade proprement sous Node).
3
+ // — Lot 1 —
4
+ export * from './wake-lock.js';
5
+ export * from './fullscreen.js';
6
+ export * from './visibility.js';
7
+ export * from './network.js';
8
+ export * from './web-share.js';
9
+ export * from './clipboard.js';
10
+ export * from './vibration.js';
11
+ export * from './screen-orientation.js';
12
+ // — Lot 2 —
13
+ export * from './geolocation.js';
14
+ export * from './notifications.js';
15
+ export * from './permissions.js';
16
+ export * from './battery.js';
17
+ export * from './idle-detection.js';
18
+ export * from './media-devices.js';
19
+ export * from './sensors.js';
20
+ export * from './audio.js';
21
+ export * from './badging.js';
22
+ export * from './file-system-access.js';
23
+ export * from './contact-picker.js';
24
+
25
+ import { isWakeLockSupported } from './wake-lock.js';
26
+ import { isFullscreenSupported } from './fullscreen.js';
27
+ import { isVisibilitySupported } from './visibility.js';
28
+ import { isNetworkInfoSupported } from './network.js';
29
+ import { isShareSupported } from './web-share.js';
30
+ import { isClipboardSupported } from './clipboard.js';
31
+ import { isVibrationSupported } from './vibration.js';
32
+ import { isOrientationSupported } from './screen-orientation.js';
33
+ import { isGeolocationSupported } from './geolocation.js';
34
+ import { isNotificationSupported } from './notifications.js';
35
+ import { isPermissionsSupported } from './permissions.js';
36
+ import { isBatterySupported } from './battery.js';
37
+ import { isIdleDetectionSupported } from './idle-detection.js';
38
+ import { isMediaDevicesSupported, isDisplayMediaSupported } from './media-devices.js';
39
+ import { isOrientationSensorSupported, isMotionSensorSupported } from './sensors.js';
40
+ import { isAudioSupported } from './audio.js';
41
+ import { isBadgingSupported } from './badging.js';
42
+ import { isFileSystemAccessSupported } from './file-system-access.js';
43
+ import { isContactPickerSupported } from './contact-picker.js';
44
+
45
+ /** Carte de support de toutes les capacités du navigateur courant (booléens). */
46
+ export function capabilities() {
47
+ return {
48
+ wakeLock: isWakeLockSupported(),
49
+ fullscreen: isFullscreenSupported(),
50
+ visibility: isVisibilitySupported(),
51
+ network: isNetworkInfoSupported(),
52
+ share: isShareSupported(),
53
+ clipboard: isClipboardSupported(),
54
+ vibration: isVibrationSupported(),
55
+ orientation: isOrientationSupported(),
56
+ geolocation: isGeolocationSupported(),
57
+ notifications: isNotificationSupported(),
58
+ permissions: isPermissionsSupported(),
59
+ battery: isBatterySupported(),
60
+ idleDetection: isIdleDetectionSupported(),
61
+ mediaDevices: isMediaDevicesSupported(),
62
+ displayMedia: isDisplayMediaSupported(),
63
+ orientationSensor: isOrientationSensorSupported(),
64
+ motionSensor: isMotionSensorSupported(),
65
+ audio: isAudioSupported(),
66
+ badging: isBadgingSupported(),
67
+ fileSystemAccess: isFileSystemAccessSupported(),
68
+ contactPicker: isContactPickerSupported(),
69
+ };
70
+ }
@@ -0,0 +1,45 @@
1
+ // @mostajs/browser-kit — MediaDevices (caméras/micros, getUserMedia, partage d'écran). Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ const md = () => (typeof navigator !== 'undefined' ? navigator.mediaDevices : null);
3
+
4
+ export const isMediaDevicesSupported = () => !!(md() && md().getUserMedia);
5
+ export const isDisplayMediaSupported = () => !!(md() && md().getDisplayMedia);
6
+
7
+ /** Liste les périphériques groupés. → { cameras, microphones, speakers } (tableaux de MediaDeviceInfo). */
8
+ export async function enumerateDevices() {
9
+ const m = md();
10
+ if (!m || !m.enumerateDevices) return { cameras: [], microphones: [], speakers: [] };
11
+ try {
12
+ const all = await m.enumerateDevices();
13
+ return {
14
+ cameras: all.filter((d) => d.kind === 'videoinput'),
15
+ microphones: all.filter((d) => d.kind === 'audioinput'),
16
+ speakers: all.filter((d) => d.kind === 'audiooutput'),
17
+ };
18
+ } catch { return { cameras: [], microphones: [], speakers: [] }; }
19
+ }
20
+
21
+ /** getUserMedia (caméra/micro). → MediaStream ou null. */
22
+ export async function getUserMedia(constraints = { video: true, audio: true }) {
23
+ const m = md();
24
+ if (!m || !m.getUserMedia) return null;
25
+ try { return await m.getUserMedia(constraints); } catch { return null; }
26
+ }
27
+
28
+ /** getDisplayMedia (partage d'écran). → MediaStream ou null. */
29
+ export async function getDisplayMedia(constraints = { video: true }) {
30
+ const m = md();
31
+ if (!m || !m.getDisplayMedia) return null;
32
+ try { return await m.getDisplayMedia(constraints); } catch { return null; }
33
+ }
34
+
35
+ /** Arrête toutes les pistes d'un flux. */
36
+ export function stopStream(stream) { stream?.getTracks?.().forEach((t) => t.stop()); }
37
+
38
+ /** Abonne aux changements de périphériques (branchement/débranchement). → désabonnement. */
39
+ export function onDeviceChange(cb) {
40
+ const m = md();
41
+ if (!m || !m.addEventListener) return () => {};
42
+ const handler = () => cb();
43
+ m.addEventListener('devicechange', handler);
44
+ return () => m.removeEventListener('devicechange', handler);
45
+ }
package/src/network.js ADDED
@@ -0,0 +1,35 @@
1
+ // @mostajs/browser-kit — Online/Offline + Network Information API. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ const hasNavigator = () => typeof navigator !== 'undefined';
3
+ const hasWindow = () => typeof window !== 'undefined';
4
+ const conn = () => (hasNavigator() ? navigator.connection || navigator.mozConnection || navigator.webkitConnection : null);
5
+
6
+ export const isOnline = () => (hasNavigator() && 'onLine' in navigator ? navigator.onLine : true);
7
+ export const isNetworkInfoSupported = () => !!conn();
8
+
9
+ /** Infos de connexion (Network Information API) : { effectiveType, downlink, rtt, saveData } ou null. */
10
+ export function connectionInfo() {
11
+ const c = conn();
12
+ if (!c) return null;
13
+ return { effectiveType: c.effectiveType, downlink: c.downlink, rtt: c.rtt, saveData: !!c.saveData, type: c.type };
14
+ }
15
+
16
+ /** Abonne aux passages online/offline. cb(online:boolean). → désabonnement. */
17
+ export function onNetworkChange(cb) {
18
+ if (!hasWindow()) return () => {};
19
+ const on = () => cb(true), off = () => cb(false);
20
+ window.addEventListener('online', on);
21
+ window.addEventListener('offline', off);
22
+ return () => { window.removeEventListener('online', on); window.removeEventListener('offline', off); };
23
+ }
24
+
25
+ export const onOnline = (cb) => onNetworkChange((o) => { if (o) cb(); });
26
+ export const onOffline = (cb) => onNetworkChange((o) => { if (!o) cb(); });
27
+
28
+ /** Abonne aux changements de qualité de connexion (effectiveType…). → désabonnement. */
29
+ export function onConnectionChange(cb) {
30
+ const c = conn();
31
+ if (!c || !c.addEventListener) return () => {};
32
+ const handler = () => cb(connectionInfo());
33
+ c.addEventListener('change', handler);
34
+ return () => c.removeEventListener('change', handler);
35
+ }
@@ -0,0 +1,20 @@
1
+ // @mostajs/browser-kit — Notifications API. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ const N = () => (typeof Notification !== 'undefined' ? Notification : null);
3
+
4
+ export const isNotificationSupported = () => !!N();
5
+ export const notificationPermission = () => (N() ? N().permission : 'denied');
6
+
7
+ /** Demande la permission. → 'granted' | 'denied' | 'default'. */
8
+ export async function requestNotificationPermission() {
9
+ const n = N();
10
+ if (!n) return 'denied';
11
+ try { return await n.requestPermission(); } catch { return 'denied'; }
12
+ }
13
+
14
+ /** Affiche une notification (demande la permission si besoin). → Notification | null. */
15
+ export async function notify(title, options = {}) {
16
+ const n = N();
17
+ if (!n) return null;
18
+ if (n.permission !== 'granted') { if ((await requestNotificationPermission()) !== 'granted') return null; }
19
+ try { return new n(title, options); } catch { return null; }
20
+ }
@@ -0,0 +1,23 @@
1
+ // @mostajs/browser-kit — Permissions API. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ const perms = () => (typeof navigator !== 'undefined' ? navigator.permissions : null);
3
+
4
+ export const isPermissionsSupported = () => !!perms();
5
+
6
+ /** État d'une permission ('granted'|'denied'|'prompt') ou null. name = 'camera'|'microphone'|'geolocation'|'notifications'|… */
7
+ export async function queryPermission(name) {
8
+ const p = perms();
9
+ if (!p) return null;
10
+ try { const status = await p.query({ name }); return status.state; } catch { return null; }
11
+ }
12
+
13
+ /** Abonne aux changements d'état d'une permission. cb(state). → désabonnement. */
14
+ export async function onPermissionChange(name, cb) {
15
+ const p = perms();
16
+ if (!p) return () => {};
17
+ try {
18
+ const status = await p.query({ name });
19
+ const handler = () => cb(status.state);
20
+ status.addEventListener('change', handler);
21
+ return () => status.removeEventListener('change', handler);
22
+ } catch { return () => {}; }
23
+ }
@@ -0,0 +1,27 @@
1
+ // @mostajs/browser-kit — Screen Orientation API. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ const so = () => (typeof screen !== 'undefined' ? screen.orientation : null);
3
+
4
+ export const isOrientationSupported = () => !!so();
5
+ export const currentOrientation = () => { const o = so(); return o ? { type: o.type, angle: o.angle } : null; };
6
+
7
+ /** Verrouille l'orientation (ex. 'landscape', 'portrait'). Nécessite souvent le plein écran. → true/false. */
8
+ export async function lockOrientation(type) {
9
+ const o = so();
10
+ if (!o || !o.lock) return false;
11
+ try { await o.lock(type); return true; } catch { return false; }
12
+ }
13
+
14
+ export function unlockOrientation() {
15
+ const o = so();
16
+ if (!o || !o.unlock) return false;
17
+ try { o.unlock(); return true; } catch { return false; }
18
+ }
19
+
20
+ /** Abonne aux changements d'orientation. cb({type,angle}). → désabonnement. */
21
+ export function onOrientationChange(cb) {
22
+ const o = so();
23
+ if (!o || !o.addEventListener) return () => {};
24
+ const handler = () => cb(currentOrientation());
25
+ o.addEventListener('change', handler);
26
+ return () => o.removeEventListener('change', handler);
27
+ }
package/src/sensors.js ADDED
@@ -0,0 +1,33 @@
1
+ // @mostajs/browser-kit — DeviceOrientation / DeviceMotion (capteurs). Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ const hasWindow = () => typeof window !== 'undefined';
3
+
4
+ export const isOrientationSensorSupported = () => hasWindow() && 'DeviceOrientationEvent' in window;
5
+ export const isMotionSensorSupported = () => hasWindow() && 'DeviceMotionEvent' in window;
6
+
7
+ /** iOS 13+ exige une permission explicite (sur geste utilisateur). → 'granted' | 'denied'. */
8
+ export async function requestSensorPermission() {
9
+ const E = hasWindow() ? window.DeviceOrientationEvent : null;
10
+ if (E && typeof E.requestPermission === 'function') {
11
+ try { return await E.requestPermission(); } catch { return 'denied'; }
12
+ }
13
+ return 'granted'; // pas de gate de permission sur ce navigateur
14
+ }
15
+
16
+ /** Abonne à l'orientation de l'appareil. cb({ alpha, beta, gamma, absolute }). → désabonnement. */
17
+ export function onDeviceOrientation(cb) {
18
+ if (!isOrientationSensorSupported()) return () => {};
19
+ const handler = (e) => cb({ alpha: e.alpha, beta: e.beta, gamma: e.gamma, absolute: e.absolute });
20
+ window.addEventListener('deviceorientation', handler);
21
+ return () => window.removeEventListener('deviceorientation', handler);
22
+ }
23
+
24
+ /** Abonne au mouvement de l'appareil. cb({ acceleration, accelerationIncludingGravity, rotationRate, interval }). → désabonnement. */
25
+ export function onDeviceMotion(cb) {
26
+ if (!isMotionSensorSupported()) return () => {};
27
+ const handler = (e) => cb({
28
+ acceleration: e.acceleration, accelerationIncludingGravity: e.accelerationIncludingGravity,
29
+ rotationRate: e.rotationRate, interval: e.interval,
30
+ });
31
+ window.addEventListener('devicemotion', handler);
32
+ return () => window.removeEventListener('devicemotion', handler);
33
+ }
@@ -0,0 +1,12 @@
1
+ // @mostajs/browser-kit — Vibration API. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ const hasNavigator = () => typeof navigator !== 'undefined';
3
+
4
+ export const isVibrationSupported = () => hasNavigator() && typeof navigator.vibrate === 'function';
5
+
6
+ /** Fait vibrer. pattern = ms ou tableau [vibre, pause, vibre, …]. → true/false. */
7
+ export function vibrate(pattern = 200) {
8
+ if (!isVibrationSupported()) return false;
9
+ try { return navigator.vibrate(pattern); } catch { return false; }
10
+ }
11
+
12
+ export const stopVibration = () => vibrate(0);
@@ -0,0 +1,16 @@
1
+ // @mostajs/browser-kit — Page Visibility API. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ const hasDocument = () => typeof document !== 'undefined';
3
+
4
+ export const isVisibilitySupported = () => hasDocument() && 'visibilityState' in document;
5
+ export const isVisible = () => (hasDocument() ? document.visibilityState === 'visible' : true);
6
+
7
+ /** Abonne aux changements de visibilité de l'onglet. cb(visible:boolean). → désabonnement. */
8
+ export function onVisibilityChange(cb) {
9
+ if (!hasDocument()) return () => {};
10
+ const handler = () => cb(isVisible());
11
+ document.addEventListener('visibilitychange', handler);
12
+ return () => document.removeEventListener('visibilitychange', handler);
13
+ }
14
+
15
+ export const onVisible = (cb) => onVisibilityChange((v) => { if (v) cb(); });
16
+ export const onHidden = (cb) => onVisibilityChange((v) => { if (!v) cb(); });
@@ -0,0 +1,48 @@
1
+ // @mostajs/browser-kit — Screen Wake Lock (empêche la mise en veille de l'écran). Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ // Extrait du plateau VoxStudio (live.amia.fr/host). API : https://developer.mozilla.org/docs/Web/API/Screen_Wake_Lock_API
3
+
4
+ const hasNavigator = () => typeof navigator !== 'undefined';
5
+ const hasDocument = () => typeof document !== 'undefined';
6
+
7
+ export const isWakeLockSupported = () => hasNavigator() && 'wakeLock' in navigator;
8
+
9
+ /**
10
+ * Crée un contrôleur de verrou d'écran.
11
+ * onChange(active:boolean) : notifié à l'acquisition / libération.
12
+ * → { isSupported, acquire, release, isActive, bindAutoReacquire, dispose }
13
+ * Le verrou est auto-libéré par le navigateur quand l'onglet passe en arrière-plan : bindAutoReacquire() le ré-acquiert au retour.
14
+ */
15
+ export function createWakeLock({ onChange } = {}) {
16
+ let sentinel = null;
17
+ let unbind = null;
18
+
19
+ async function acquire() {
20
+ if (!isWakeLockSupported()) return false;
21
+ if (sentinel) return true;
22
+ try {
23
+ sentinel = await navigator.wakeLock.request('screen');
24
+ sentinel.addEventListener('release', () => { sentinel = null; onChange?.(false); });
25
+ onChange?.(true);
26
+ return true;
27
+ } catch { sentinel = null; return false; }
28
+ }
29
+
30
+ async function release() {
31
+ try { await sentinel?.release(); } catch { /* déjà libéré */ }
32
+ sentinel = null;
33
+ }
34
+
35
+ function bindAutoReacquire() {
36
+ if (!hasDocument() || unbind) return unbind || (() => {});
37
+ const handler = () => { if (document.visibilityState === 'visible' && !sentinel) acquire(); };
38
+ document.addEventListener('visibilitychange', handler);
39
+ unbind = () => { document.removeEventListener('visibilitychange', handler); unbind = null; };
40
+ return unbind;
41
+ }
42
+
43
+ function dispose() { unbind?.(); return release(); }
44
+
45
+ return { isSupported: isWakeLockSupported, acquire, release, isActive: () => !!sentinel, bindAutoReacquire, dispose };
46
+ }
47
+
48
+ export default createWakeLock;
@@ -0,0 +1,19 @@
1
+ // @mostajs/browser-kit — Web Share API. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ const hasNavigator = () => typeof navigator !== 'undefined';
3
+
4
+ export const isShareSupported = () => hasNavigator() && typeof navigator.share === 'function';
5
+ export const canShareFiles = () => hasNavigator() && typeof navigator.canShare === 'function';
6
+
7
+ /** Teste si une charge utile est partageable (utile pour les fichiers). */
8
+ export function canShare(data) {
9
+ if (!isShareSupported()) return false;
10
+ if (data && navigator.canShare) { try { return navigator.canShare(data); } catch { return false; } }
11
+ return true;
12
+ }
13
+
14
+ /** Ouvre la feuille de partage native. data = { title?, text?, url?, files? }. → true si partagé, false si annulé/non supporté. */
15
+ export async function share(data) {
16
+ if (!isShareSupported()) return false;
17
+ try { await navigator.share(data); return true; }
18
+ catch (e) { if (e && e.name === 'AbortError') return false; return false; }
19
+ }