@klodd/ds 3.0.1 → 3.0.3
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/css/base/pwa.css +8 -0
- package/js/pwa-register.js +160 -127
- package/package.json +1 -1
package/css/base/pwa.css
CHANGED
|
@@ -36,6 +36,14 @@ body {
|
|
|
36
36
|
overscroll-behavior: contain; /* stoppar pull-to-refresh-konflikter */
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/* HTML5 hidden-attribut respekteras alltid - oavsett vilken display-property
|
|
40
|
+
en komponent satter sig sjalv. Default UA-stylesheet har display:none for
|
|
41
|
+
[hidden] men en mer specifik komponentregel (t.ex. .my-form { display: flex })
|
|
42
|
+
kan over-rida det. !important saker att hidden alltid vinner. */
|
|
43
|
+
[hidden] {
|
|
44
|
+
display: none !important;
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
/* Globala fokusringar - alltid synliga vid keyboard-navigation, aldrig vid tap. */
|
|
40
48
|
:focus-visible {
|
|
41
49
|
outline: 2px solid var(--border-focus);
|
package/js/pwa-register.js
CHANGED
|
@@ -1,132 +1,165 @@
|
|
|
1
1
|
/*--------------------------------------------------------------
|
|
2
|
-
|
|
3
|
-
- isNetworkError() - iOS-saker nat-detektering
|
|
4
|
-
- Service worker registrering med updateViaCache: 'none'
|
|
5
|
-
- Install-prompt (beforeinstallprompt for Chrome/Android)
|
|
6
|
-
- iOS install-hint (Safari har ingen beforeinstallprompt)
|
|
7
|
-
--------------------------------------------------------------*/
|
|
2
|
+
@klodd/ds - PWA-registrering + install-prompts
|
|
8
3
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
kolla errortyp och message-text. Returnerar true om ett fetch-fel
|
|
13
|
-
troligen beror pa natverket (= OK att visa offline-meddelande).
|
|
14
|
-
--------------------------------------------------------------*/
|
|
15
|
-
window.isNetworkError = function ( err ) {
|
|
16
|
-
if ( ! navigator.onLine ) return true;
|
|
17
|
-
if ( err instanceof TypeError ) return true;
|
|
18
|
-
if ( err && err.message ) {
|
|
19
|
-
return /network|failed to fetch|load failed|offline|cancelled/i.test( err.message );
|
|
20
|
-
}
|
|
21
|
-
return false;
|
|
22
|
-
};
|
|
4
|
+
Auto-detekterar app-namn fran <html data-app="X">-attributet.
|
|
5
|
+
Defaults: appName = capitalize(data-app), appKey = data-app, swPath = "/sw.js".
|
|
6
|
+
Funkar identiskt for Jubb och Ekonom utan konfiguration.
|
|
23
7
|
|
|
24
|
-
|
|
25
|
-
SW-registrering
|
|
26
|
-
--------------------------------------------------------------*/
|
|
27
|
-
if ( 'serviceWorker' in navigator ) {
|
|
28
|
-
window.addEventListener( 'load', async function () {
|
|
29
|
-
try {
|
|
30
|
-
const reg = await navigator.serviceWorker.register( '/sw.js', {
|
|
31
|
-
scope: '/',
|
|
32
|
-
updateViaCache: 'none',
|
|
33
|
-
} );
|
|
34
|
-
console.log( 'Ekonom SW registered. Scope:', reg.scope );
|
|
35
|
-
} catch ( err ) {
|
|
36
|
-
console.warn( 'Ekonom SW registration failed:', err );
|
|
37
|
-
}
|
|
38
|
-
} );
|
|
39
|
-
}
|
|
8
|
+
Manuell override: KloddPWA.init({ appName, appKey, swPath }) innan load.
|
|
40
9
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
window.addEventListener( 'beforeinstallprompt', function ( e ) {
|
|
47
|
-
e.preventDefault();
|
|
48
|
-
deferredInstallPrompt = e;
|
|
49
|
-
maybeShowInstallChip();
|
|
50
|
-
} );
|
|
51
|
-
|
|
52
|
-
function maybeShowInstallChip() {
|
|
53
|
-
// Inte om redan dismissat denna session
|
|
54
|
-
if ( sessionStorage.getItem( 'ekonom_install_dismissed' ) ) return;
|
|
55
|
-
// Inte om redan installerad
|
|
56
|
-
if ( window.matchMedia( '(display-mode: standalone)' ).matches ) return;
|
|
57
|
-
|
|
58
|
-
// Bara efter 3+ besok
|
|
59
|
-
const visits = parseInt( localStorage.getItem( 'ekonom_visits' ) || '0', 10 ) + 1;
|
|
60
|
-
localStorage.setItem( 'ekonom_visits', String( visits ) );
|
|
61
|
-
if ( visits < 3 ) return;
|
|
62
|
-
|
|
63
|
-
showInstallChip();
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function showInstallChip() {
|
|
67
|
-
if ( document.getElementById( 'ekonom-install-chip' ) ) return;
|
|
68
|
-
|
|
69
|
-
const chip = document.createElement( 'div' );
|
|
70
|
-
chip.className = 'install-chip';
|
|
71
|
-
chip.id = 'ekonom-install-chip';
|
|
72
|
-
chip.setAttribute( 'role', 'dialog' );
|
|
73
|
-
chip.setAttribute( 'aria-label', 'Installera Ekonom' );
|
|
74
|
-
chip.innerHTML =
|
|
75
|
-
'<span>Installera Ekonom p\u00e5 hemsk\u00e4rmen</span>' +
|
|
76
|
-
'<button type="button" data-install>Installera</button>' +
|
|
77
|
-
'<button type="button" data-dismiss aria-label="St\u00e4ng">\u00D7</button>';
|
|
78
|
-
document.body.appendChild( chip );
|
|
79
|
-
|
|
80
|
-
chip.querySelector( '[data-install]' ).addEventListener( 'click', async function () {
|
|
81
|
-
if ( ! deferredInstallPrompt ) { chip.remove(); return; }
|
|
82
|
-
deferredInstallPrompt.prompt();
|
|
83
|
-
try { await deferredInstallPrompt.userChoice; } catch ( e ) {}
|
|
84
|
-
deferredInstallPrompt = null;
|
|
85
|
-
chip.remove();
|
|
86
|
-
} );
|
|
87
|
-
chip.querySelector( '[data-dismiss]' ).addEventListener( 'click', function () {
|
|
88
|
-
sessionStorage.setItem( 'ekonom_install_dismissed', '1' );
|
|
89
|
-
chip.remove();
|
|
90
|
-
} );
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/*--------------------------------------------------------------
|
|
94
|
-
iOS install-hint
|
|
95
|
-
Safari har ingen beforeinstallprompt - anvandaren maste manuellt
|
|
96
|
-
"Dela" -> "Lagg till pa hemskarmen". Visa en diskret chip efter
|
|
97
|
-
3+ besok sa de vet var de ska titta.
|
|
10
|
+
Funktioner:
|
|
11
|
+
- isNetworkError() - iOS-saker nat-detektering (window.isNetworkError)
|
|
12
|
+
- SW-registrering med updateViaCache: 'none'
|
|
13
|
+
- Install-prompt (beforeinstallprompt for Chrome/Android)
|
|
14
|
+
- iOS install-hint (Safari har ingen beforeinstallprompt)
|
|
98
15
|
--------------------------------------------------------------*/
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
16
|
+
(function (root, doc) {
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
/*------------------------------------------------------------
|
|
20
|
+
Auto-detect: las data-app fran <html>. Override via KloddPWA._cfg.
|
|
21
|
+
------------------------------------------------------------*/
|
|
22
|
+
function getCfg() {
|
|
23
|
+
const userCfg = root.KloddPWA && root.KloddPWA._cfg ? root.KloddPWA._cfg : {};
|
|
24
|
+
const dataApp = (doc.documentElement.getAttribute('data-app') || 'app').toLowerCase();
|
|
25
|
+
const cap = dataApp.charAt(0).toUpperCase() + dataApp.slice(1);
|
|
26
|
+
return {
|
|
27
|
+
appName: userCfg.appName || cap,
|
|
28
|
+
appKey: userCfg.appKey || dataApp,
|
|
29
|
+
swPath: userCfg.swPath || '/sw.js',
|
|
30
|
+
minVisits: userCfg.minVisits || 3,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/*------------------------------------------------------------
|
|
35
|
+
Network-error-detektor (window-prop sa appens egen JS kan anvanda).
|
|
36
|
+
navigator.onLine ljuger pa iOS Safari - triangulera mot errortyp.
|
|
37
|
+
------------------------------------------------------------*/
|
|
38
|
+
root.isNetworkError = function (err) {
|
|
39
|
+
if (!navigator.onLine) return true;
|
|
40
|
+
if (err instanceof TypeError) return true;
|
|
41
|
+
if (err && err.message) {
|
|
42
|
+
return /network|failed to fetch|load failed|offline|cancelled/i.test(err.message);
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/*------------------------------------------------------------
|
|
48
|
+
SW-registrering
|
|
49
|
+
------------------------------------------------------------*/
|
|
50
|
+
function registerSW(cfg) {
|
|
51
|
+
if (!('serviceWorker' in navigator)) return;
|
|
52
|
+
root.addEventListener('load', async function () {
|
|
53
|
+
try {
|
|
54
|
+
const reg = await navigator.serviceWorker.register(cfg.swPath, {
|
|
55
|
+
scope: '/',
|
|
56
|
+
updateViaCache: 'none',
|
|
57
|
+
});
|
|
58
|
+
console.log(cfg.appName + ' SW registered. Scope:', reg.scope);
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.warn(cfg.appName + ' SW registration failed:', err);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/*------------------------------------------------------------
|
|
66
|
+
Install-prompt (Android / Chrome / Edge)
|
|
67
|
+
------------------------------------------------------------*/
|
|
68
|
+
let deferredInstallPrompt = null;
|
|
69
|
+
|
|
70
|
+
function setupInstallPrompt(cfg) {
|
|
71
|
+
root.addEventListener('beforeinstallprompt', function (e) {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
deferredInstallPrompt = e;
|
|
74
|
+
maybeShowInstallChip(cfg);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function maybeShowInstallChip(cfg) {
|
|
79
|
+
if (sessionStorage.getItem(cfg.appKey + '_install_dismissed')) return;
|
|
80
|
+
if (root.matchMedia('(display-mode: standalone)').matches) return;
|
|
81
|
+
const visits = parseInt(localStorage.getItem(cfg.appKey + '_visits') || '0', 10) + 1;
|
|
82
|
+
localStorage.setItem(cfg.appKey + '_visits', String(visits));
|
|
83
|
+
if (visits < cfg.minVisits) return;
|
|
84
|
+
showInstallChip(cfg);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function showInstallChip(cfg) {
|
|
88
|
+
const id = cfg.appKey + '-install-chip';
|
|
89
|
+
if (doc.getElementById(id)) return;
|
|
90
|
+
|
|
91
|
+
const chip = doc.createElement('div');
|
|
92
|
+
chip.className = 'install-chip';
|
|
93
|
+
chip.id = id;
|
|
94
|
+
chip.setAttribute('role', 'dialog');
|
|
95
|
+
chip.setAttribute('aria-label', 'Installera ' + cfg.appName);
|
|
96
|
+
chip.innerHTML =
|
|
97
|
+
'<span>Installera ' + cfg.appName + ' på hemskärmen</span>' +
|
|
98
|
+
'<button type="button" data-install>Installera</button>' +
|
|
99
|
+
'<button type="button" data-dismiss aria-label="Stäng">×</button>';
|
|
100
|
+
doc.body.appendChild(chip);
|
|
101
|
+
|
|
102
|
+
chip.querySelector('[data-install]').addEventListener('click', async function () {
|
|
103
|
+
if (!deferredInstallPrompt) { chip.remove(); return; }
|
|
104
|
+
deferredInstallPrompt.prompt();
|
|
105
|
+
try { await deferredInstallPrompt.userChoice; } catch (e) {}
|
|
106
|
+
deferredInstallPrompt = null;
|
|
107
|
+
chip.remove();
|
|
108
|
+
});
|
|
109
|
+
chip.querySelector('[data-dismiss]').addEventListener('click', function () {
|
|
110
|
+
sessionStorage.setItem(cfg.appKey + '_install_dismissed', '1');
|
|
111
|
+
chip.remove();
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/*------------------------------------------------------------
|
|
116
|
+
iOS install-hint
|
|
117
|
+
Safari har ingen beforeinstallprompt - manuell flow via "Dela" ->
|
|
118
|
+
"Lagg till pa hemskarmen". Visa diskret hint efter N besok.
|
|
119
|
+
------------------------------------------------------------*/
|
|
120
|
+
function setupIOSHint(cfg) {
|
|
121
|
+
root.addEventListener('load', function () {
|
|
122
|
+
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) && !root.MSStream;
|
|
123
|
+
const inStandalone = root.matchMedia('(display-mode: standalone)').matches
|
|
124
|
+
|| root.navigator.standalone;
|
|
125
|
+
if (!isIOS || inStandalone) return;
|
|
126
|
+
if (sessionStorage.getItem(cfg.appKey + '_ios_install_dismissed')) return;
|
|
127
|
+
const visits = parseInt(localStorage.getItem(cfg.appKey + '_visits') || '0', 10);
|
|
128
|
+
if (visits < cfg.minVisits) return;
|
|
129
|
+
showIOSInstallHint(cfg);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function showIOSInstallHint(cfg) {
|
|
134
|
+
const id = cfg.appKey + '-ios-install-hint';
|
|
135
|
+
if (doc.getElementById(id)) return;
|
|
136
|
+
|
|
137
|
+
const hint = doc.createElement('div');
|
|
138
|
+
hint.className = 'install-chip install-chip-ios';
|
|
139
|
+
hint.id = id;
|
|
140
|
+
hint.setAttribute('role', 'dialog');
|
|
141
|
+
hint.setAttribute('aria-label', 'Installera ' + cfg.appName + ' pa iOS');
|
|
142
|
+
hint.innerHTML =
|
|
143
|
+
'<span>Tryck <strong>Dela</strong> → <strong>Lägg till på hemskärmen</strong></span>' +
|
|
144
|
+
'<button type="button" data-dismiss aria-label="Stäng">×</button>';
|
|
145
|
+
doc.body.appendChild(hint);
|
|
146
|
+
|
|
147
|
+
hint.querySelector('[data-dismiss]').addEventListener('click', function () {
|
|
148
|
+
sessionStorage.setItem(cfg.appKey + '_ios_install_dismissed', '1');
|
|
149
|
+
hint.remove();
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/*------------------------------------------------------------
|
|
154
|
+
Public API. Anvand KloddPWA.init({...}) FORE script-laddning av
|
|
155
|
+
denna fil (sa _cfg ar satt nar getCfg() koras), eller bara lat
|
|
156
|
+
auto-detect kora pa <html data-app="X">.
|
|
157
|
+
------------------------------------------------------------*/
|
|
158
|
+
const cfg = getCfg();
|
|
159
|
+
root.KloddPWA = root.KloddPWA || {};
|
|
160
|
+
root.KloddPWA.config = cfg;
|
|
161
|
+
|
|
162
|
+
registerSW(cfg);
|
|
163
|
+
setupInstallPrompt(cfg);
|
|
164
|
+
setupIOSHint(cfg);
|
|
165
|
+
})(typeof window !== 'undefined' ? window : globalThis, document);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@klodd/ds",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.3",
|
|
4
4
|
"description": "Klodd Design System - shared tokens, typography, components and JS for Jubb, Ekonom, and future apps. v2.0 inkluderar all komponentkod och delad JS - app-repona haller bara data och affarslogik.",
|
|
5
5
|
"main": "css/index.css",
|
|
6
6
|
"files": [
|