@k2b/ui 0.14.0 → 0.16.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.
Files changed (72) hide show
  1. package/README.md +5 -0
  2. package/dist/base.css +1097 -0
  3. package/dist/browser/chat/ChatPrimitives.js +19 -16
  4. package/dist/browser/chat/ChatPrimitives.js.map +1 -1
  5. package/dist/browser/chat/ChatTimeline.js +110 -87
  6. package/dist/browser/chat/ChatTimeline.js.map +1 -1
  7. package/dist/browser/content/FileGrid.js +28 -8
  8. package/dist/browser/content/FileGrid.js.map +1 -1
  9. package/dist/browser/content/MarkdownView.js +11 -5
  10. package/dist/browser/content/MarkdownView.js.map +1 -1
  11. package/dist/browser/content/MessageRow.js +1250 -0
  12. package/dist/browser/content/MessageRow.js.map +1 -0
  13. package/dist/browser/content/StructuredDataPreview.js +4 -1
  14. package/dist/browser/content/StructuredDataPreview.js.map +1 -1
  15. package/dist/browser/content/VirtualFeed.js +1127 -0
  16. package/dist/browser/content/VirtualFeed.js.map +1 -0
  17. package/dist/browser/content/collection-selection.js +27 -11
  18. package/dist/browser/content/collection-selection.js.map +1 -1
  19. package/dist/browser/content/index.js +2 -0
  20. package/dist/browser/content/index.js.map +1 -1
  21. package/dist/browser/feedback/InstallGuide.js +199 -145
  22. package/dist/browser/feedback/InstallGuide.js.map +1 -1
  23. package/dist/browser/feedback/install.js +8 -4
  24. package/dist/browser/feedback/install.js.map +1 -1
  25. package/dist/browser/inputs/QrScanner.js +101 -21
  26. package/dist/browser/inputs/QrScanner.js.map +1 -1
  27. package/dist/browser/intl/messages.js +86 -0
  28. package/dist/browser/intl/messages.js.map +1 -1
  29. package/dist/browser/surfaces/Avatar.js +33 -17
  30. package/dist/browser/surfaces/Avatar.js.map +1 -1
  31. package/dist/ssr/chat/ChatPrimitives.js +2 -2
  32. package/dist/ssr/chat/ChatPrimitives.js.map +1 -1
  33. package/dist/ssr/chat/ChatTimeline.js +110 -87
  34. package/dist/ssr/chat/ChatTimeline.js.map +1 -1
  35. package/dist/ssr/content/FileGrid.js +17 -3
  36. package/dist/ssr/content/FileGrid.js.map +1 -1
  37. package/dist/ssr/content/MarkdownView.js +11 -5
  38. package/dist/ssr/content/MarkdownView.js.map +1 -1
  39. package/dist/ssr/content/MessageRow.js +796 -0
  40. package/dist/ssr/content/MessageRow.js.map +1 -0
  41. package/dist/ssr/content/StructuredDataPreview.js +1 -1
  42. package/dist/ssr/content/StructuredDataPreview.js.map +1 -1
  43. package/dist/ssr/content/VirtualFeed.js +989 -0
  44. package/dist/ssr/content/VirtualFeed.js.map +1 -0
  45. package/dist/ssr/content/collection-selection.js +27 -11
  46. package/dist/ssr/content/collection-selection.js.map +1 -1
  47. package/dist/ssr/content/index.js +2 -0
  48. package/dist/ssr/content/index.js.map +1 -1
  49. package/dist/ssr/feedback/InstallGuide.js +150 -98
  50. package/dist/ssr/feedback/InstallGuide.js.map +1 -1
  51. package/dist/ssr/feedback/install.js +8 -4
  52. package/dist/ssr/feedback/install.js.map +1 -1
  53. package/dist/ssr/inputs/QrScanner.js +101 -21
  54. package/dist/ssr/inputs/QrScanner.js.map +1 -1
  55. package/dist/ssr/intl/messages.js +86 -0
  56. package/dist/ssr/intl/messages.js.map +1 -1
  57. package/dist/ssr/surfaces/Avatar.js +14 -1
  58. package/dist/ssr/surfaces/Avatar.js.map +1 -1
  59. package/dist/styles.css +1 -1
  60. package/dist/types/chat/ChatPrimitives.d.ts +2 -0
  61. package/dist/types/chat/ChatTimeline.d.ts +2 -0
  62. package/dist/types/content/FileGrid.d.ts +6 -1
  63. package/dist/types/content/MarkdownView.d.ts +8 -0
  64. package/dist/types/content/MessageRow.d.ts +168 -0
  65. package/dist/types/content/VirtualFeed.d.ts +81 -0
  66. package/dist/types/content/collection-selection.d.ts +15 -1
  67. package/dist/types/content/index.d.ts +4 -0
  68. package/dist/types/feedback/InstallGuide.d.ts +5 -3
  69. package/dist/types/feedback/install.d.ts +4 -3
  70. package/dist/types/inputs/QrScanner.d.ts +10 -2
  71. package/dist/types/intl/messages.d.ts +43 -0
  72. package/package.json +3 -1
@@ -5,22 +5,40 @@ import { ssrAttribute as _$ssrAttribute } from "solid-js/web";
5
5
  import { escape as _$escape } from "solid-js/web";
6
6
  var _tmpl$ = ["<p role=\"alert\">", "</p>"],
7
7
  _tmpl$2 = ["<p role=\"status\">", "</p>"],
8
- _tmpl$3 = ["<div", ">", "", "</div>"],
9
- _tmpl$4 = ["<p>", "</p>"],
10
- _tmpl$5 = ["<i", " aria-hidden=\"true\"></i>"],
11
- _tmpl$6 = ["<code>", "</code>"],
8
+ _tmpl$3 = ["<p>", "</p>"],
9
+ _tmpl$4 = ["<i", " aria-hidden=\"true\"></i>"],
10
+ _tmpl$5 = ["<code>", "</code>"],
11
+ _tmpl$6 = ["<div", ">", "", "</div>"],
12
12
  _tmpl$7 = ["<ol class=\"k2b-install-guide__steps\">", "</ol>"],
13
- _tmpl$8 = ["<li><span class=\"k2b-install-guide__step-icon\"><i", " aria-hidden=\"true\"></i></span><div><h3>", "</h3><p>", "</p></div></li>"];
13
+ _tmpl$8 = ["<li><span class=\"k2b-install-guide__step-icon\"><i", " aria-hidden=\"true\"></i></span><div><h3>", "</h3><p>", "</p></div></li>"],
14
+ _tmpl$9 = "<i class=\"ti ti-brand-chrome\" aria-hidden=\"true\"></i>";
14
15
  import { clipboard } from "@k2b/stdlib/solid";
15
- import { createMemo, For, Show } from "solid-js";
16
- import { Button } from "../actions/Button";
16
+ import { createMemo, For, Match, Show, Switch } from "solid-js";
17
+ import { Button, ButtonLink } from "../actions/Button";
17
18
  import { useUiMessages } from "../intl/messages";
18
19
  import { NoticeCard } from "../surfaces/NoticeCard";
20
+ /**
21
+ * Opens `url` in Chrome on Android. Without Chrome, the browser loads `url` itself. Only an absolute http or https
22
+ * address has an intent; anything else returns `undefined`.
23
+ */
24
+ const chromeIntent = url => {
25
+ let target;
26
+ try {
27
+ target = new URL(url);
28
+ } catch {
29
+ return undefined;
30
+ }
31
+ if (target.protocol !== "https:" && target.protocol !== "http:") return undefined;
32
+ const scheme = target.protocol.slice(0, -1);
33
+ return `intent://${target.host}${target.pathname}${target.search}${target.hash}#Intent;scheme=${scheme};package=com.android.chrome;S.browser_fallback_url=${encodeURIComponent(url)};end`;
34
+ };
35
+
19
36
  /**
20
37
  * Explains how to install the current page as an app on this platform: the browser's own dialog where it offers
21
38
  * one, otherwise the steps for Safari on iPhone, iPad, and Mac, other browsers on iPhone and iPad, Android, and other
22
39
  * browsers. A browser inside another app cannot install, so it gets a warning and the link to copy into Safari or
23
- * Chrome instead. The host owns the surrounding surface, its heading, and dismissal.
40
+ * Chrome instead. Samsung Internet gets a link to Chrome even when it offers its own dialog, because Android may block
41
+ * the app package it builds. The host owns the surrounding surface, its heading, and dismissal.
24
42
  */
25
43
  export function InstallGuide(props) {
26
44
  const messages = useUiMessages();
@@ -77,108 +95,106 @@ export function InstallGuide(props) {
77
95
  detail: state().platform === "android" || state().platform === "android-browser" ? t.installAndroidDetail : t.installBrowserDetail
78
96
  }];
79
97
  });
80
- return _$ssr(_tmpl$3, _$ssrAttribute("class", props.class ? `k2b-install-guide ${_$escape(props.class, true)}` : "k2b-install-guide", false) + _$ssrAttribute("data-platform", _$escape(state().platform, true), false), _$escape(_$createComponent(Show, {
98
+ return _$ssr(_tmpl$6, _$ssrAttribute("class", props.class ? `k2b-install-guide ${_$escape(props.class, true)}` : "k2b-install-guide", false) + _$ssrAttribute("data-platform", _$escape(state().platform, true), false), _$escape(_$createComponent(Show, {
81
99
  get when() {
82
100
  return state().failed();
83
101
  },
84
102
  get children() {
85
103
  return _$ssr(_tmpl$, _$escape(messages().installFailed));
86
104
  }
87
- })), _$escape(_$createComponent(Show, {
88
- get when() {
89
- return state().requested();
90
- },
105
+ })), _$escape(_$createComponent(Switch, {
91
106
  get fallback() {
92
- return _$createComponent(Show, {
107
+ return [_$createComponent(Show, {
93
108
  get when() {
94
- return state().canPrompt() || state().busy();
109
+ return hint();
110
+ },
111
+ children: hint => _$createComponent(NoticeCard, {
112
+ tone: "info",
113
+ get title() {
114
+ return hint().title;
115
+ },
116
+ get detail() {
117
+ return hint().detail;
118
+ }
119
+ })
120
+ }), _$ssr(_tmpl$7, _$escape(_$createComponent(For, {
121
+ get each() {
122
+ return steps();
123
+ },
124
+ children: step => _$ssr(_tmpl$8, _$ssrAttribute("class", _$escape(step.icon, true), false), _$escape(step.title), _$escape(step.detail))
125
+ }))), _$createComponent(Show, {
126
+ get when() {
127
+ return state().platform === "apple-mobile";
128
+ },
129
+ get children() {
130
+ return _$ssr(_tmpl$3, _$escape(messages().installSafariFallback));
131
+ }
132
+ }), _$createComponent(Show, {
133
+ get when() {
134
+ return props.note;
135
+ },
136
+ get children() {
137
+ return _$ssr(_tmpl$3, _$escape(props.note));
138
+ }
139
+ }), _$createComponent(Show, {
140
+ get when() {
141
+ return state().platform === "generic";
142
+ },
143
+ get children() {
144
+ return _$ssr(_tmpl$3, _$escape(messages().installUnavailable));
145
+ }
146
+ })];
147
+ },
148
+ get children() {
149
+ return [_$createComponent(Match, {
150
+ get when() {
151
+ return state().requested();
152
+ },
153
+ get children() {
154
+ return _$ssr(_tmpl$2, _$escape(messages().installRequested));
155
+ }
156
+ }), _$createComponent(Match, {
157
+ get when() {
158
+ return state().platform === "android-samsung";
95
159
  },
96
- get fallback() {
97
- return _$createComponent(Show, {
160
+ get children() {
161
+ return [_$createComponent(NoticeCard, {
162
+ tone: "info",
163
+ get title() {
164
+ return messages().installSamsungTitle;
165
+ },
166
+ get detail() {
167
+ return messages().installSamsungDetail({
168
+ appName: props.appName
169
+ });
170
+ }
171
+ }), _$createComponent(Show, {
98
172
  get when() {
99
- return embedded();
173
+ return chromeIntent(props.url);
100
174
  },
101
- get fallback() {
102
- return [_$createComponent(Show, {
103
- get when() {
104
- return hint();
105
- },
106
- children: hint => _$createComponent(NoticeCard, {
107
- tone: "info",
108
- get title() {
109
- return hint().title;
110
- },
111
- get detail() {
112
- return hint().detail;
113
- }
114
- })
115
- }), _$ssr(_tmpl$7, _$escape(_$createComponent(For, {
116
- get each() {
117
- return steps();
118
- },
119
- children: step => _$ssr(_tmpl$8, _$ssrAttribute("class", _$escape(step.icon, true), false), _$escape(step.title), _$escape(step.detail))
120
- }))), _$createComponent(Show, {
121
- get when() {
122
- return state().platform === "apple-mobile";
123
- },
124
- get children() {
125
- return _$ssr(_tmpl$4, _$escape(messages().installSafariFallback));
126
- }
127
- }), _$createComponent(Show, {
128
- get when() {
129
- return props.note;
130
- },
131
- get children() {
132
- return _$ssr(_tmpl$4, _$escape(props.note));
133
- }
134
- }), _$createComponent(Show, {
135
- get when() {
136
- return state().platform === "generic";
137
- },
138
- get children() {
139
- return _$ssr(_tmpl$4, _$escape(messages().installUnavailable));
140
- }
141
- })];
175
+ children: href => _$createComponent(ButtonLink, {
176
+ get href() {
177
+ return href();
178
+ },
179
+ get children() {
180
+ return [_$ssr(_tmpl$9), messages().installOpenChrome];
181
+ }
182
+ })
183
+ }), _$createComponent(Show, {
184
+ get when() {
185
+ return props.note;
142
186
  },
143
187
  get children() {
144
- return [_$createComponent(NoticeCard, {
145
- tone: "warning",
146
- get title() {
147
- return _$memo(() => state().platform === "apple-in-app")() ? messages().installAppleInAppTitle : messages().installInAppTitle;
148
- },
149
- get detail() {
150
- return _$memo(() => state().platform === "apple-in-app")() ? messages().installAppleInAppDetail({
151
- appName: props.appName
152
- }) : messages().installInAppDetail({
153
- appName: props.appName
154
- });
155
- }
156
- }), _$createComponent(Button, {
157
- variant: "secondary",
158
- onClick: () => void copy.copy(props.url),
159
- get children() {
160
- return [_$ssr(_tmpl$5, _$ssrAttribute("class", copy.wasCopied() ? "ti ti-check" : "ti ti-copy", false)), copy.wasCopied() ? messages().installLinkCopied : messages().installCopyLink];
161
- }
162
- }), _$createComponent(Show, {
163
- get when() {
164
- return copy.error();
165
- },
166
- get children() {
167
- return [_$ssr(_tmpl$, _$escape(messages().installCopyFailed)), _$ssr(_tmpl$6, _$escape(props.url))];
168
- }
169
- }), _$createComponent(Show, {
170
- get when() {
171
- return props.note;
172
- },
173
- get children() {
174
- return _$ssr(_tmpl$4, _$escape(props.note));
175
- }
176
- })];
188
+ return _$ssr(_tmpl$3, _$escape(props.note));
177
189
  }
178
- });
190
+ })];
191
+ }
192
+ }), _$createComponent(Match, {
193
+ get when() {
194
+ return state().canPrompt() || state().busy();
179
195
  },
180
196
  get children() {
181
- return [_$ssr(_tmpl$4, _$escape(messages().installNative({
197
+ return [_$ssr(_tmpl$3, _$escape(messages().installNative({
182
198
  appName: props.appName
183
199
  }))), _$createComponent(Button, {
184
200
  get loading() {
@@ -190,10 +206,46 @@ export function InstallGuide(props) {
190
206
  }
191
207
  })];
192
208
  }
193
- });
194
- },
195
- get children() {
196
- return _$ssr(_tmpl$2, _$escape(messages().installRequested));
209
+ }), _$createComponent(Match, {
210
+ get when() {
211
+ return embedded();
212
+ },
213
+ get children() {
214
+ return [_$createComponent(NoticeCard, {
215
+ tone: "warning",
216
+ get title() {
217
+ return _$memo(() => state().platform === "apple-in-app")() ? messages().installAppleInAppTitle : messages().installInAppTitle;
218
+ },
219
+ get detail() {
220
+ return _$memo(() => state().platform === "apple-in-app")() ? messages().installAppleInAppDetail({
221
+ appName: props.appName
222
+ }) : messages().installInAppDetail({
223
+ appName: props.appName
224
+ });
225
+ }
226
+ }), _$createComponent(Button, {
227
+ variant: "secondary",
228
+ onClick: () => void copy.copy(props.url),
229
+ get children() {
230
+ return [_$ssr(_tmpl$4, _$ssrAttribute("class", copy.wasCopied() ? "ti ti-check" : "ti ti-copy", false)), copy.wasCopied() ? messages().installLinkCopied : messages().installCopyLink];
231
+ }
232
+ }), _$createComponent(Show, {
233
+ get when() {
234
+ return copy.error();
235
+ },
236
+ get children() {
237
+ return [_$ssr(_tmpl$, _$escape(messages().installCopyFailed)), _$ssr(_tmpl$5, _$escape(props.url))];
238
+ }
239
+ }), _$createComponent(Show, {
240
+ get when() {
241
+ return props.note;
242
+ },
243
+ get children() {
244
+ return _$ssr(_tmpl$3, _$escape(props.note));
245
+ }
246
+ })];
247
+ }
248
+ })];
197
249
  }
198
250
  })));
199
251
  }
@@ -1 +1 @@
1
- {"version":3,"names":[],"sources":["feedback/InstallGuide.tsx"],"sourcesContent":["import { clipboard } from \"@k2b/stdlib/solid\";\nimport { createMemo, For, type JSX, Show } from \"solid-js\";\nimport { Button } from \"../actions/Button\";\nimport { useUiMessages } from \"../intl/messages\";\nimport { NoticeCard } from \"../surfaces/NoticeCard\";\nimport type { InstallPrompt } from \"./install\";\n\nexport type InstallGuideProps = {\n /** The name people see on their Home Screen, used in the guidance text. */\n appName: string;\n install: InstallPrompt;\n /**\n * The address a browser inside another app copies, to open it in Safari or Chrome. Shown as text when copying\n * fails, so it can be copied by hand.\n */\n url: string;\n /**\n * An application note under the installation steps or the embedded browser's link, for example what else the\n * installed app enables.\n */\n note?: string;\n class?: string;\n};\n\n/**\n * Explains how to install the current page as an app on this platform: the browser's own dialog where it offers\n * one, otherwise the steps for Safari on iPhone, iPad, and Mac, other browsers on iPhone and iPad, Android, and other\n * browsers. A browser inside another app cannot install, so it gets a warning and the link to copy into Safari or\n * Chrome instead. The host owns the surrounding surface, its heading, and dismissal.\n */\nexport function InstallGuide(props: InstallGuideProps): JSX.Element {\n const messages = useUiMessages();\n // The host may hand over another state after mount, such as the browser's own in place of the server's view.\n const state = () => props.install;\n const copy = clipboard.createWriter({ write: (url: string) => navigator.clipboard.writeText(url) });\n const embedded = () => state().platform === \"in-app\" || state().platform === \"apple-in-app\";\n /** A browser that installs, but less reliably than the platform's own. */\n const hint = createMemo(() => {\n const t = messages();\n if (state().platform === \"apple-browser\") return { title: t.installAppleBrowserTitle, detail: t.installAppleBrowserDetail };\n if (state().platform === \"android-browser\") return { title: t.installAndroidBrowserTitle, detail: t.installAndroidBrowserDetail };\n return undefined;\n });\n const steps = createMemo(() => {\n const t = messages();\n if (state().platform === \"apple-mobile\" || state().platform === \"apple-browser\")\n return [\n {\n icon: \"ti ti-share-2\",\n title: t.installOpenShare,\n detail: state().platform === \"apple-browser\" ? t.installOpenShareBrowserDetail : t.installOpenShareDetail,\n },\n { icon: \"ti ti-square-plus\", title: t.installHomeScreen, detail: t.installHomeScreenDetail },\n { icon: \"ti ti-check\", title: t.installConfirmAdd, detail: t.installConfirmAddDetail },\n ];\n if (state().platform === \"apple-desktop\")\n return [\n { icon: \"ti ti-share-2\", title: t.installSafariMenu, detail: t.installSafariMenuDetail },\n { icon: \"ti ti-app-window\", title: t.installAddDock, detail: t.installAddDockDetail },\n ];\n return [\n { icon: \"ti ti-dots\", title: t.installBrowserMenu, detail: t.installBrowserMenuDetail },\n {\n icon: \"ti ti-square-plus\",\n title: t.installApp,\n detail: state().platform === \"android\" || state().platform === \"android-browser\" ? t.installAndroidDetail : t.installBrowserDetail,\n },\n ];\n });\n return (\n <div class={props.class ? `k2b-install-guide ${props.class}` : \"k2b-install-guide\"} data-platform={state().platform}>\n <Show when={state().failed()}>\n <p role=\"alert\">{messages().installFailed}</p>\n </Show>\n <Show\n when={state().requested()}\n fallback={\n <Show\n when={state().canPrompt() || state().busy()}\n fallback={\n <Show\n when={embedded()}\n fallback={\n <>\n <Show when={hint()}>{(hint) => <NoticeCard tone=\"info\" title={hint().title} detail={hint().detail} />}</Show>\n <ol class=\"k2b-install-guide__steps\">\n <For each={steps()}>\n {(step) => (\n <li>\n <span class=\"k2b-install-guide__step-icon\">\n <i class={step.icon} aria-hidden=\"true\" />\n </span>\n <div>\n <h3>{step.title}</h3>\n <p>{step.detail}</p>\n </div>\n </li>\n )}\n </For>\n </ol>\n <Show when={state().platform === \"apple-mobile\"}>\n <p>{messages().installSafariFallback}</p>\n </Show>\n <Show when={props.note}>\n <p>{props.note}</p>\n </Show>\n <Show when={state().platform === \"generic\"}>\n <p>{messages().installUnavailable}</p>\n </Show>\n </>\n }\n >\n <NoticeCard\n tone=\"warning\"\n title={state().platform === \"apple-in-app\" ? messages().installAppleInAppTitle : messages().installInAppTitle}\n detail={\n state().platform === \"apple-in-app\"\n ? messages().installAppleInAppDetail({ appName: props.appName })\n : messages().installInAppDetail({ appName: props.appName })\n }\n />\n <Button variant=\"secondary\" onClick={() => void copy.copy(props.url)}>\n <i class={copy.wasCopied() ? \"ti ti-check\" : \"ti ti-copy\"} aria-hidden=\"true\" />\n {copy.wasCopied() ? messages().installLinkCopied : messages().installCopyLink}\n </Button>\n <Show when={copy.error()}>\n <p role=\"alert\">{messages().installCopyFailed}</p>\n <code>{props.url}</code>\n </Show>\n <Show when={props.note}>\n <p>{props.note}</p>\n </Show>\n </Show>\n }\n >\n <p>{messages().installNative({ appName: props.appName })}</p>\n <Button loading={state().busy()} onClick={() => void state().install()}>\n {messages().installApp}\n </Button>\n </Show>\n }\n >\n <p role=\"status\">{messages().installRequested}</p>\n </Show>\n </div>\n );\n}\n\nexport default InstallGuide;\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":[],"sources":["feedback/InstallGuide.tsx"],"sourcesContent":["import { clipboard } from \"@k2b/stdlib/solid\";\nimport { createMemo, For, type JSX, Match, Show, Switch } from \"solid-js\";\nimport { Button, ButtonLink } from \"../actions/Button\";\nimport { useUiMessages } from \"../intl/messages\";\nimport { NoticeCard } from \"../surfaces/NoticeCard\";\nimport type { InstallPrompt } from \"./install\";\n\nexport type InstallGuideProps = {\n /** The name people see on their Home Screen, used in the guidance text. */\n appName: string;\n install: InstallPrompt;\n /**\n * The absolute address of the page to install. A browser inside another app copies it, to open it in Safari or\n * Chrome, and shows it as text when copying fails, so it can be copied by hand. Samsung Internet opens an http or\n * https address in Chrome.\n */\n url: string;\n /**\n * An application note under the installation steps or the embedded browser's link, for example what else the\n * installed app enables.\n */\n note?: string;\n class?: string;\n};\n\n/**\n * Opens `url` in Chrome on Android. Without Chrome, the browser loads `url` itself. Only an absolute http or https\n * address has an intent; anything else returns `undefined`.\n */\nconst chromeIntent = (url: string) => {\n let target: URL;\n try {\n target = new URL(url);\n } catch {\n return undefined;\n }\n if (target.protocol !== \"https:\" && target.protocol !== \"http:\") return undefined;\n const scheme = target.protocol.slice(0, -1);\n return `intent://${target.host}${target.pathname}${target.search}${target.hash}#Intent;scheme=${scheme};package=com.android.chrome;S.browser_fallback_url=${encodeURIComponent(url)};end`;\n};\n\n/**\n * Explains how to install the current page as an app on this platform: the browser's own dialog where it offers\n * one, otherwise the steps for Safari on iPhone, iPad, and Mac, other browsers on iPhone and iPad, Android, and other\n * browsers. A browser inside another app cannot install, so it gets a warning and the link to copy into Safari or\n * Chrome instead. Samsung Internet gets a link to Chrome even when it offers its own dialog, because Android may block\n * the app package it builds. The host owns the surrounding surface, its heading, and dismissal.\n */\nexport function InstallGuide(props: InstallGuideProps): JSX.Element {\n const messages = useUiMessages();\n // The host may hand over another state after mount, such as the browser's own in place of the server's view.\n const state = () => props.install;\n const copy = clipboard.createWriter({ write: (url: string) => navigator.clipboard.writeText(url) });\n const embedded = () => state().platform === \"in-app\" || state().platform === \"apple-in-app\";\n /** A browser that installs, but less reliably than the platform's own. */\n const hint = createMemo(() => {\n const t = messages();\n if (state().platform === \"apple-browser\") return { title: t.installAppleBrowserTitle, detail: t.installAppleBrowserDetail };\n if (state().platform === \"android-browser\") return { title: t.installAndroidBrowserTitle, detail: t.installAndroidBrowserDetail };\n return undefined;\n });\n const steps = createMemo(() => {\n const t = messages();\n if (state().platform === \"apple-mobile\" || state().platform === \"apple-browser\")\n return [\n {\n icon: \"ti ti-share-2\",\n title: t.installOpenShare,\n detail: state().platform === \"apple-browser\" ? t.installOpenShareBrowserDetail : t.installOpenShareDetail,\n },\n { icon: \"ti ti-square-plus\", title: t.installHomeScreen, detail: t.installHomeScreenDetail },\n { icon: \"ti ti-check\", title: t.installConfirmAdd, detail: t.installConfirmAddDetail },\n ];\n if (state().platform === \"apple-desktop\")\n return [\n { icon: \"ti ti-share-2\", title: t.installSafariMenu, detail: t.installSafariMenuDetail },\n { icon: \"ti ti-app-window\", title: t.installAddDock, detail: t.installAddDockDetail },\n ];\n return [\n { icon: \"ti ti-dots\", title: t.installBrowserMenu, detail: t.installBrowserMenuDetail },\n {\n icon: \"ti ti-square-plus\",\n title: t.installApp,\n detail: state().platform === \"android\" || state().platform === \"android-browser\" ? t.installAndroidDetail : t.installBrowserDetail,\n },\n ];\n });\n return (\n <div class={props.class ? `k2b-install-guide ${props.class}` : \"k2b-install-guide\"} data-platform={state().platform}>\n <Show when={state().failed()}>\n <p role=\"alert\">{messages().installFailed}</p>\n </Show>\n <Switch\n fallback={\n <>\n <Show when={hint()}>{(hint) => <NoticeCard tone=\"info\" title={hint().title} detail={hint().detail} />}</Show>\n <ol class=\"k2b-install-guide__steps\">\n <For each={steps()}>\n {(step) => (\n <li>\n <span class=\"k2b-install-guide__step-icon\">\n <i class={step.icon} aria-hidden=\"true\" />\n </span>\n <div>\n <h3>{step.title}</h3>\n <p>{step.detail}</p>\n </div>\n </li>\n )}\n </For>\n </ol>\n <Show when={state().platform === \"apple-mobile\"}>\n <p>{messages().installSafariFallback}</p>\n </Show>\n <Show when={props.note}>\n <p>{props.note}</p>\n </Show>\n <Show when={state().platform === \"generic\"}>\n <p>{messages().installUnavailable}</p>\n </Show>\n </>\n }\n >\n <Match when={state().requested()}>\n <p role=\"status\">{messages().installRequested}</p>\n </Match>\n <Match when={state().platform === \"android-samsung\"}>\n <NoticeCard\n tone=\"info\"\n title={messages().installSamsungTitle}\n detail={messages().installSamsungDetail({ appName: props.appName })}\n />\n <Show when={chromeIntent(props.url)}>\n {(href) => (\n <ButtonLink href={href()}>\n <i class=\"ti ti-brand-chrome\" aria-hidden=\"true\" />\n {messages().installOpenChrome}\n </ButtonLink>\n )}\n </Show>\n <Show when={props.note}>\n <p>{props.note}</p>\n </Show>\n </Match>\n <Match when={state().canPrompt() || state().busy()}>\n <p>{messages().installNative({ appName: props.appName })}</p>\n <Button loading={state().busy()} onClick={() => void state().install()}>\n {messages().installApp}\n </Button>\n </Match>\n <Match when={embedded()}>\n <NoticeCard\n tone=\"warning\"\n title={state().platform === \"apple-in-app\" ? messages().installAppleInAppTitle : messages().installInAppTitle}\n detail={\n state().platform === \"apple-in-app\"\n ? messages().installAppleInAppDetail({ appName: props.appName })\n : messages().installInAppDetail({ appName: props.appName })\n }\n />\n <Button variant=\"secondary\" onClick={() => void copy.copy(props.url)}>\n <i class={copy.wasCopied() ? \"ti ti-check\" : \"ti ti-copy\"} aria-hidden=\"true\" />\n {copy.wasCopied() ? messages().installLinkCopied : messages().installCopyLink}\n </Button>\n <Show when={copy.error()}>\n <p role=\"alert\">{messages().installCopyFailed}</p>\n <code>{props.url}</code>\n </Show>\n <Show when={props.note}>\n <p>{props.note}</p>\n </Show>\n </Match>\n </Switch>\n </div>\n );\n}\n\nexport default InstallGuide;\n"],"mappings":"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","ignoreList":[]}
@@ -3,8 +3,9 @@ import { createSignal, onCleanup } from "solid-js";
3
3
  /**
4
4
  * Which installation steps a browser needs. `apple-in-app` and `in-app` are browsers embedded in another app, on
5
5
  * iPhone or iPad and elsewhere, which cannot install at all. `apple-browser` is Chrome, Firefox, Edge, or another
6
- * browser on iPhone or iPad, which installs through its Share menu but less reliably than Safari. `android-browser`
7
- * is an Android browser other than Chrome.
6
+ * browser on iPhone or iPad, which installs through its Share menu but less reliably than Safari. `android-samsung` is
7
+ * Samsung Internet on Android, whose installed apps Android may block as built for an older Android version.
8
+ * `android-browser` is another Android browser, such as Firefox or Edge.
8
9
  */
9
10
 
10
11
  /** Tokens of apps that open links in their own browser view. Case-sensitive, so `Line/` matches only LINE. */
@@ -12,7 +13,7 @@ const EMBEDDED = /FBAN|FBAV|FB_IAB|Instagram|LinkedInApp|Twitter|Snapchat|musica
12
13
  /** Browsers on iPhone and iPad other than Safari. */
13
14
  const APPLE_BROWSER = /CriOS\/|FxiOS\/|EdgiOS\/|OPiOS\/|OPT\/|Ddg\/|DuckDuckGo\/|YaBrowser\//;
14
15
  /** Android browsers whose user agent also names Chrome. */
15
- const ANDROID_OTHER = /SamsungBrowser|EdgA\/|OPR\/|YaBrowser|Firefox\//;
16
+ const ANDROID_OTHER = /EdgA\/|OPR\/|YaBrowser|Firefox\//;
16
17
 
17
18
  /**
18
19
  * Classifies the browser from its user agent; `platform` and `touchPoints` only tell an iPad that reports a Mac apart.
@@ -29,7 +30,10 @@ export function installationPlatform(userAgent, platform, touchPoints) {
29
30
  }
30
31
  if (EMBEDDED.test(userAgent)) return "in-app";
31
32
  if (/Macintosh/.test(userAgent) && /Safari/.test(userAgent) && !/Chrome|Chromium|Edg\//.test(userAgent)) return "apple-desktop";
32
- if (/Android/.test(userAgent)) return /Chrome\//.test(userAgent) && !ANDROID_OTHER.test(userAgent) ? "android" : "android-browser";
33
+ if (/Android/.test(userAgent)) {
34
+ if (/SamsungBrowser\//.test(userAgent)) return "android-samsung";
35
+ return /Chrome\//.test(userAgent) && !ANDROID_OTHER.test(userAgent) ? "android" : "android-browser";
36
+ }
33
37
  return "generic";
34
38
  }
35
39
  function isInstallPrompt(event) {
@@ -1 +1 @@
1
- {"version":3,"names":[],"sources":["feedback/install.ts"],"sourcesContent":["import { createSignal, onCleanup } from \"solid-js\";\n\n/**\n * Which installation steps a browser needs. `apple-in-app` and `in-app` are browsers embedded in another app, on\n * iPhone or iPad and elsewhere, which cannot install at all. `apple-browser` is Chrome, Firefox, Edge, or another\n * browser on iPhone or iPad, which installs through its Share menu but less reliably than Safari. `android-browser`\n * is an Android browser other than Chrome.\n */\nexport type InstallationPlatform =\n | \"in-app\"\n | \"apple-in-app\"\n | \"apple-browser\"\n | \"apple-mobile\"\n | \"apple-desktop\"\n | \"android-browser\"\n | \"android\"\n | \"generic\";\n\n/** Tokens of apps that open links in their own browser view. Case-sensitive, so `Line/` matches only LINE. */\nconst EMBEDDED =\n /FBAN|FBAV|FB_IAB|Instagram|LinkedInApp|Twitter|Snapchat|musical_ly|BytedanceWebview|Pinterest|MicroMessenger|\\bLine\\/|GSA\\/|; wv\\)/;\n/** Browsers on iPhone and iPad other than Safari. */\nconst APPLE_BROWSER = /CriOS\\/|FxiOS\\/|EdgiOS\\/|OPiOS\\/|OPT\\/|Ddg\\/|DuckDuckGo\\/|YaBrowser\\//;\n/** Android browsers whose user agent also names Chrome. */\nconst ANDROID_OTHER = /SamsungBrowser|EdgA\\/|OPR\\/|YaBrowser|Firefox\\//;\n\n/**\n * Classifies the browser from its user agent; `platform` and `touchPoints` only tell an iPad that reports a Mac apart.\n * A pure function, so the server and the browser compute the same value from the same user agent. Not a device\n * check: an installed app on iPhone may itself look like an embedded browser.\n */\nexport function installationPlatform(userAgent: string, platform: string, touchPoints: number): InstallationPlatform {\n if (/iPad|iPhone|iPod/.test(userAgent) || (platform === \"MacIntel\" && touchPoints > 1)) {\n if (EMBEDDED.test(userAgent)) return \"apple-in-app\";\n if (APPLE_BROWSER.test(userAgent)) return \"apple-browser\";\n // Safari and every real browser send `Safari/`; a bare web view inside an app, such as a QR scanner, does not.\n if (!/Safari\\//.test(userAgent)) return \"apple-in-app\";\n return \"apple-mobile\";\n }\n if (EMBEDDED.test(userAgent)) return \"in-app\";\n if (/Macintosh/.test(userAgent) && /Safari/.test(userAgent) && !/Chrome|Chromium|Edg\\//.test(userAgent)) return \"apple-desktop\";\n if (/Android/.test(userAgent)) return /Chrome\\//.test(userAgent) && !ANDROID_OTHER.test(userAgent) ? \"android\" : \"android-browser\";\n return \"generic\";\n}\n\ninterface BeforeInstallPromptEvent extends Event {\n prompt(): Promise<{ outcome: \"accepted\" | \"dismissed\" }>;\n}\n\nfunction isInstallPrompt(event: Event): event is BeforeInstallPromptEvent {\n return \"prompt\" in event && typeof event.prompt === \"function\";\n}\n\nexport type InstallPrompt = {\n platform: InstallationPlatform;\n /** Running from the Home Screen or as an installed app, or installed during this visit. */\n installed: () => boolean;\n /** The browser offered its own installation dialog, which `install()` opens. */\n canPrompt: () => boolean;\n busy: () => boolean;\n /** The person accepted the browser's dialog; the browser finishes the installation. */\n requested: () => boolean;\n /** The browser's dialog could not be opened. */\n failed: () => boolean;\n /** Opens the browser's dialog once. Call it directly from a click. */\n install: () => Promise<void>;\n};\n\n/**\n * Tracks whether the page runs installed and captures the browser's installation prompt. Browser-only; call it once,\n * when the page loads, under an owner that lives as long as the page: Chrome offers its prompt only once, early.\n */\nexport function createInstallPrompt(): InstallPrompt {\n const displayMode = matchMedia(\"(display-mode: standalone)\");\n const isStandalone = () => displayMode.matches || (\"standalone\" in navigator && navigator.standalone === true);\n const [installed, setInstalled] = createSignal(isStandalone());\n const [prompt, setPrompt] = createSignal<BeforeInstallPromptEvent>();\n const [busy, setBusy] = createSignal(false);\n const [requested, setRequested] = createSignal(false);\n const [failed, setFailed] = createSignal(false);\n const capture = (event: Event) => {\n if (!isInstallPrompt(event)) return;\n event.preventDefault();\n if (installed()) return;\n setPrompt(event);\n setRequested(false);\n };\n const complete = () => {\n setInstalled(true);\n setPrompt(undefined);\n };\n const displayChanged = () => {\n if (isStandalone()) complete();\n };\n window.addEventListener(\"beforeinstallprompt\", capture);\n window.addEventListener(\"appinstalled\", complete);\n displayMode.addEventListener(\"change\", displayChanged);\n onCleanup(() => {\n window.removeEventListener(\"beforeinstallprompt\", capture);\n window.removeEventListener(\"appinstalled\", complete);\n displayMode.removeEventListener(\"change\", displayChanged);\n });\n const install = async () => {\n if (busy() || installed()) return;\n const event = prompt();\n setFailed(false);\n if (!event) return;\n // Consume once, and invoke synchronously within the user's click.\n setPrompt(undefined);\n setBusy(true);\n try {\n const result = await event.prompt();\n if (!installed()) setRequested(result.outcome === \"accepted\");\n } catch {\n setFailed(true);\n } finally {\n setBusy(false);\n }\n };\n return {\n platform: installationPlatform(navigator.userAgent, navigator.platform, navigator.maxTouchPoints),\n installed,\n canPrompt: () => Boolean(prompt()),\n busy,\n requested,\n failed,\n install,\n };\n}\n"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":[],"sources":["feedback/install.ts"],"sourcesContent":["import { createSignal, onCleanup } from \"solid-js\";\n\n/**\n * Which installation steps a browser needs. `apple-in-app` and `in-app` are browsers embedded in another app, on\n * iPhone or iPad and elsewhere, which cannot install at all. `apple-browser` is Chrome, Firefox, Edge, or another\n * browser on iPhone or iPad, which installs through its Share menu but less reliably than Safari. `android-samsung` is\n * Samsung Internet on Android, whose installed apps Android may block as built for an older Android version.\n * `android-browser` is another Android browser, such as Firefox or Edge.\n */\nexport type InstallationPlatform =\n | \"in-app\"\n | \"apple-in-app\"\n | \"apple-browser\"\n | \"apple-mobile\"\n | \"apple-desktop\"\n | \"android-samsung\"\n | \"android-browser\"\n | \"android\"\n | \"generic\";\n\n/** Tokens of apps that open links in their own browser view. Case-sensitive, so `Line/` matches only LINE. */\nconst EMBEDDED =\n /FBAN|FBAV|FB_IAB|Instagram|LinkedInApp|Twitter|Snapchat|musical_ly|BytedanceWebview|Pinterest|MicroMessenger|\\bLine\\/|GSA\\/|; wv\\)/;\n/** Browsers on iPhone and iPad other than Safari. */\nconst APPLE_BROWSER = /CriOS\\/|FxiOS\\/|EdgiOS\\/|OPiOS\\/|OPT\\/|Ddg\\/|DuckDuckGo\\/|YaBrowser\\//;\n/** Android browsers whose user agent also names Chrome. */\nconst ANDROID_OTHER = /EdgA\\/|OPR\\/|YaBrowser|Firefox\\//;\n\n/**\n * Classifies the browser from its user agent; `platform` and `touchPoints` only tell an iPad that reports a Mac apart.\n * A pure function, so the server and the browser compute the same value from the same user agent. Not a device\n * check: an installed app on iPhone may itself look like an embedded browser.\n */\nexport function installationPlatform(userAgent: string, platform: string, touchPoints: number): InstallationPlatform {\n if (/iPad|iPhone|iPod/.test(userAgent) || (platform === \"MacIntel\" && touchPoints > 1)) {\n if (EMBEDDED.test(userAgent)) return \"apple-in-app\";\n if (APPLE_BROWSER.test(userAgent)) return \"apple-browser\";\n // Safari and every real browser send `Safari/`; a bare web view inside an app, such as a QR scanner, does not.\n if (!/Safari\\//.test(userAgent)) return \"apple-in-app\";\n return \"apple-mobile\";\n }\n if (EMBEDDED.test(userAgent)) return \"in-app\";\n if (/Macintosh/.test(userAgent) && /Safari/.test(userAgent) && !/Chrome|Chromium|Edg\\//.test(userAgent)) return \"apple-desktop\";\n if (/Android/.test(userAgent)) {\n if (/SamsungBrowser\\//.test(userAgent)) return \"android-samsung\";\n return /Chrome\\//.test(userAgent) && !ANDROID_OTHER.test(userAgent) ? \"android\" : \"android-browser\";\n }\n return \"generic\";\n}\n\ninterface BeforeInstallPromptEvent extends Event {\n prompt(): Promise<{ outcome: \"accepted\" | \"dismissed\" }>;\n}\n\nfunction isInstallPrompt(event: Event): event is BeforeInstallPromptEvent {\n return \"prompt\" in event && typeof event.prompt === \"function\";\n}\n\nexport type InstallPrompt = {\n platform: InstallationPlatform;\n /** Running from the Home Screen or as an installed app, or installed during this visit. */\n installed: () => boolean;\n /** The browser offered its own installation dialog, which `install()` opens. */\n canPrompt: () => boolean;\n busy: () => boolean;\n /** The person accepted the browser's dialog; the browser finishes the installation. */\n requested: () => boolean;\n /** The browser's dialog could not be opened. */\n failed: () => boolean;\n /** Opens the browser's dialog once. Call it directly from a click. */\n install: () => Promise<void>;\n};\n\n/**\n * Tracks whether the page runs installed and captures the browser's installation prompt. Browser-only; call it once,\n * when the page loads, under an owner that lives as long as the page: Chrome offers its prompt only once, early.\n */\nexport function createInstallPrompt(): InstallPrompt {\n const displayMode = matchMedia(\"(display-mode: standalone)\");\n const isStandalone = () => displayMode.matches || (\"standalone\" in navigator && navigator.standalone === true);\n const [installed, setInstalled] = createSignal(isStandalone());\n const [prompt, setPrompt] = createSignal<BeforeInstallPromptEvent>();\n const [busy, setBusy] = createSignal(false);\n const [requested, setRequested] = createSignal(false);\n const [failed, setFailed] = createSignal(false);\n const capture = (event: Event) => {\n if (!isInstallPrompt(event)) return;\n event.preventDefault();\n if (installed()) return;\n setPrompt(event);\n setRequested(false);\n };\n const complete = () => {\n setInstalled(true);\n setPrompt(undefined);\n };\n const displayChanged = () => {\n if (isStandalone()) complete();\n };\n window.addEventListener(\"beforeinstallprompt\", capture);\n window.addEventListener(\"appinstalled\", complete);\n displayMode.addEventListener(\"change\", displayChanged);\n onCleanup(() => {\n window.removeEventListener(\"beforeinstallprompt\", capture);\n window.removeEventListener(\"appinstalled\", complete);\n displayMode.removeEventListener(\"change\", displayChanged);\n });\n const install = async () => {\n if (busy() || installed()) return;\n const event = prompt();\n setFailed(false);\n if (!event) return;\n // Consume once, and invoke synchronously within the user's click.\n setPrompt(undefined);\n setBusy(true);\n try {\n const result = await event.prompt();\n if (!installed()) setRequested(result.outcome === \"accepted\");\n } catch {\n setFailed(true);\n } finally {\n setBusy(false);\n }\n };\n return {\n platform: installationPlatform(navigator.userAgent, navigator.platform, navigator.maxTouchPoints),\n installed,\n canPrompt: () => Boolean(prompt()),\n busy,\n requested,\n failed,\n install,\n };\n}\n"],"mappings":"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","ignoreList":[]}
@@ -7,18 +7,87 @@ var _tmpl$ = ["<p class=\"k2b-qr-scanner__note\">", "</p>"],
7
7
  import { timed } from "@k2b/stdlib/solid";
8
8
  import { createSignal, onCleanup, onMount, Show } from "solid-js";
9
9
  import { useUiMessages } from "../intl/messages";
10
+
10
11
  /**
11
- * qr-scanner turns every getUserMedia failure into the string "Camera not found.", so only the Permissions API can
12
- * tell a refusal apart. Safari and Chrome expose the camera permission; elsewhere a refusal counts as unavailable.
12
+ * Why the scanner could not start, as far as the browser tells: `"denied"` when camera access is off, `"no-camera"`
13
+ * when the device has no camera, `"in-use"` when the camera is busy or could not start, most often because another app
14
+ * holds it, and `"unavailable"` for anything else, including a scanning engine that cannot load.
13
15
  */
14
- const cameraDenied = async () => {
15
- try {
16
- return (await navigator.permissions.query({
17
- name: "camera"
18
- })).state === "denied";
19
- } catch {
20
- return false;
16
+
17
+ const sizes = [{
18
+ width: {
19
+ min: 1024
20
+ }
21
+ }, {
22
+ width: {
23
+ min: 768
24
+ }
25
+ }, {}];
26
+ /** The rear camera at a resolution that keeps small codes readable, then any camera: the order qr-scanner tries. */
27
+ const cameraRequests = [...sizes.map(size => ({
28
+ ...size,
29
+ facingMode: {
30
+ exact: "environment"
21
31
  }
32
+ })), ...sizes];
33
+ const errorName = error => typeof error === "object" && error !== null && "name" in error ? String(error.name) : "";
34
+ const cameraFailure = name => {
35
+ if (name === "NotAllowedError" || name === "SecurityError") return "denied";
36
+ if (name === "NotFoundError" || name === "OverconstrainedError") return "no-camera";
37
+ if (name === "NotReadableError") return "in-use";
38
+ return "unavailable";
39
+ };
40
+
41
+ /** The facing a track reports, or else the one its label names, as qr-scanner reads it. */
42
+ const facingOf = track => {
43
+ if (!track) return undefined;
44
+ const {
45
+ facingMode
46
+ } = track.getSettings();
47
+ if (facingMode) return facingMode;
48
+ if (/rear|back|environment/i.test(track.label)) return "environment";
49
+ if (/front|user|face/i.test(track.label)) return "user";
50
+ return undefined;
51
+ };
52
+
53
+ /**
54
+ * Opens the camera here rather than in qr-scanner, which turns every failure into "Camera not found.". A refusal ends
55
+ * the search, because asking again could show the permission prompt again. Any other failure may belong to the request,
56
+ * such as a camera that cannot start at the larger size, so the next, looser request follows, as in qr-scanner. When
57
+ * none succeeds, a camera that exists but fails outweighs a request no camera could meet. A closed scanner asks no more.
58
+ */
59
+ const openCamera = async closed => {
60
+ if (!navigator.mediaDevices) return {
61
+ error: "unavailable"
62
+ };
63
+ let error = "no-camera";
64
+ for (const video of cameraRequests) {
65
+ if (closed()) break;
66
+ try {
67
+ const stream = await navigator.mediaDevices.getUserMedia({
68
+ video,
69
+ audio: false
70
+ });
71
+ const facing = facingOf(stream.getVideoTracks()[0]);
72
+ // Like qr-scanner, mirror a front camera, and a camera of unknown facing that the rear request did not find.
73
+ return {
74
+ stream,
75
+ mirrored: facing ? facing === "user" : !video.facingMode
76
+ };
77
+ } catch (failure) {
78
+ const reason = cameraFailure(errorName(failure));
79
+ if (reason === "denied") return {
80
+ error: reason
81
+ };
82
+ if (reason !== "no-camera") error = reason;
83
+ }
84
+ }
85
+ return {
86
+ error
87
+ };
88
+ };
89
+ const stopStream = stream => {
90
+ for (const track of stream.getTracks()) track.stop();
22
91
  };
23
92
 
24
93
  /**
@@ -38,24 +107,22 @@ export function QrScanner(props) {
38
107
  let video;
39
108
  let scanner;
40
109
  let closed = false;
41
- /** The host has unmounted the scanner; it no longer hears about failures that finish later. */
42
- let unmounted = false;
43
110
  let cleanupMotion;
44
111
  const destroy = () => {
45
112
  closed = true;
46
113
  cleanupMotion?.();
47
114
  resetRejected.cancel();
48
115
  resetFeedback.cancel();
49
- // pause(true) stops existing tracks immediately; destroy also handles a still-pending permission request.
116
+ // pause(true) stops the camera stream immediately.
50
117
  void scanner?.pause(true);
51
118
  scanner?.destroy();
52
119
  scanner = undefined;
53
120
  };
54
- const fail = async () => {
121
+ // Once closed, the host has unmounted the scanner or heard why it stopped; later failures stay silent.
122
+ const fail = reason => {
55
123
  if (closed) return;
56
124
  destroy();
57
- const denied = await cameraDenied();
58
- if (!unmounted) props.onError(denied ? "denied" : "unavailable");
125
+ props.onError(reason);
59
126
  };
60
127
  onMount(() => {
61
128
  const hidden = () => {
@@ -65,7 +132,6 @@ export function QrScanner(props) {
65
132
  document.addEventListener("visibilitychange", hidden);
66
133
  window.addEventListener("pagehide", leaving);
67
134
  onCleanup(() => {
68
- unmounted = true;
69
135
  destroy();
70
136
  document.removeEventListener("visibilitychange", hidden);
71
137
  window.removeEventListener("pagehide", leaving);
@@ -89,12 +155,20 @@ export function QrScanner(props) {
89
155
  setInvalid(true);
90
156
  resetFeedback.debouncedFn();
91
157
  }, {
92
- preferredCamera: "environment",
93
158
  highlightScanRegion: true,
94
- // Empty frames are expected. Never log decoded content or frame data.
159
+ // A frame that cannot be decoded never ends the session: the camera runs, and the next frame may read.
160
+ // An engine error other than an empty frame, which the native BarcodeDetector reports as "Scanner error:
161
+ // No QR code found", means the detector cannot read on this device. Fall back to the bundled worker for
162
+ // the page through the flag qr-scanner 1.4.2 sets itself for "not implemented" and "service unavailable";
163
+ // it swaps engines after this frame. Worker errors carry the same prefix; the flag changes nothing there.
164
+ // qr-scanner reports frames only as strings: an Error means the engine itself did not load, such as a
165
+ // decoder chunk gone after a deploy or a worker that cannot start, and no later frame can read. Never log
166
+ // decoded content or frame data.
95
167
  onDecodeError: error => {
96
- if (closed || error === Engine.NO_QR_CODE_FOUND) return;
97
- void fail();
168
+ if (typeof error !== "string") return fail("unavailable");
169
+ if (error.startsWith("Scanner error: ") && !error.endsWith(Engine.NO_QR_CODE_FOUND)) {
170
+ Reflect.set(Engine, "_disableBarcodeDetector", true);
171
+ }
98
172
  }
99
173
  });
100
174
  const motion = matchMedia("(prefers-reduced-motion: reduce)");
@@ -108,10 +182,16 @@ export function QrScanner(props) {
108
182
  applyMotion();
109
183
  motion.addEventListener("change", applyMotion);
110
184
  cleanupMotion = () => motion.removeEventListener("change", applyMotion);
185
+ const camera = await openCamera(() => closed);
186
+ if ("error" in camera) return fail(camera.error);
187
+ if (closed) return stopStream(camera.stream);
188
+ // qr-scanner plays a stream that is already set instead of opening its own.
189
+ video.srcObject = camera.stream;
190
+ video.style.transform = camera.mirrored ? "scaleX(-1)" : "";
111
191
  await scanner.start();
112
192
  if (!closed) setStarting(false);
113
193
  } catch {
114
- await fail();
194
+ fail("unavailable");
115
195
  }
116
196
  })();
117
197
  });