rei-kit 2.24.0 → 3.0.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/README.md +5 -7
- package/dist/{BaseButton-RN2an975.js → BaseButton-BD2HKB1L.js} +2 -2
- package/dist/BaseButton-BD2HKB1L.js.map +1 -0
- package/dist/{BaseListbox-BQoSwYdJ.js → BaseListbox-D-NQ6nDd.js} +3 -3
- package/dist/{BaseListbox-BQoSwYdJ.js.map → BaseListbox-D-NQ6nDd.js.map} +1 -1
- package/dist/{SettingsRow-Ct1p2C9Q.js → SettingsRow-C6Spi7Np.js} +2 -2
- package/dist/SettingsRow-C6Spi7Np.js.map +1 -0
- package/dist/app/index.d.ts +6 -0
- package/dist/app.js +5 -5
- package/dist/app.js.map +1 -1
- package/dist/composables/use-field-attrs.d.ts +17 -0
- package/dist/{focus-target-DRgd8UnQ.js → focus-target-BtcoJdmz.js} +4 -4
- package/dist/focus-target-BtcoJdmz.js.map +1 -0
- package/dist/index.d.ts +76 -0
- package/dist/index.js +31 -21
- package/dist/index.js.map +1 -1
- package/dist/motion/index.d.ts +11 -0
- package/dist/palettes/palettes.generated.json.d.ts +30 -30
- package/dist/palettes.css +28 -28
- package/dist/pwa/index.d.ts +6 -0
- package/dist/pwa.js +4 -4
- package/dist/pwa.js.map +1 -1
- package/dist/styles.css +84 -68
- package/dist/tokens.css +16 -3
- package/dist/{use-announce-qM0ZD9Ey.js → use-announce-B_JGTXHZ.js} +33 -5
- package/dist/use-announce-B_JGTXHZ.js.map +1 -0
- package/dist/web/index.d.ts +19 -0
- package/dist/web.js +3 -3
- package/package.json +5 -2
- package/dist/BaseButton-RN2an975.js.map +0 -1
- package/dist/SettingsRow-Ct1p2C9Q.js.map +0 -1
- package/dist/focus-target-DRgd8UnQ.js.map +0 -1
- package/dist/use-announce-qM0ZD9Ey.js.map +0 -1
package/dist/motion/index.d.ts
CHANGED
|
@@ -8,8 +8,19 @@
|
|
|
8
8
|
* with them are in `rei-kit/motion.css`, which both presets include.
|
|
9
9
|
*/
|
|
10
10
|
export { default as NumberTicker } from './NumberTicker.vue';
|
|
11
|
+
/** A number that counts up to its value once it scrolls into view. */
|
|
11
12
|
export { default as CountUp } from './CountUp.vue';
|
|
13
|
+
/**
|
|
14
|
+
* One word in a sentence that changes on its own: "Build it *glass*", then
|
|
15
|
+
* *brutal*, then *soft*.
|
|
16
|
+
*/
|
|
12
17
|
export { default as TextRotate } from './TextRotate.vue';
|
|
18
|
+
/**
|
|
19
|
+
* Text that types itself out, and — given several lines — deletes each one
|
|
20
|
+
* and types the next.
|
|
21
|
+
*/
|
|
13
22
|
export { default as TypeWriter } from './TypeWriter.vue';
|
|
23
|
+
/** Content that fades or rises into place as it scrolls into view. */
|
|
14
24
|
export { default as BaseReveal } from './BaseReveal.vue';
|
|
25
|
+
/** A row that scrolls sideways without end: logos, testimonials, a ticker. */
|
|
15
26
|
export { default as BaseMarquee } from './BaseMarquee.vue';
|
|
@@ -7,13 +7,13 @@ declare const _default: [
|
|
|
7
7
|
"primary": "#26667f",
|
|
8
8
|
"accent": "#124170",
|
|
9
9
|
"positive": "#67c090",
|
|
10
|
-
"negative": "#
|
|
10
|
+
"negative": "#c7453a",
|
|
11
11
|
"warning": "#d89a3e",
|
|
12
12
|
"canvas": "#f4faf8",
|
|
13
13
|
"surface": "#ffffff",
|
|
14
14
|
"muted": "#ddf4e7",
|
|
15
15
|
"ink": "#0f2229",
|
|
16
|
-
"inkSoft": "#
|
|
16
|
+
"inkSoft": "#586f7b",
|
|
17
17
|
"hair": "#e1edea",
|
|
18
18
|
"onPrimary": "#ffffff",
|
|
19
19
|
"onAccent": "#ffffff",
|
|
@@ -51,10 +51,10 @@ declare const _default: [
|
|
|
51
51
|
"label": "Nord",
|
|
52
52
|
"origin": "Arctic Ice Studio",
|
|
53
53
|
"light": {
|
|
54
|
-
"primary": "#
|
|
54
|
+
"primary": "#506e93",
|
|
55
55
|
"accent": "#b48ead",
|
|
56
56
|
"positive": "#a3be8c",
|
|
57
|
-
"negative": "#
|
|
57
|
+
"negative": "#a6545c",
|
|
58
58
|
"warning": "#ebcb8b",
|
|
59
59
|
"canvas": "#eceff4",
|
|
60
60
|
"surface": "#ffffff",
|
|
@@ -62,17 +62,17 @@ declare const _default: [
|
|
|
62
62
|
"ink": "#2e3440",
|
|
63
63
|
"inkSoft": "#4c566a",
|
|
64
64
|
"hair": "#d8dee9",
|
|
65
|
-
"onPrimary": "#
|
|
65
|
+
"onPrimary": "#ffffff",
|
|
66
66
|
"onAccent": "#111111",
|
|
67
67
|
"onPositive": "#2e3440",
|
|
68
|
-
"onNegative": "#
|
|
68
|
+
"onNegative": "#ffffff",
|
|
69
69
|
"onWarning": "#2e3440"
|
|
70
70
|
},
|
|
71
71
|
"dark": {
|
|
72
72
|
"primary": "#88c0d0",
|
|
73
73
|
"accent": "#b48ead",
|
|
74
74
|
"positive": "#a3be8c",
|
|
75
|
-
"negative": "#
|
|
75
|
+
"negative": "#d89fa4",
|
|
76
76
|
"warning": "#ebcb8b",
|
|
77
77
|
"canvas": "#2e3440",
|
|
78
78
|
"surface": "#3b4252",
|
|
@@ -83,11 +83,11 @@ declare const _default: [
|
|
|
83
83
|
"onPrimary": "#2e3440",
|
|
84
84
|
"onAccent": "#111111",
|
|
85
85
|
"onPositive": "#2e3440",
|
|
86
|
-
"onNegative": "#
|
|
86
|
+
"onNegative": "#2e3440",
|
|
87
87
|
"onWarning": "#2e3440"
|
|
88
88
|
},
|
|
89
89
|
"swatch": [
|
|
90
|
-
"#
|
|
90
|
+
"#506e93",
|
|
91
91
|
"#b48ead",
|
|
92
92
|
"#a3be8c",
|
|
93
93
|
"#ebcb8b"
|
|
@@ -192,43 +192,43 @@ declare const _default: [
|
|
|
192
192
|
"label": "Solarized",
|
|
193
193
|
"origin": "Ethan Schoonover",
|
|
194
194
|
"light": {
|
|
195
|
-
"primary": "#
|
|
195
|
+
"primary": "#2075b2",
|
|
196
196
|
"accent": "#6a6fc0",
|
|
197
197
|
"positive": "#859900",
|
|
198
|
-
"negative": "#
|
|
198
|
+
"negative": "#d5312e",
|
|
199
199
|
"warning": "#b58900",
|
|
200
200
|
"canvas": "#fdf6e3",
|
|
201
201
|
"surface": "#fffcf3",
|
|
202
202
|
"muted": "#eee8d5",
|
|
203
203
|
"ink": "#073642",
|
|
204
|
-
"inkSoft": "#
|
|
204
|
+
"inkSoft": "#566c73",
|
|
205
205
|
"hair": "#e4dcc4",
|
|
206
|
-
"onPrimary": "#
|
|
206
|
+
"onPrimary": "#ffffff",
|
|
207
207
|
"onAccent": "#ffffff",
|
|
208
208
|
"onPositive": "#002b36",
|
|
209
209
|
"onNegative": "#ffffff",
|
|
210
210
|
"onWarning": "#002b36"
|
|
211
211
|
},
|
|
212
212
|
"dark": {
|
|
213
|
-
"primary": "#
|
|
213
|
+
"primary": "#4b9fda",
|
|
214
214
|
"accent": "#6a6fc0",
|
|
215
215
|
"positive": "#859900",
|
|
216
|
-
"negative": "#
|
|
216
|
+
"negative": "#e87674",
|
|
217
217
|
"warning": "#b58900",
|
|
218
218
|
"canvas": "#002b36",
|
|
219
219
|
"surface": "#073642",
|
|
220
220
|
"muted": "#0b3f4c",
|
|
221
221
|
"ink": "#eee8d5",
|
|
222
|
-
"inkSoft": "#
|
|
222
|
+
"inkSoft": "#97a5a5",
|
|
223
223
|
"hair": "#16505e",
|
|
224
|
-
"onPrimary": "#
|
|
224
|
+
"onPrimary": "#002b36",
|
|
225
225
|
"onAccent": "#ffffff",
|
|
226
226
|
"onPositive": "#002b36",
|
|
227
|
-
"onNegative": "#
|
|
227
|
+
"onNegative": "#002b36",
|
|
228
228
|
"onWarning": "#002b36"
|
|
229
229
|
},
|
|
230
230
|
"swatch": [
|
|
231
|
-
"#
|
|
231
|
+
"#2075b2",
|
|
232
232
|
"#6a6fc0",
|
|
233
233
|
"#859900",
|
|
234
234
|
"#b58900"
|
|
@@ -260,7 +260,7 @@ declare const _default: [
|
|
|
260
260
|
"primary": "#83a598",
|
|
261
261
|
"accent": "#d3869b",
|
|
262
262
|
"positive": "#b8bb26",
|
|
263
|
-
"negative": "#
|
|
263
|
+
"negative": "#fc6857",
|
|
264
264
|
"warning": "#fabd2f",
|
|
265
265
|
"canvas": "#282828",
|
|
266
266
|
"surface": "#32302f",
|
|
@@ -271,7 +271,7 @@ declare const _default: [
|
|
|
271
271
|
"onPrimary": "#282828",
|
|
272
272
|
"onAccent": "#282828",
|
|
273
273
|
"onPositive": "#282828",
|
|
274
|
-
"onNegative": "#
|
|
274
|
+
"onNegative": "#282828",
|
|
275
275
|
"onWarning": "#282828"
|
|
276
276
|
},
|
|
277
277
|
"swatch": [
|
|
@@ -292,21 +292,21 @@ declare const _default: [
|
|
|
292
292
|
"#1a1b26"
|
|
293
293
|
],
|
|
294
294
|
"light": {
|
|
295
|
-
"primary": "#
|
|
295
|
+
"primary": "#2463b8",
|
|
296
296
|
"accent": "#9351ea",
|
|
297
297
|
"positive": "#587539",
|
|
298
|
-
"negative": "#
|
|
298
|
+
"negative": "#c0214f",
|
|
299
299
|
"warning": "#8c6c3e",
|
|
300
300
|
"canvas": "#e1e2e7",
|
|
301
301
|
"surface": "#f5f5f8",
|
|
302
302
|
"muted": "#d5d6db",
|
|
303
|
-
"ink": "#
|
|
304
|
-
"inkSoft": "#
|
|
303
|
+
"ink": "#2e51a3",
|
|
304
|
+
"inkSoft": "#4d5b8c",
|
|
305
305
|
"hair": "#c4c8da",
|
|
306
|
-
"onPrimary": "#
|
|
306
|
+
"onPrimary": "#ffffff",
|
|
307
307
|
"onAccent": "#ffffff",
|
|
308
308
|
"onPositive": "#ffffff",
|
|
309
|
-
"onNegative": "#
|
|
309
|
+
"onNegative": "#ffffff",
|
|
310
310
|
"onWarning": "#ffffff"
|
|
311
311
|
},
|
|
312
312
|
"dark": {
|
|
@@ -336,18 +336,18 @@ declare const _default: [
|
|
|
336
336
|
"primary": "#286983",
|
|
337
337
|
"accent": "#907aa9",
|
|
338
338
|
"positive": "#56949f",
|
|
339
|
-
"negative": "#
|
|
339
|
+
"negative": "#a45a6f",
|
|
340
340
|
"warning": "#ea9d34",
|
|
341
341
|
"canvas": "#faf4ed",
|
|
342
342
|
"surface": "#fffaf3",
|
|
343
343
|
"muted": "#f2e9e1",
|
|
344
344
|
"ink": "#575279",
|
|
345
|
-
"inkSoft": "#
|
|
345
|
+
"inkSoft": "#6a6681",
|
|
346
346
|
"hair": "#dfdad9",
|
|
347
347
|
"onPrimary": "#ffffff",
|
|
348
348
|
"onAccent": "#191724",
|
|
349
349
|
"onPositive": "#191724",
|
|
350
|
-
"onNegative": "#
|
|
350
|
+
"onNegative": "#ffffff",
|
|
351
351
|
"onWarning": "#191724"
|
|
352
352
|
},
|
|
353
353
|
"dark": {
|
package/dist/palettes.css
CHANGED
|
@@ -14,13 +14,13 @@
|
|
|
14
14
|
--color-primary: #26667f;
|
|
15
15
|
--color-accent: #124170;
|
|
16
16
|
--color-positive: #67c090;
|
|
17
|
-
--color-negative: #
|
|
17
|
+
--color-negative: #c7453a;
|
|
18
18
|
--color-warning: #d89a3e;
|
|
19
19
|
--color-canvas: #f4faf8;
|
|
20
20
|
--color-surface: #ffffff;
|
|
21
21
|
--color-muted: #ddf4e7;
|
|
22
22
|
--color-ink: #0f2229;
|
|
23
|
-
--color-ink-soft: #
|
|
23
|
+
--color-ink-soft: #586f7b;
|
|
24
24
|
--color-hair: #e1edea;
|
|
25
25
|
--color-on-primary: #ffffff;
|
|
26
26
|
--color-on-accent: #ffffff;
|
|
@@ -52,10 +52,10 @@
|
|
|
52
52
|
|
|
53
53
|
/* Nord — Arctic Ice Studio */
|
|
54
54
|
[data-palette='nord'] {
|
|
55
|
-
--color-primary: #
|
|
55
|
+
--color-primary: #506e93;
|
|
56
56
|
--color-accent: #b48ead;
|
|
57
57
|
--color-positive: #a3be8c;
|
|
58
|
-
--color-negative: #
|
|
58
|
+
--color-negative: #a6545c;
|
|
59
59
|
--color-warning: #ebcb8b;
|
|
60
60
|
--color-canvas: #eceff4;
|
|
61
61
|
--color-surface: #ffffff;
|
|
@@ -63,10 +63,10 @@
|
|
|
63
63
|
--color-ink: #2e3440;
|
|
64
64
|
--color-ink-soft: #4c566a;
|
|
65
65
|
--color-hair: #d8dee9;
|
|
66
|
-
--color-on-primary: #
|
|
66
|
+
--color-on-primary: #ffffff;
|
|
67
67
|
--color-on-accent: #111111;
|
|
68
68
|
--color-on-positive: #2e3440;
|
|
69
|
-
--color-on-negative: #
|
|
69
|
+
--color-on-negative: #ffffff;
|
|
70
70
|
--color-on-warning: #2e3440;
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
--color-primary: #88c0d0;
|
|
76
76
|
--color-accent: #b48ead;
|
|
77
77
|
--color-positive: #a3be8c;
|
|
78
|
-
--color-negative: #
|
|
78
|
+
--color-negative: #d89fa4;
|
|
79
79
|
--color-warning: #ebcb8b;
|
|
80
80
|
--color-canvas: #2e3440;
|
|
81
81
|
--color-surface: #3b4252;
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
--color-on-primary: #2e3440;
|
|
87
87
|
--color-on-accent: #111111;
|
|
88
88
|
--color-on-positive: #2e3440;
|
|
89
|
-
--color-on-negative: #
|
|
89
|
+
--color-on-negative: #2e3440;
|
|
90
90
|
--color-on-warning: #2e3440;
|
|
91
91
|
--shadow-color: #000000;
|
|
92
92
|
}
|
|
@@ -175,18 +175,18 @@
|
|
|
175
175
|
|
|
176
176
|
/* Solarized — Ethan Schoonover */
|
|
177
177
|
[data-palette='solarized'] {
|
|
178
|
-
--color-primary: #
|
|
178
|
+
--color-primary: #2075b2;
|
|
179
179
|
--color-accent: #6a6fc0;
|
|
180
180
|
--color-positive: #859900;
|
|
181
|
-
--color-negative: #
|
|
181
|
+
--color-negative: #d5312e;
|
|
182
182
|
--color-warning: #b58900;
|
|
183
183
|
--color-canvas: #fdf6e3;
|
|
184
184
|
--color-surface: #fffcf3;
|
|
185
185
|
--color-muted: #eee8d5;
|
|
186
186
|
--color-ink: #073642;
|
|
187
|
-
--color-ink-soft: #
|
|
187
|
+
--color-ink-soft: #566c73;
|
|
188
188
|
--color-hair: #e4dcc4;
|
|
189
|
-
--color-on-primary: #
|
|
189
|
+
--color-on-primary: #ffffff;
|
|
190
190
|
--color-on-accent: #ffffff;
|
|
191
191
|
--color-on-positive: #002b36;
|
|
192
192
|
--color-on-negative: #ffffff;
|
|
@@ -195,21 +195,21 @@
|
|
|
195
195
|
|
|
196
196
|
.dark[data-palette='solarized'],
|
|
197
197
|
.dark [data-palette='solarized'] {
|
|
198
|
-
--color-primary: #
|
|
198
|
+
--color-primary: #4b9fda;
|
|
199
199
|
--color-accent: #6a6fc0;
|
|
200
200
|
--color-positive: #859900;
|
|
201
|
-
--color-negative: #
|
|
201
|
+
--color-negative: #e87674;
|
|
202
202
|
--color-warning: #b58900;
|
|
203
203
|
--color-canvas: #002b36;
|
|
204
204
|
--color-surface: #073642;
|
|
205
205
|
--color-muted: #0b3f4c;
|
|
206
206
|
--color-ink: #eee8d5;
|
|
207
|
-
--color-ink-soft: #
|
|
207
|
+
--color-ink-soft: #97a5a5;
|
|
208
208
|
--color-hair: #16505e;
|
|
209
|
-
--color-on-primary: #
|
|
209
|
+
--color-on-primary: #002b36;
|
|
210
210
|
--color-on-accent: #ffffff;
|
|
211
211
|
--color-on-positive: #002b36;
|
|
212
|
-
--color-on-negative: #
|
|
212
|
+
--color-on-negative: #002b36;
|
|
213
213
|
--color-on-warning: #002b36;
|
|
214
214
|
--shadow-color: #000000;
|
|
215
215
|
}
|
|
@@ -239,7 +239,7 @@
|
|
|
239
239
|
--color-primary: #83a598;
|
|
240
240
|
--color-accent: #d3869b;
|
|
241
241
|
--color-positive: #b8bb26;
|
|
242
|
-
--color-negative: #
|
|
242
|
+
--color-negative: #fc6857;
|
|
243
243
|
--color-warning: #fabd2f;
|
|
244
244
|
--color-canvas: #282828;
|
|
245
245
|
--color-surface: #32302f;
|
|
@@ -250,28 +250,28 @@
|
|
|
250
250
|
--color-on-primary: #282828;
|
|
251
251
|
--color-on-accent: #282828;
|
|
252
252
|
--color-on-positive: #282828;
|
|
253
|
-
--color-on-negative: #
|
|
253
|
+
--color-on-negative: #282828;
|
|
254
254
|
--color-on-warning: #282828;
|
|
255
255
|
--shadow-color: #000000;
|
|
256
256
|
}
|
|
257
257
|
|
|
258
258
|
/* Tokyo Night — enkia */
|
|
259
259
|
[data-palette='tokyo-night'] {
|
|
260
|
-
--color-primary: #
|
|
260
|
+
--color-primary: #2463b8;
|
|
261
261
|
--color-accent: #9351ea;
|
|
262
262
|
--color-positive: #587539;
|
|
263
|
-
--color-negative: #
|
|
263
|
+
--color-negative: #c0214f;
|
|
264
264
|
--color-warning: #8c6c3e;
|
|
265
265
|
--color-canvas: #e1e2e7;
|
|
266
266
|
--color-surface: #f5f5f8;
|
|
267
267
|
--color-muted: #d5d6db;
|
|
268
|
-
--color-ink: #
|
|
269
|
-
--color-ink-soft: #
|
|
268
|
+
--color-ink: #2e51a3;
|
|
269
|
+
--color-ink-soft: #4d5b8c;
|
|
270
270
|
--color-hair: #c4c8da;
|
|
271
|
-
--color-on-primary: #
|
|
271
|
+
--color-on-primary: #ffffff;
|
|
272
272
|
--color-on-accent: #ffffff;
|
|
273
273
|
--color-on-positive: #ffffff;
|
|
274
|
-
--color-on-negative: #
|
|
274
|
+
--color-on-negative: #ffffff;
|
|
275
275
|
--color-on-warning: #ffffff;
|
|
276
276
|
}
|
|
277
277
|
|
|
@@ -301,18 +301,18 @@
|
|
|
301
301
|
--color-primary: #286983;
|
|
302
302
|
--color-accent: #907aa9;
|
|
303
303
|
--color-positive: #56949f;
|
|
304
|
-
--color-negative: #
|
|
304
|
+
--color-negative: #a45a6f;
|
|
305
305
|
--color-warning: #ea9d34;
|
|
306
306
|
--color-canvas: #faf4ed;
|
|
307
307
|
--color-surface: #fffaf3;
|
|
308
308
|
--color-muted: #f2e9e1;
|
|
309
309
|
--color-ink: #575279;
|
|
310
|
-
--color-ink-soft: #
|
|
310
|
+
--color-ink-soft: #6a6681;
|
|
311
311
|
--color-hair: #dfdad9;
|
|
312
312
|
--color-on-primary: #ffffff;
|
|
313
313
|
--color-on-accent: #191724;
|
|
314
314
|
--color-on-positive: #191724;
|
|
315
|
-
--color-on-negative: #
|
|
315
|
+
--color-on-negative: #ffffff;
|
|
316
316
|
--color-on-warning: #191724;
|
|
317
317
|
}
|
|
318
318
|
|
package/dist/pwa/index.d.ts
CHANGED
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export { useInstall, watchInstallability } from './use-install';
|
|
13
13
|
export { useSnooze } from './use-snooze';
|
|
14
|
+
/**
|
|
15
|
+
* The card that offers to install the app, and remembers being turned
|
|
16
|
+
* down.
|
|
17
|
+
*/
|
|
14
18
|
export { default as InstallPrompt } from './InstallPrompt.vue';
|
|
19
|
+
/** The way back to installing after the card has been dismissed. */
|
|
15
20
|
export { default as InstallSettings } from './InstallSettings.vue';
|
|
21
|
+
/** The card that says a new version is waiting. */
|
|
16
22
|
export { default as UpdatePrompt } from './UpdatePrompt.vue';
|
package/dist/pwa.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as addDays, f as todayKey, i as needsIosInstall, r as isInstalled, t as SettingsGroup_default } from "./SettingsGroup-BXkajKGl.js";
|
|
2
2
|
import { t as _plugin_vue_export_helper_default } from "./_plugin-vue_export-helper-BOaGB7Aw.js";
|
|
3
|
-
import { t as BaseButton_default } from "./BaseButton-
|
|
4
|
-
import { t as SettingsRow_default } from "./SettingsRow-
|
|
3
|
+
import { t as BaseButton_default } from "./BaseButton-BD2HKB1L.js";
|
|
4
|
+
import { t as SettingsRow_default } from "./SettingsRow-C6Spi7Np.js";
|
|
5
5
|
import { Transition, computed, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, openBlock, ref, resolveDynamicComponent, toDisplayString, unref, withCtx } from "vue";
|
|
6
6
|
import { CheckCircle2, Download, RefreshCw, Share, X } from "lucide-vue-next";
|
|
7
7
|
//#region src/pwa/use-install.ts
|
|
@@ -118,7 +118,7 @@ var _hoisted_1$1 = {
|
|
|
118
118
|
class: "border-positive/25 bg-positive/5 rounded-card flex gap-3 border p-3.5"
|
|
119
119
|
};
|
|
120
120
|
var _hoisted_2$1 = {
|
|
121
|
-
class: "bg-positive/15 text-positive flex size-10 shrink-0 items-center justify-center
|
|
121
|
+
class: "bg-positive/15 text-positive rounded-control flex size-10 shrink-0 items-center justify-center",
|
|
122
122
|
"aria-hidden": "true"
|
|
123
123
|
};
|
|
124
124
|
var _hoisted_3$1 = { class: "flex min-w-0 flex-1 flex-col gap-2" };
|
|
@@ -172,7 +172,7 @@ var InstallPrompt_default = /*#__PURE__*/ _plugin_vue_export_helper_default(/* @
|
|
|
172
172
|
});
|
|
173
173
|
};
|
|
174
174
|
}
|
|
175
|
-
}), [["__scopeId", "data-v-
|
|
175
|
+
}), [["__scopeId", "data-v-c5d173d3"]]);
|
|
176
176
|
//#endregion
|
|
177
177
|
//#region src/pwa/InstallSettings.vue
|
|
178
178
|
var InstallSettings_default = /* @__PURE__ */ defineComponent({
|
package/dist/pwa.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pwa.js","names":["$emit"],"sources":["../src/pwa/use-install.ts","../src/pwa/use-snooze.ts","../src/pwa/InstallPrompt.vue","../src/pwa/InstallPrompt.vue","../src/pwa/InstallSettings.vue","../src/pwa/InstallSettings.vue","../src/pwa/UpdatePrompt.vue","../src/pwa/UpdatePrompt.vue"],"sourcesContent":["import { computed, ref } from 'vue'\n\nimport { isInstalled, needsIosInstall } from '../utils/platform'\n\n/**\n * The event Chromium fires when it decides the app is installable.\n *\n * Not in lib.dom yet, and it is the only way to trigger the install sheet from\n * the app's own button rather than the browser's.\n */\ninterface BeforeInstallPromptEvent extends Event {\n prompt: () => Promise<void>\n userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>\n}\n\nconst deferred = ref<BeforeInstallPromptEvent | null>(null)\nconst installed = ref(false)\n\nlet listening = false\n\n/**\n * Starts listening for the install event.\n *\n * **Call this from the app's entry file, not from a component.**\n * `beforeinstallprompt` fires once, early, and only once per page load — a\n * listener attached when a component mounts has usually already missed it.\n *\n * It is a function rather than module-scope side effects so the package can be\n * imported on a server: the phone apps this came from ran it at module scope\n * and the kit spent a release learning why that is not free.\n *\n * @example\n * ```ts\n * // main.ts\n * import { watchInstallability } from 'rei-kit/pwa'\n * watchInstallability()\n * ```\n */\nexport function watchInstallability(): void {\n if (listening || typeof window === 'undefined') return\n\n listening = true\n installed.value = isInstalled()\n\n window.addEventListener('beforeinstallprompt', (event) => {\n // Without this the browser shows its own bar, and then the app's card and\n // the browser's bar are both on screen saying the same thing.\n event.preventDefault()\n deferred.value = event as BeforeInstallPromptEvent\n })\n\n window.addEventListener('appinstalled', () => {\n installed.value = true\n deferred.value = null\n })\n}\n\n/**\n * Adding the app to the Home Screen.\n *\n * Three states, because the platforms genuinely differ: Chromium hands over an\n * event that can be triggered from a button, Safari on iOS has no API at all\n * and needs the user walked through Share → Add to Home Screen, and everything\n * else can only be told that installing is possible.\n *\n * @example\n * ```ts\n * const install = useInstall()\n * if (install.canPrompt.value) await install.prompt()\n * ```\n */\nexport function useInstall() {\n async function prompt(): Promise<boolean> {\n const event = deferred.value\n if (!event) return false\n\n await event.prompt()\n const { outcome } = await event.userChoice\n\n // The event is single-use: Chromium refuses a second prompt() on it.\n deferred.value = null\n\n return outcome === 'accepted'\n }\n\n return {\n isInstalled: computed(() => installed.value),\n /** A button can open the real install sheet. */\n canPrompt: computed(() => !installed.value && deferred.value !== null),\n /** No API — the user has to be shown the Share menu. */\n needsManualSteps: computed(() => !installed.value && needsIosInstall()),\n prompt,\n }\n}\n","import { computed, ref } from 'vue'\n\nimport { addDays, todayKey } from '../utils/date'\n\n/**\n * A nudge that stops asking for a while after it is dismissed.\n *\n * Both phone apps had this inside their install card, identically: a date in\n * `localStorage`, compared against today. Pulled out because the install card\n * is not the only thing that should stop asking — a notification nudge wants\n * exactly the same behaviour, and had exactly the same code.\n *\n * Storage is wrapped in try/catch on both sides. A private window or a browser\n * set to block site data throws on access, and the honest failure there is a\n * nudge that reappears next session rather than one that crashes the screen it\n * is asking from.\n *\n * @param key - Where the date is kept. Namespace it to the app.\n * @param days - How long to stay quiet after a dismissal.\n */\nexport function useSnooze(key: string, days = 7) {\n const read = (): string => {\n try {\n return localStorage.getItem(key) ?? ''\n } catch {\n return ''\n }\n }\n\n const until = ref(read())\n\n return {\n /** False while the nudge is snoozed. */\n isOver: computed(() => todayKey() >= until.value),\n\n /** Stop asking for `days`. */\n snooze(): void {\n const next = addDays(todayKey(), days)\n until.value = next\n\n try {\n localStorage.setItem(key, next)\n } catch {\n // Storage blocked; it reappears next session rather than never.\n }\n },\n }\n}\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { Download, Share } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\nimport { useInstall } from './use-install'\nimport { useSnooze } from './use-snooze'\n\n/**\n * The card that offers to install the app, and remembers being turned down.\n *\n * Both phone apps had this, 111 lines, differing in a storage key and a colour.\n * What is here is the part that was the same: the three-way platform check, the\n * week-long snooze, and the shape.\n *\n * Every string is a prop. The kit does not know a language, and \"Add to Home\n * Screen\" is a sentence about a platform, not about this component.\n */\nconst {\n storageKey,\n title,\n body,\n action,\n later,\n iosTitle = '',\n iosBody = '',\n snoozeDays = 7,\n} = defineProps<{\n /** Where the dismissal is remembered. Namespace it: `hibi-install-nudge`. */\n storageKey: string\n title: string\n body: string\n /** The install button. Not shown on iOS, which has no API to call. */\n action: string\n later: string\n /** Shown instead of `title` where the user has to use the Share menu. */\n iosTitle?: string | undefined\n iosBody?: string | undefined\n snoozeDays?: number | undefined\n}>()\n\nconst { canPrompt, needsManualSteps, prompt } = useInstall()\nconst { isOver, snooze } = useSnooze(storageKey, snoozeDays)\n\nconst visible = computed(() => (canPrompt.value || needsManualSteps.value) && isOver.value)\n\nasync function install() {\n await prompt()\n\n // Accepted or dismissed, stop asking for a week. The prompt is what gets\n // tiring, not the answer.\n snooze()\n}\n</script>\n\n<template>\n <Transition name=\"install\">\n <section\n v-if=\"visible\"\n class=\"border-positive/25 bg-positive/5 rounded-card flex gap-3 border p-3.5\"\n >\n <span\n class=\"bg-positive/15 text-positive flex size-10 shrink-0 items-center justify-center rounded-xl\"\n aria-hidden=\"true\"\n >\n <component :is=\"needsManualSteps ? Share : Download\" class=\"size-5\" />\n </span>\n\n <div class=\"flex min-w-0 flex-1 flex-col gap-2\">\n <div>\n <p class=\"text-ink text-sm font-semibold\">\n {{ needsManualSteps && iosTitle ? iosTitle : title }}\n </p>\n <p class=\"text-ink-soft mt-0.5 text-xs leading-relaxed\">\n {{ needsManualSteps && iosBody ? iosBody : body }}\n </p>\n </div>\n\n <div class=\"flex items-center gap-2\">\n <!-- No button on iOS: Safari exposes no way to open the Share sheet\n from script, so a button here could only fail. -->\n <BaseButton\n v-if=\"canPrompt\"\n variant=\"positive\"\n pill\n size=\"xs\"\n class=\"font-semibold\"\n @click=\"install\"\n >\n {{ action }}\n </BaseButton>\n\n <BaseButton pill size=\"xs\" variant=\"quiet\" @click=\"snooze\">{{ later }}</BaseButton>\n </div>\n </div>\n </section>\n </Transition>\n</template>\n\n<style scoped>\n.install-enter-active,\n.install-leave-active {\n transition:\n opacity var(--duration-base) ease,\n transform var(--duration-base) ease;\n}\n\n.install-enter-from,\n.install-leave-to {\n opacity: 0;\n transform: translateY(-4px);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .install-enter-from,\n .install-leave-to {\n transform: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { Download, Share } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\nimport { useInstall } from './use-install'\nimport { useSnooze } from './use-snooze'\n\n/**\n * The card that offers to install the app, and remembers being turned down.\n *\n * Both phone apps had this, 111 lines, differing in a storage key and a colour.\n * What is here is the part that was the same: the three-way platform check, the\n * week-long snooze, and the shape.\n *\n * Every string is a prop. The kit does not know a language, and \"Add to Home\n * Screen\" is a sentence about a platform, not about this component.\n */\nconst {\n storageKey,\n title,\n body,\n action,\n later,\n iosTitle = '',\n iosBody = '',\n snoozeDays = 7,\n} = defineProps<{\n /** Where the dismissal is remembered. Namespace it: `hibi-install-nudge`. */\n storageKey: string\n title: string\n body: string\n /** The install button. Not shown on iOS, which has no API to call. */\n action: string\n later: string\n /** Shown instead of `title` where the user has to use the Share menu. */\n iosTitle?: string | undefined\n iosBody?: string | undefined\n snoozeDays?: number | undefined\n}>()\n\nconst { canPrompt, needsManualSteps, prompt } = useInstall()\nconst { isOver, snooze } = useSnooze(storageKey, snoozeDays)\n\nconst visible = computed(() => (canPrompt.value || needsManualSteps.value) && isOver.value)\n\nasync function install() {\n await prompt()\n\n // Accepted or dismissed, stop asking for a week. The prompt is what gets\n // tiring, not the answer.\n snooze()\n}\n</script>\n\n<template>\n <Transition name=\"install\">\n <section\n v-if=\"visible\"\n class=\"border-positive/25 bg-positive/5 rounded-card flex gap-3 border p-3.5\"\n >\n <span\n class=\"bg-positive/15 text-positive flex size-10 shrink-0 items-center justify-center rounded-xl\"\n aria-hidden=\"true\"\n >\n <component :is=\"needsManualSteps ? Share : Download\" class=\"size-5\" />\n </span>\n\n <div class=\"flex min-w-0 flex-1 flex-col gap-2\">\n <div>\n <p class=\"text-ink text-sm font-semibold\">\n {{ needsManualSteps && iosTitle ? iosTitle : title }}\n </p>\n <p class=\"text-ink-soft mt-0.5 text-xs leading-relaxed\">\n {{ needsManualSteps && iosBody ? iosBody : body }}\n </p>\n </div>\n\n <div class=\"flex items-center gap-2\">\n <!-- No button on iOS: Safari exposes no way to open the Share sheet\n from script, so a button here could only fail. -->\n <BaseButton\n v-if=\"canPrompt\"\n variant=\"positive\"\n pill\n size=\"xs\"\n class=\"font-semibold\"\n @click=\"install\"\n >\n {{ action }}\n </BaseButton>\n\n <BaseButton pill size=\"xs\" variant=\"quiet\" @click=\"snooze\">{{ later }}</BaseButton>\n </div>\n </div>\n </section>\n </Transition>\n</template>\n\n<style scoped>\n.install-enter-active,\n.install-leave-active {\n transition:\n opacity var(--duration-base) ease,\n transform var(--duration-base) ease;\n}\n\n.install-enter-from,\n.install-leave-to {\n opacity: 0;\n transform: translateY(-4px);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .install-enter-from,\n .install-leave-to {\n transform: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { CheckCircle2, Download, Share } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\nimport SettingsGroup from '../components/SettingsGroup.vue'\nimport SettingsRow from '../components/SettingsRow.vue'\nimport { useInstall } from './use-install'\n\n/**\n * The way back to installing after the card has been dismissed.\n *\n * The card snoozes for a week; without this row, someone who tapped \"Not now\"\n * and then changed their mind would have nowhere to go. Both phone apps had it,\n * thirty-four lines, with no difference at all.\n *\n * It renders nothing where installing is neither possible nor already done — a\n * settings group that says \"you cannot install this\" is worse than silence.\n */\ndefineProps<{\n /** The group's heading. */\n title: string\n /** The row's label while installing is still possible. */\n label: string\n /** The row's label once it is installed. */\n installedLabel: string\n body: string\n /** Shown instead of `body` on iOS, where the user must use the Share menu. */\n iosBody: string\n /** The install button. Absent on iOS, which has no API to call. */\n action: string\n}>()\n\nconst { isInstalled, canPrompt, needsManualSteps, prompt } = useInstall()\n</script>\n\n<template>\n <SettingsGroup v-if=\"isInstalled || canPrompt || needsManualSteps\" :title=\"title\">\n <SettingsRow\n :label=\"isInstalled ? installedLabel : label\"\n :description=\"isInstalled ? '' : needsManualSteps ? iosBody : body\"\n :icon=\"isInstalled ? CheckCircle2 : needsManualSteps ? Share : Download\"\n stacked\n >\n <BaseButton v-if=\"canPrompt\" variant=\"primary\" size=\"sm\" class=\"self-start\" @click=\"prompt\">\n {{ action }}\n </BaseButton>\n </SettingsRow>\n </SettingsGroup>\n</template>\n","<script setup lang=\"ts\">\nimport { CheckCircle2, Download, Share } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\nimport SettingsGroup from '../components/SettingsGroup.vue'\nimport SettingsRow from '../components/SettingsRow.vue'\nimport { useInstall } from './use-install'\n\n/**\n * The way back to installing after the card has been dismissed.\n *\n * The card snoozes for a week; without this row, someone who tapped \"Not now\"\n * and then changed their mind would have nowhere to go. Both phone apps had it,\n * thirty-four lines, with no difference at all.\n *\n * It renders nothing where installing is neither possible nor already done — a\n * settings group that says \"you cannot install this\" is worse than silence.\n */\ndefineProps<{\n /** The group's heading. */\n title: string\n /** The row's label while installing is still possible. */\n label: string\n /** The row's label once it is installed. */\n installedLabel: string\n body: string\n /** Shown instead of `body` on iOS, where the user must use the Share menu. */\n iosBody: string\n /** The install button. Absent on iOS, which has no API to call. */\n action: string\n}>()\n\nconst { isInstalled, canPrompt, needsManualSteps, prompt } = useInstall()\n</script>\n\n<template>\n <SettingsGroup v-if=\"isInstalled || canPrompt || needsManualSteps\" :title=\"title\">\n <SettingsRow\n :label=\"isInstalled ? installedLabel : label\"\n :description=\"isInstalled ? '' : needsManualSteps ? iosBody : body\"\n :icon=\"isInstalled ? CheckCircle2 : needsManualSteps ? Share : Download\"\n stacked\n >\n <BaseButton v-if=\"canPrompt\" variant=\"primary\" size=\"sm\" class=\"self-start\" @click=\"prompt\">\n {{ action }}\n </BaseButton>\n </SettingsRow>\n </SettingsGroup>\n</template>\n","<script setup lang=\"ts\">\nimport { RefreshCw, X } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\n\n/**\n * The card that says a new version is waiting.\n *\n * `registerType: 'prompt'` means a new service worker waits rather than taking\n * over, and this is what asks whether to apply it. Asking rather than reloading\n * is the decision worth keeping: an automatic swap mid-sentence loses whatever\n * was being typed.\n *\n * **The service worker stays the app's.** `virtual:pwa-register/vue` is a\n * build-time module from `vite-plugin-pwa`, and a library cannot import one —\n * so the app owns `useRegisterSW` and hands the answer down as `open`. That\n * split is also honest: whether an update is waiting is the app's business,\n * and what the card looks like is this component's.\n *\n * @example\n * ```vue\n * <script setup>\n * const { needRefresh, updateServiceWorker } = useRegisterSW()\n * <\\/script>\n *\n * <UpdatePrompt\n * :open=\"needRefresh\"\n * :title=\"t('pwa.updateTitle')\"\n * :body=\"t('pwa.updateBody')\"\n * :action=\"t('pwa.reload')\"\n * :dismiss-label=\"t('pwa.later')\"\n * @update=\"updateServiceWorker(true)\"\n * @dismiss=\"needRefresh = false\"\n * />\n * ```\n */\ndefineProps<{\n open: boolean\n title: string\n body: string\n action: string\n /** The X's accessible name. An X on its own has none. */\n dismissLabel: string\n}>()\n\ndefineEmits<{ update: []; dismiss: [] }>()\n</script>\n\n<template>\n <Transition name=\"update\">\n <aside v-if=\"open\" class=\"update-card\" role=\"status\">\n <span class=\"update-icon\" aria-hidden=\"true\">\n <RefreshCw class=\"size-4\" />\n </span>\n\n <div class=\"min-w-0 flex-1\">\n <p class=\"text-ink text-sm font-semibold\">{{ title }}</p>\n <p class=\"text-ink-soft text-xs leading-snug\">{{ body }}</p>\n </div>\n\n <BaseButton\n variant=\"primary\"\n pill\n size=\"xs\"\n class=\"shrink-0 font-semibold\"\n @click=\"$emit('update')\"\n >\n {{ action }}\n </BaseButton>\n\n <BaseButton\n variant=\"quiet\"\n icon\n pill\n size=\"sm\"\n class=\"shrink-0\"\n :aria-label=\"dismissLabel\"\n @click=\"$emit('dismiss')\"\n >\n <X class=\"size-4\" />\n </BaseButton>\n </aside>\n </Transition>\n</template>\n\n<style scoped>\n/* Sits above the tab bar and the action button, because it outranks both — but\n absolutely inside its container, so on a desktop-sized shell it does not\n float off into the page. Give the shell `relative`. */\n.update-card {\n border-color: var(--color-hair);\n background: color-mix(in srgb, var(--color-surface) 95%, transparent);\n border-radius: var(--radius-card);\n position: absolute;\n left: 50%;\n z-index: 50;\n display: flex;\n width: 100%;\n max-width: 360px;\n align-items: center;\n gap: 0.75rem;\n border-width: 1px;\n padding: 0.75rem;\n backdrop-filter: blur(12px);\n box-shadow: var(--shadow-overlay);\n transform: translateX(-50%);\n bottom: calc(1rem + env(safe-area-inset-bottom, 0px));\n}\n\n.update-icon {\n background: color-mix(in srgb, var(--color-primary) 15%, transparent);\n color: var(--color-primary);\n display: flex;\n height: 2.25rem;\n width: 2.25rem;\n flex-shrink: 0;\n align-items: center;\n justify-content: center;\n border-radius: 0.75rem;\n}\n\n.update-enter-active,\n.update-leave-active {\n transition:\n opacity var(--duration-slow) ease,\n transform var(--duration-slow) var(--ease-sheet);\n}\n\n.update-enter-from,\n.update-leave-to {\n opacity: 0;\n transform: translate(-50%, 1rem);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .update-enter-from,\n .update-leave-to {\n transform: translate(-50%, 0);\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { RefreshCw, X } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\n\n/**\n * The card that says a new version is waiting.\n *\n * `registerType: 'prompt'` means a new service worker waits rather than taking\n * over, and this is what asks whether to apply it. Asking rather than reloading\n * is the decision worth keeping: an automatic swap mid-sentence loses whatever\n * was being typed.\n *\n * **The service worker stays the app's.** `virtual:pwa-register/vue` is a\n * build-time module from `vite-plugin-pwa`, and a library cannot import one —\n * so the app owns `useRegisterSW` and hands the answer down as `open`. That\n * split is also honest: whether an update is waiting is the app's business,\n * and what the card looks like is this component's.\n *\n * @example\n * ```vue\n * <script setup>\n * const { needRefresh, updateServiceWorker } = useRegisterSW()\n * <\\/script>\n *\n * <UpdatePrompt\n * :open=\"needRefresh\"\n * :title=\"t('pwa.updateTitle')\"\n * :body=\"t('pwa.updateBody')\"\n * :action=\"t('pwa.reload')\"\n * :dismiss-label=\"t('pwa.later')\"\n * @update=\"updateServiceWorker(true)\"\n * @dismiss=\"needRefresh = false\"\n * />\n * ```\n */\ndefineProps<{\n open: boolean\n title: string\n body: string\n action: string\n /** The X's accessible name. An X on its own has none. */\n dismissLabel: string\n}>()\n\ndefineEmits<{ update: []; dismiss: [] }>()\n</script>\n\n<template>\n <Transition name=\"update\">\n <aside v-if=\"open\" class=\"update-card\" role=\"status\">\n <span class=\"update-icon\" aria-hidden=\"true\">\n <RefreshCw class=\"size-4\" />\n </span>\n\n <div class=\"min-w-0 flex-1\">\n <p class=\"text-ink text-sm font-semibold\">{{ title }}</p>\n <p class=\"text-ink-soft text-xs leading-snug\">{{ body }}</p>\n </div>\n\n <BaseButton\n variant=\"primary\"\n pill\n size=\"xs\"\n class=\"shrink-0 font-semibold\"\n @click=\"$emit('update')\"\n >\n {{ action }}\n </BaseButton>\n\n <BaseButton\n variant=\"quiet\"\n icon\n pill\n size=\"sm\"\n class=\"shrink-0\"\n :aria-label=\"dismissLabel\"\n @click=\"$emit('dismiss')\"\n >\n <X class=\"size-4\" />\n </BaseButton>\n </aside>\n </Transition>\n</template>\n\n<style scoped>\n/* Sits above the tab bar and the action button, because it outranks both — but\n absolutely inside its container, so on a desktop-sized shell it does not\n float off into the page. Give the shell `relative`. */\n.update-card {\n border-color: var(--color-hair);\n background: color-mix(in srgb, var(--color-surface) 95%, transparent);\n border-radius: var(--radius-card);\n position: absolute;\n left: 50%;\n z-index: 50;\n display: flex;\n width: 100%;\n max-width: 360px;\n align-items: center;\n gap: 0.75rem;\n border-width: 1px;\n padding: 0.75rem;\n backdrop-filter: blur(12px);\n box-shadow: var(--shadow-overlay);\n transform: translateX(-50%);\n bottom: calc(1rem + env(safe-area-inset-bottom, 0px));\n}\n\n.update-icon {\n background: color-mix(in srgb, var(--color-primary) 15%, transparent);\n color: var(--color-primary);\n display: flex;\n height: 2.25rem;\n width: 2.25rem;\n flex-shrink: 0;\n align-items: center;\n justify-content: center;\n border-radius: 0.75rem;\n}\n\n.update-enter-active,\n.update-leave-active {\n transition:\n opacity var(--duration-slow) ease,\n transform var(--duration-slow) var(--ease-sheet);\n}\n\n.update-enter-from,\n.update-leave-to {\n opacity: 0;\n transform: translate(-50%, 1rem);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .update-enter-from,\n .update-leave-to {\n transform: translate(-50%, 0);\n }\n}\n</style>\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pwa.js","names":["$emit"],"sources":["../src/pwa/use-install.ts","../src/pwa/use-snooze.ts","../src/pwa/InstallPrompt.vue","../src/pwa/InstallPrompt.vue","../src/pwa/InstallSettings.vue","../src/pwa/InstallSettings.vue","../src/pwa/UpdatePrompt.vue","../src/pwa/UpdatePrompt.vue"],"sourcesContent":["import { computed, ref } from 'vue'\n\nimport { isInstalled, needsIosInstall } from '../utils/platform'\n\n/**\n * The event Chromium fires when it decides the app is installable.\n *\n * Not in lib.dom yet, and it is the only way to trigger the install sheet from\n * the app's own button rather than the browser's.\n */\ninterface BeforeInstallPromptEvent extends Event {\n prompt: () => Promise<void>\n userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>\n}\n\nconst deferred = ref<BeforeInstallPromptEvent | null>(null)\nconst installed = ref(false)\n\nlet listening = false\n\n/**\n * Starts listening for the install event.\n *\n * **Call this from the app's entry file, not from a component.**\n * `beforeinstallprompt` fires once, early, and only once per page load — a\n * listener attached when a component mounts has usually already missed it.\n *\n * It is a function rather than module-scope side effects so the package can be\n * imported on a server: the phone apps this came from ran it at module scope\n * and the kit spent a release learning why that is not free.\n *\n * @example\n * ```ts\n * // main.ts\n * import { watchInstallability } from 'rei-kit/pwa'\n * watchInstallability()\n * ```\n */\nexport function watchInstallability(): void {\n if (listening || typeof window === 'undefined') return\n\n listening = true\n installed.value = isInstalled()\n\n window.addEventListener('beforeinstallprompt', (event) => {\n // Without this the browser shows its own bar, and then the app's card and\n // the browser's bar are both on screen saying the same thing.\n event.preventDefault()\n deferred.value = event as BeforeInstallPromptEvent\n })\n\n window.addEventListener('appinstalled', () => {\n installed.value = true\n deferred.value = null\n })\n}\n\n/**\n * Adding the app to the Home Screen.\n *\n * Three states, because the platforms genuinely differ: Chromium hands over an\n * event that can be triggered from a button, Safari on iOS has no API at all\n * and needs the user walked through Share → Add to Home Screen, and everything\n * else can only be told that installing is possible.\n *\n * @example\n * ```ts\n * const install = useInstall()\n * if (install.canPrompt.value) await install.prompt()\n * ```\n */\nexport function useInstall() {\n async function prompt(): Promise<boolean> {\n const event = deferred.value\n if (!event) return false\n\n await event.prompt()\n const { outcome } = await event.userChoice\n\n // The event is single-use: Chromium refuses a second prompt() on it.\n deferred.value = null\n\n return outcome === 'accepted'\n }\n\n return {\n isInstalled: computed(() => installed.value),\n /** A button can open the real install sheet. */\n canPrompt: computed(() => !installed.value && deferred.value !== null),\n /** No API — the user has to be shown the Share menu. */\n needsManualSteps: computed(() => !installed.value && needsIosInstall()),\n prompt,\n }\n}\n","import { computed, ref } from 'vue'\n\nimport { addDays, todayKey } from '../utils/date'\n\n/**\n * A nudge that stops asking for a while after it is dismissed.\n *\n * Both phone apps had this inside their install card, identically: a date in\n * `localStorage`, compared against today. Pulled out because the install card\n * is not the only thing that should stop asking — a notification nudge wants\n * exactly the same behaviour, and had exactly the same code.\n *\n * Storage is wrapped in try/catch on both sides. A private window or a browser\n * set to block site data throws on access, and the honest failure there is a\n * nudge that reappears next session rather than one that crashes the screen it\n * is asking from.\n *\n * @param key - Where the date is kept. Namespace it to the app.\n * @param days - How long to stay quiet after a dismissal.\n */\nexport function useSnooze(key: string, days = 7) {\n const read = (): string => {\n try {\n return localStorage.getItem(key) ?? ''\n } catch {\n return ''\n }\n }\n\n const until = ref(read())\n\n return {\n /** False while the nudge is snoozed. */\n isOver: computed(() => todayKey() >= until.value),\n\n /** Stop asking for `days`. */\n snooze(): void {\n const next = addDays(todayKey(), days)\n until.value = next\n\n try {\n localStorage.setItem(key, next)\n } catch {\n // Storage blocked; it reappears next session rather than never.\n }\n },\n }\n}\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { Download, Share } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\nimport { useInstall } from './use-install'\nimport { useSnooze } from './use-snooze'\n\n/**\n * The card that offers to install the app, and remembers being turned down.\n *\n * Both phone apps had this, 111 lines, differing in a storage key and a colour.\n * What is here is the part that was the same: the three-way platform check, the\n * week-long snooze, and the shape.\n *\n * Every string is a prop. The kit does not know a language, and \"Add to Home\n * Screen\" is a sentence about a platform, not about this component.\n */\nconst {\n storageKey,\n title,\n body,\n action,\n later,\n iosTitle = '',\n iosBody = '',\n snoozeDays = 7,\n} = defineProps<{\n /** Where the dismissal is remembered. Namespace it: `hibi-install-nudge`. */\n storageKey: string\n title: string\n body: string\n /** The install button. Not shown on iOS, which has no API to call. */\n action: string\n later: string\n /** Shown instead of `title` where the user has to use the Share menu. */\n iosTitle?: string | undefined\n iosBody?: string | undefined\n snoozeDays?: number | undefined\n}>()\n\nconst { canPrompt, needsManualSteps, prompt } = useInstall()\nconst { isOver, snooze } = useSnooze(storageKey, snoozeDays)\n\nconst visible = computed(() => (canPrompt.value || needsManualSteps.value) && isOver.value)\n\nasync function install() {\n await prompt()\n\n // Accepted or dismissed, stop asking for a week. The prompt is what gets\n // tiring, not the answer.\n snooze()\n}\n</script>\n\n<template>\n <Transition name=\"install\">\n <section\n v-if=\"visible\"\n class=\"border-positive/25 bg-positive/5 rounded-card flex gap-3 border p-3.5\"\n >\n <span\n class=\"bg-positive/15 text-positive rounded-control flex size-10 shrink-0 items-center justify-center\"\n aria-hidden=\"true\"\n >\n <component :is=\"needsManualSteps ? Share : Download\" class=\"size-5\" />\n </span>\n\n <div class=\"flex min-w-0 flex-1 flex-col gap-2\">\n <div>\n <p class=\"text-ink text-sm font-semibold\">\n {{ needsManualSteps && iosTitle ? iosTitle : title }}\n </p>\n <p class=\"text-ink-soft mt-0.5 text-xs leading-relaxed\">\n {{ needsManualSteps && iosBody ? iosBody : body }}\n </p>\n </div>\n\n <div class=\"flex items-center gap-2\">\n <!-- No button on iOS: Safari exposes no way to open the Share sheet\n from script, so a button here could only fail. -->\n <BaseButton\n v-if=\"canPrompt\"\n variant=\"positive\"\n pill\n size=\"xs\"\n class=\"font-semibold\"\n @click=\"install\"\n >\n {{ action }}\n </BaseButton>\n\n <BaseButton pill size=\"xs\" variant=\"quiet\" @click=\"snooze\">{{ later }}</BaseButton>\n </div>\n </div>\n </section>\n </Transition>\n</template>\n\n<style scoped>\n.install-enter-active,\n.install-leave-active {\n transition:\n opacity var(--duration-base) ease,\n transform var(--duration-base) ease;\n}\n\n.install-enter-from,\n.install-leave-to {\n opacity: 0;\n transform: translateY(-4px);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .install-enter-from,\n .install-leave-to {\n transform: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { Download, Share } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\nimport { useInstall } from './use-install'\nimport { useSnooze } from './use-snooze'\n\n/**\n * The card that offers to install the app, and remembers being turned down.\n *\n * Both phone apps had this, 111 lines, differing in a storage key and a colour.\n * What is here is the part that was the same: the three-way platform check, the\n * week-long snooze, and the shape.\n *\n * Every string is a prop. The kit does not know a language, and \"Add to Home\n * Screen\" is a sentence about a platform, not about this component.\n */\nconst {\n storageKey,\n title,\n body,\n action,\n later,\n iosTitle = '',\n iosBody = '',\n snoozeDays = 7,\n} = defineProps<{\n /** Where the dismissal is remembered. Namespace it: `hibi-install-nudge`. */\n storageKey: string\n title: string\n body: string\n /** The install button. Not shown on iOS, which has no API to call. */\n action: string\n later: string\n /** Shown instead of `title` where the user has to use the Share menu. */\n iosTitle?: string | undefined\n iosBody?: string | undefined\n snoozeDays?: number | undefined\n}>()\n\nconst { canPrompt, needsManualSteps, prompt } = useInstall()\nconst { isOver, snooze } = useSnooze(storageKey, snoozeDays)\n\nconst visible = computed(() => (canPrompt.value || needsManualSteps.value) && isOver.value)\n\nasync function install() {\n await prompt()\n\n // Accepted or dismissed, stop asking for a week. The prompt is what gets\n // tiring, not the answer.\n snooze()\n}\n</script>\n\n<template>\n <Transition name=\"install\">\n <section\n v-if=\"visible\"\n class=\"border-positive/25 bg-positive/5 rounded-card flex gap-3 border p-3.5\"\n >\n <span\n class=\"bg-positive/15 text-positive rounded-control flex size-10 shrink-0 items-center justify-center\"\n aria-hidden=\"true\"\n >\n <component :is=\"needsManualSteps ? Share : Download\" class=\"size-5\" />\n </span>\n\n <div class=\"flex min-w-0 flex-1 flex-col gap-2\">\n <div>\n <p class=\"text-ink text-sm font-semibold\">\n {{ needsManualSteps && iosTitle ? iosTitle : title }}\n </p>\n <p class=\"text-ink-soft mt-0.5 text-xs leading-relaxed\">\n {{ needsManualSteps && iosBody ? iosBody : body }}\n </p>\n </div>\n\n <div class=\"flex items-center gap-2\">\n <!-- No button on iOS: Safari exposes no way to open the Share sheet\n from script, so a button here could only fail. -->\n <BaseButton\n v-if=\"canPrompt\"\n variant=\"positive\"\n pill\n size=\"xs\"\n class=\"font-semibold\"\n @click=\"install\"\n >\n {{ action }}\n </BaseButton>\n\n <BaseButton pill size=\"xs\" variant=\"quiet\" @click=\"snooze\">{{ later }}</BaseButton>\n </div>\n </div>\n </section>\n </Transition>\n</template>\n\n<style scoped>\n.install-enter-active,\n.install-leave-active {\n transition:\n opacity var(--duration-base) ease,\n transform var(--duration-base) ease;\n}\n\n.install-enter-from,\n.install-leave-to {\n opacity: 0;\n transform: translateY(-4px);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .install-enter-from,\n .install-leave-to {\n transform: none;\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { CheckCircle2, Download, Share } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\nimport SettingsGroup from '../components/SettingsGroup.vue'\nimport SettingsRow from '../components/SettingsRow.vue'\nimport { useInstall } from './use-install'\n\n/**\n * The way back to installing after the card has been dismissed.\n *\n * The card snoozes for a week; without this row, someone who tapped \"Not now\"\n * and then changed their mind would have nowhere to go. Both phone apps had it,\n * thirty-four lines, with no difference at all.\n *\n * It renders nothing where installing is neither possible nor already done — a\n * settings group that says \"you cannot install this\" is worse than silence.\n */\ndefineProps<{\n /** The group's heading. */\n title: string\n /** The row's label while installing is still possible. */\n label: string\n /** The row's label once it is installed. */\n installedLabel: string\n body: string\n /** Shown instead of `body` on iOS, where the user must use the Share menu. */\n iosBody: string\n /** The install button. Absent on iOS, which has no API to call. */\n action: string\n}>()\n\nconst { isInstalled, canPrompt, needsManualSteps, prompt } = useInstall()\n</script>\n\n<template>\n <SettingsGroup v-if=\"isInstalled || canPrompt || needsManualSteps\" :title=\"title\">\n <SettingsRow\n :label=\"isInstalled ? installedLabel : label\"\n :description=\"isInstalled ? '' : needsManualSteps ? iosBody : body\"\n :icon=\"isInstalled ? CheckCircle2 : needsManualSteps ? Share : Download\"\n stacked\n >\n <BaseButton v-if=\"canPrompt\" variant=\"primary\" size=\"sm\" class=\"self-start\" @click=\"prompt\">\n {{ action }}\n </BaseButton>\n </SettingsRow>\n </SettingsGroup>\n</template>\n","<script setup lang=\"ts\">\nimport { CheckCircle2, Download, Share } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\nimport SettingsGroup from '../components/SettingsGroup.vue'\nimport SettingsRow from '../components/SettingsRow.vue'\nimport { useInstall } from './use-install'\n\n/**\n * The way back to installing after the card has been dismissed.\n *\n * The card snoozes for a week; without this row, someone who tapped \"Not now\"\n * and then changed their mind would have nowhere to go. Both phone apps had it,\n * thirty-four lines, with no difference at all.\n *\n * It renders nothing where installing is neither possible nor already done — a\n * settings group that says \"you cannot install this\" is worse than silence.\n */\ndefineProps<{\n /** The group's heading. */\n title: string\n /** The row's label while installing is still possible. */\n label: string\n /** The row's label once it is installed. */\n installedLabel: string\n body: string\n /** Shown instead of `body` on iOS, where the user must use the Share menu. */\n iosBody: string\n /** The install button. Absent on iOS, which has no API to call. */\n action: string\n}>()\n\nconst { isInstalled, canPrompt, needsManualSteps, prompt } = useInstall()\n</script>\n\n<template>\n <SettingsGroup v-if=\"isInstalled || canPrompt || needsManualSteps\" :title=\"title\">\n <SettingsRow\n :label=\"isInstalled ? installedLabel : label\"\n :description=\"isInstalled ? '' : needsManualSteps ? iosBody : body\"\n :icon=\"isInstalled ? CheckCircle2 : needsManualSteps ? Share : Download\"\n stacked\n >\n <BaseButton v-if=\"canPrompt\" variant=\"primary\" size=\"sm\" class=\"self-start\" @click=\"prompt\">\n {{ action }}\n </BaseButton>\n </SettingsRow>\n </SettingsGroup>\n</template>\n","<script setup lang=\"ts\">\nimport { RefreshCw, X } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\n\n/**\n * The card that says a new version is waiting.\n *\n * `registerType: 'prompt'` means a new service worker waits rather than taking\n * over, and this is what asks whether to apply it. Asking rather than reloading\n * is the decision worth keeping: an automatic swap mid-sentence loses whatever\n * was being typed.\n *\n * **The service worker stays the app's.** `virtual:pwa-register/vue` is a\n * build-time module from `vite-plugin-pwa`, and a library cannot import one —\n * so the app owns `useRegisterSW` and hands the answer down as `open`. That\n * split is also honest: whether an update is waiting is the app's business,\n * and what the card looks like is this component's.\n *\n * @example\n * ```vue\n * <script setup>\n * const { needRefresh, updateServiceWorker } = useRegisterSW()\n * <\\/script>\n *\n * <UpdatePrompt\n * :open=\"needRefresh\"\n * :title=\"t('pwa.updateTitle')\"\n * :body=\"t('pwa.updateBody')\"\n * :action=\"t('pwa.reload')\"\n * :dismiss-label=\"t('pwa.later')\"\n * @update=\"updateServiceWorker(true)\"\n * @dismiss=\"needRefresh = false\"\n * />\n * ```\n */\ndefineProps<{\n open: boolean\n title: string\n body: string\n action: string\n /** The X's accessible name. An X on its own has none. */\n dismissLabel: string\n}>()\n\ndefineEmits<{ update: []; dismiss: [] }>()\n</script>\n\n<template>\n <Transition name=\"update\">\n <aside v-if=\"open\" class=\"update-card\" role=\"status\">\n <span class=\"update-icon\" aria-hidden=\"true\">\n <RefreshCw class=\"size-4\" />\n </span>\n\n <div class=\"min-w-0 flex-1\">\n <p class=\"text-ink text-sm font-semibold\">{{ title }}</p>\n <p class=\"text-ink-soft text-xs leading-snug\">{{ body }}</p>\n </div>\n\n <BaseButton\n variant=\"primary\"\n pill\n size=\"xs\"\n class=\"shrink-0 font-semibold\"\n @click=\"$emit('update')\"\n >\n {{ action }}\n </BaseButton>\n\n <BaseButton\n variant=\"quiet\"\n icon\n pill\n size=\"sm\"\n class=\"shrink-0\"\n :aria-label=\"dismissLabel\"\n @click=\"$emit('dismiss')\"\n >\n <X class=\"size-4\" />\n </BaseButton>\n </aside>\n </Transition>\n</template>\n\n<style scoped>\n/* Sits above the tab bar and the action button, because it outranks both — but\n absolutely inside its container, so on a desktop-sized shell it does not\n float off into the page. Give the shell `relative`. */\n.update-card {\n border-color: var(--color-hair);\n background: color-mix(in srgb, var(--color-surface) 95%, transparent);\n border-radius: var(--radius-card);\n position: absolute;\n left: 50%;\n z-index: 50;\n display: flex;\n width: 100%;\n max-width: 360px;\n align-items: center;\n gap: 0.75rem;\n border-width: 1px;\n padding: 0.75rem;\n backdrop-filter: blur(12px);\n box-shadow: var(--shadow-overlay);\n transform: translateX(-50%);\n bottom: calc(1rem + env(safe-area-inset-bottom, 0px));\n}\n\n.update-icon {\n background: color-mix(in srgb, var(--color-primary) 15%, transparent);\n color: var(--color-primary);\n display: flex;\n height: 2.25rem;\n width: 2.25rem;\n flex-shrink: 0;\n align-items: center;\n justify-content: center;\n border-radius: 0.75rem;\n}\n\n.update-enter-active,\n.update-leave-active {\n transition:\n opacity var(--duration-slow) ease,\n transform var(--duration-slow) var(--ease-sheet);\n}\n\n.update-enter-from,\n.update-leave-to {\n opacity: 0;\n transform: translate(-50%, 1rem);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .update-enter-from,\n .update-leave-to {\n transform: translate(-50%, 0);\n }\n}\n</style>\n","<script setup lang=\"ts\">\nimport { RefreshCw, X } from 'lucide-vue-next'\n\nimport BaseButton from '../components/BaseButton.vue'\n\n/**\n * The card that says a new version is waiting.\n *\n * `registerType: 'prompt'` means a new service worker waits rather than taking\n * over, and this is what asks whether to apply it. Asking rather than reloading\n * is the decision worth keeping: an automatic swap mid-sentence loses whatever\n * was being typed.\n *\n * **The service worker stays the app's.** `virtual:pwa-register/vue` is a\n * build-time module from `vite-plugin-pwa`, and a library cannot import one —\n * so the app owns `useRegisterSW` and hands the answer down as `open`. That\n * split is also honest: whether an update is waiting is the app's business,\n * and what the card looks like is this component's.\n *\n * @example\n * ```vue\n * <script setup>\n * const { needRefresh, updateServiceWorker } = useRegisterSW()\n * <\\/script>\n *\n * <UpdatePrompt\n * :open=\"needRefresh\"\n * :title=\"t('pwa.updateTitle')\"\n * :body=\"t('pwa.updateBody')\"\n * :action=\"t('pwa.reload')\"\n * :dismiss-label=\"t('pwa.later')\"\n * @update=\"updateServiceWorker(true)\"\n * @dismiss=\"needRefresh = false\"\n * />\n * ```\n */\ndefineProps<{\n open: boolean\n title: string\n body: string\n action: string\n /** The X's accessible name. An X on its own has none. */\n dismissLabel: string\n}>()\n\ndefineEmits<{ update: []; dismiss: [] }>()\n</script>\n\n<template>\n <Transition name=\"update\">\n <aside v-if=\"open\" class=\"update-card\" role=\"status\">\n <span class=\"update-icon\" aria-hidden=\"true\">\n <RefreshCw class=\"size-4\" />\n </span>\n\n <div class=\"min-w-0 flex-1\">\n <p class=\"text-ink text-sm font-semibold\">{{ title }}</p>\n <p class=\"text-ink-soft text-xs leading-snug\">{{ body }}</p>\n </div>\n\n <BaseButton\n variant=\"primary\"\n pill\n size=\"xs\"\n class=\"shrink-0 font-semibold\"\n @click=\"$emit('update')\"\n >\n {{ action }}\n </BaseButton>\n\n <BaseButton\n variant=\"quiet\"\n icon\n pill\n size=\"sm\"\n class=\"shrink-0\"\n :aria-label=\"dismissLabel\"\n @click=\"$emit('dismiss')\"\n >\n <X class=\"size-4\" />\n </BaseButton>\n </aside>\n </Transition>\n</template>\n\n<style scoped>\n/* Sits above the tab bar and the action button, because it outranks both — but\n absolutely inside its container, so on a desktop-sized shell it does not\n float off into the page. Give the shell `relative`. */\n.update-card {\n border-color: var(--color-hair);\n background: color-mix(in srgb, var(--color-surface) 95%, transparent);\n border-radius: var(--radius-card);\n position: absolute;\n left: 50%;\n z-index: 50;\n display: flex;\n width: 100%;\n max-width: 360px;\n align-items: center;\n gap: 0.75rem;\n border-width: 1px;\n padding: 0.75rem;\n backdrop-filter: blur(12px);\n box-shadow: var(--shadow-overlay);\n transform: translateX(-50%);\n bottom: calc(1rem + env(safe-area-inset-bottom, 0px));\n}\n\n.update-icon {\n background: color-mix(in srgb, var(--color-primary) 15%, transparent);\n color: var(--color-primary);\n display: flex;\n height: 2.25rem;\n width: 2.25rem;\n flex-shrink: 0;\n align-items: center;\n justify-content: center;\n border-radius: 0.75rem;\n}\n\n.update-enter-active,\n.update-leave-active {\n transition:\n opacity var(--duration-slow) ease,\n transform var(--duration-slow) var(--ease-sheet);\n}\n\n.update-enter-from,\n.update-leave-to {\n opacity: 0;\n transform: translate(-50%, 1rem);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .update-enter-from,\n .update-leave-to {\n transform: translate(-50%, 0);\n }\n}\n</style>\n"],"mappings":"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"}
|