@k2b/ui 0.32.0 → 0.34.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/dist/base.css +14 -1
- package/dist/browser/actions/ContextMenu.js +13 -0
- package/dist/browser/actions/ContextMenu.js.map +1 -1
- package/dist/browser/actions/Dropdown.js +173 -102
- package/dist/browser/actions/Dropdown.js.map +1 -1
- package/dist/browser/actions/FilterChip.js +113 -56
- package/dist/browser/actions/FilterChip.js.map +1 -1
- package/dist/browser/actions/SelectionToolbar.js +129 -0
- package/dist/browser/actions/SelectionToolbar.js.map +1 -0
- package/dist/browser/actions/filter-messages.js +26 -2
- package/dist/browser/actions/filter-messages.js.map +1 -1
- package/dist/browser/actions/index.js +1 -0
- package/dist/browser/actions/index.js.map +1 -1
- package/dist/browser/actions/menu-rows.js +23 -3
- package/dist/browser/actions/menu-rows.js.map +1 -1
- package/dist/browser/actions/menu-sheet.js +2 -7
- package/dist/browser/actions/menu-sheet.js.map +1 -1
- package/dist/browser/charts.js +6 -0
- package/dist/browser/charts.js.map +1 -0
- package/dist/browser/chat/ChatComposer.js +616 -421
- package/dist/browser/chat/ChatComposer.js.map +1 -1
- package/dist/browser/chat/chat-messages.js +18 -0
- package/dist/browser/chat/chat-messages.js.map +1 -1
- package/dist/browser/chat/composer-document.js +19 -0
- package/dist/browser/chat/composer-document.js.map +1 -1
- package/dist/browser/chat/index.js.map +1 -1
- package/dist/browser/content/Calendar.js +39 -29
- package/dist/browser/content/Calendar.js.map +1 -1
- package/dist/browser/content/CalendarDayView.js +159 -165
- package/dist/browser/content/CalendarDayView.js.map +1 -1
- package/dist/browser/content/Chart.js +8 -0
- package/dist/browser/content/Chart.js.map +1 -1
- package/dist/browser/content/FileGrid.js +231 -9
- package/dist/browser/content/FileGrid.js.map +1 -1
- package/dist/browser/content/FileTypeIcon.js +15 -0
- package/dist/browser/content/FileTypeIcon.js.map +1 -1
- package/dist/browser/content/FileView.js +11 -4
- package/dist/browser/content/FileView.js.map +1 -1
- package/dist/browser/content/MessageRow.js +627 -343
- package/dist/browser/content/MessageRow.js.map +1 -1
- package/dist/browser/content/ReactionBar.js +20 -1
- package/dist/browser/content/ReactionBar.js.map +1 -1
- package/dist/browser/content/calendar-day-scroll.js +4 -3
- package/dist/browser/content/calendar-day-scroll.js.map +1 -1
- package/dist/browser/content/chart-bar.js +476 -0
- package/dist/browser/content/chart-bar.js.map +1 -0
- package/dist/browser/content/file-list-messages.js +52 -0
- package/dist/browser/content/file-list-messages.js.map +1 -0
- package/dist/browser/content/index.js +2 -2
- package/dist/browser/content/index.js.map +1 -1
- package/dist/browser/feedback/Tooltip.js +31 -14
- package/dist/browser/feedback/Tooltip.js.map +1 -1
- package/dist/browser/feedback/dialog-core.js +6 -0
- package/dist/browser/feedback/dialog-core.js.map +1 -1
- package/dist/browser/inputs/ChoiceList.js +178 -0
- package/dist/browser/inputs/ChoiceList.js.map +1 -0
- package/dist/browser/inputs/ChoiceSlider.js +314 -0
- package/dist/browser/inputs/ChoiceSlider.js.map +1 -0
- package/dist/browser/inputs/ChoiceTiles.js +135 -0
- package/dist/browser/inputs/ChoiceTiles.js.map +1 -0
- package/dist/browser/inputs/ColorChoices.js +120 -0
- package/dist/browser/inputs/ColorChoices.js.map +1 -0
- package/dist/browser/inputs/QrScanner.js +95 -13
- package/dist/browser/inputs/QrScanner.js.map +1 -1
- package/dist/browser/inputs/TextInput.js +19 -14
- package/dist/browser/inputs/TextInput.js.map +1 -1
- package/dist/browser/inputs/index.js +4 -0
- package/dist/browser/inputs/index.js.map +1 -1
- package/dist/browser/inputs/qr-messages.js +2 -0
- package/dist/browser/inputs/qr-messages.js.map +1 -1
- package/dist/browser/internal/radio-keys.js +13 -0
- package/dist/browser/internal/radio-keys.js.map +1 -0
- package/dist/browser/intl/shortcut-messages.js +4 -0
- package/dist/browser/intl/shortcut-messages.js.map +1 -1
- package/dist/browser/intl/shortcut.js +4 -0
- package/dist/browser/intl/shortcut.js.map +1 -1
- package/dist/browser/layout/AnchoredPopover.js +180 -0
- package/dist/browser/layout/AnchoredPopover.js.map +1 -0
- package/dist/browser/layout/AppWorkspace.js +684 -532
- package/dist/browser/layout/AppWorkspace.js.map +1 -1
- package/dist/browser/layout/DetailPanel.js +443 -333
- package/dist/browser/layout/DetailPanel.js.map +1 -1
- package/dist/browser/layout/MobileShell.js +231 -42
- package/dist/browser/layout/MobileShell.js.map +1 -1
- package/dist/browser/layout/Settings.js +283 -175
- package/dist/browser/layout/Settings.js.map +1 -1
- package/dist/browser/layout/SidebarRailHint.js +79 -0
- package/dist/browser/layout/SidebarRailHint.js.map +1 -0
- package/dist/browser/layout/SidebarSwitcher.js +79 -66
- package/dist/browser/layout/SidebarSwitcher.js.map +1 -1
- package/dist/browser/layout/TabBar.js +18 -4
- package/dist/browser/layout/TabBar.js.map +1 -1
- package/dist/browser/layout/app-workspace-controller.js +11 -4
- package/dist/browser/layout/app-workspace-controller.js.map +1 -1
- package/dist/browser/layout/detail-panel-messages.js +4 -2
- package/dist/browser/layout/detail-panel-messages.js.map +1 -1
- package/dist/browser/layout/index.js +3 -1
- package/dist/browser/layout/index.js.map +1 -1
- package/dist/browser/layout/scroll-fade.js +32 -5
- package/dist/browser/layout/scroll-fade.js.map +1 -1
- package/dist/browser/layout/settings-messages.js +26 -2
- package/dist/browser/layout/settings-messages.js.map +1 -1
- package/dist/browser/surfaces/Avatar.js +2 -14
- package/dist/browser/surfaces/Avatar.js.map +1 -1
- package/dist/browser/surfaces/DotStrip.js +70 -0
- package/dist/browser/surfaces/DotStrip.js.map +1 -0
- package/dist/browser/surfaces/NoticeCard.js +46 -30
- package/dist/browser/surfaces/NoticeCard.js.map +1 -1
- package/dist/browser/surfaces/ProgressRing.js +3 -0
- package/dist/browser/surfaces/ProgressRing.js.map +1 -1
- package/dist/browser/surfaces/ResourceCard.js +125 -68
- package/dist/browser/surfaces/ResourceCard.js.map +1 -1
- package/dist/browser/surfaces/index.js +2 -0
- package/dist/browser/surfaces/index.js.map +1 -1
- package/dist/browser/surfaces/person-color.js +18 -0
- package/dist/browser/surfaces/person-color.js.map +1 -0
- package/dist/browser/surfaces/resource-messages.js +8 -4
- package/dist/browser/surfaces/resource-messages.js.map +1 -1
- package/dist/browser/widgets/Widget.js +3 -2
- package/dist/browser/widgets/Widget.js.map +1 -1
- package/dist/ssr/actions/ContextMenu.js +9 -0
- package/dist/ssr/actions/ContextMenu.js.map +1 -1
- package/dist/ssr/actions/Dropdown.js +65 -31
- package/dist/ssr/actions/Dropdown.js.map +1 -1
- package/dist/ssr/actions/FilterChip.js +67 -16
- package/dist/ssr/actions/FilterChip.js.map +1 -1
- package/dist/ssr/actions/SelectionToolbar.js +104 -0
- package/dist/ssr/actions/SelectionToolbar.js.map +1 -0
- package/dist/ssr/actions/filter-messages.js +26 -2
- package/dist/ssr/actions/filter-messages.js.map +1 -1
- package/dist/ssr/actions/index.js +1 -0
- package/dist/ssr/actions/index.js.map +1 -1
- package/dist/ssr/actions/menu-rows.js +23 -3
- package/dist/ssr/actions/menu-rows.js.map +1 -1
- package/dist/ssr/actions/menu-sheet.js +2 -7
- package/dist/ssr/actions/menu-sheet.js.map +1 -1
- package/dist/ssr/charts.js +6 -0
- package/dist/ssr/charts.js.map +1 -0
- package/dist/ssr/chat/ChatComposer.js +184 -76
- package/dist/ssr/chat/ChatComposer.js.map +1 -1
- package/dist/ssr/chat/chat-messages.js +18 -0
- package/dist/ssr/chat/chat-messages.js.map +1 -1
- package/dist/ssr/chat/composer-document.js +19 -0
- package/dist/ssr/chat/composer-document.js.map +1 -1
- package/dist/ssr/chat/index.js.map +1 -1
- package/dist/ssr/content/Calendar.js +40 -31
- package/dist/ssr/content/Calendar.js.map +1 -1
- package/dist/ssr/content/CalendarDayView.js +40 -48
- package/dist/ssr/content/CalendarDayView.js.map +1 -1
- package/dist/ssr/content/Chart.js +8 -0
- package/dist/ssr/content/Chart.js.map +1 -1
- package/dist/ssr/content/FileGrid.js +132 -5
- package/dist/ssr/content/FileGrid.js.map +1 -1
- package/dist/ssr/content/FileTypeIcon.js +15 -0
- package/dist/ssr/content/FileTypeIcon.js.map +1 -1
- package/dist/ssr/content/FileView.js +11 -4
- package/dist/ssr/content/FileView.js.map +1 -1
- package/dist/ssr/content/MessageRow.js +274 -91
- package/dist/ssr/content/MessageRow.js.map +1 -1
- package/dist/ssr/content/ReactionBar.js +20 -1
- package/dist/ssr/content/ReactionBar.js.map +1 -1
- package/dist/ssr/content/calendar-day-scroll.js +4 -3
- package/dist/ssr/content/calendar-day-scroll.js.map +1 -1
- package/dist/ssr/content/chart-bar.js +476 -0
- package/dist/ssr/content/chart-bar.js.map +1 -0
- package/dist/ssr/content/file-list-messages.js +52 -0
- package/dist/ssr/content/file-list-messages.js.map +1 -0
- package/dist/ssr/content/index.js +2 -2
- package/dist/ssr/content/index.js.map +1 -1
- package/dist/ssr/feedback/Tooltip.js +31 -14
- package/dist/ssr/feedback/Tooltip.js.map +1 -1
- package/dist/ssr/feedback/dialog-core.js +6 -0
- package/dist/ssr/feedback/dialog-core.js.map +1 -1
- package/dist/ssr/inputs/ChoiceList.js +95 -0
- package/dist/ssr/inputs/ChoiceList.js.map +1 -0
- package/dist/ssr/inputs/ChoiceSlider.js +191 -0
- package/dist/ssr/inputs/ChoiceSlider.js.map +1 -0
- package/dist/ssr/inputs/ChoiceTiles.js +61 -0
- package/dist/ssr/inputs/ChoiceTiles.js.map +1 -0
- package/dist/ssr/inputs/ColorChoices.js +60 -0
- package/dist/ssr/inputs/ColorChoices.js.map +1 -0
- package/dist/ssr/inputs/QrScanner.js +89 -10
- package/dist/ssr/inputs/QrScanner.js.map +1 -1
- package/dist/ssr/inputs/TextInput.js +17 -2
- package/dist/ssr/inputs/TextInput.js.map +1 -1
- package/dist/ssr/inputs/index.js +4 -0
- package/dist/ssr/inputs/index.js.map +1 -1
- package/dist/ssr/inputs/qr-messages.js +2 -0
- package/dist/ssr/inputs/qr-messages.js.map +1 -1
- package/dist/ssr/internal/radio-keys.js +13 -0
- package/dist/ssr/internal/radio-keys.js.map +1 -0
- package/dist/ssr/intl/shortcut-messages.js +4 -0
- package/dist/ssr/intl/shortcut-messages.js.map +1 -1
- package/dist/ssr/intl/shortcut.js +4 -0
- package/dist/ssr/intl/shortcut.js.map +1 -1
- package/dist/ssr/layout/AnchoredPopover.js +142 -0
- package/dist/ssr/layout/AnchoredPopover.js.map +1 -0
- package/dist/ssr/layout/AppWorkspace.js +287 -141
- package/dist/ssr/layout/AppWorkspace.js.map +1 -1
- package/dist/ssr/layout/DetailPanel.js +181 -108
- package/dist/ssr/layout/DetailPanel.js.map +1 -1
- package/dist/ssr/layout/MobileShell.js +118 -24
- package/dist/ssr/layout/MobileShell.js.map +1 -1
- package/dist/ssr/layout/Settings.js +122 -45
- package/dist/ssr/layout/Settings.js.map +1 -1
- package/dist/ssr/layout/SidebarRailHint.js +79 -0
- package/dist/ssr/layout/SidebarRailHint.js.map +1 -0
- package/dist/ssr/layout/SidebarSwitcher.js +13 -3
- package/dist/ssr/layout/SidebarSwitcher.js.map +1 -1
- package/dist/ssr/layout/TabBar.js +11 -3
- package/dist/ssr/layout/TabBar.js.map +1 -1
- package/dist/ssr/layout/app-workspace-controller.js +11 -4
- package/dist/ssr/layout/app-workspace-controller.js.map +1 -1
- package/dist/ssr/layout/detail-panel-messages.js +4 -2
- package/dist/ssr/layout/detail-panel-messages.js.map +1 -1
- package/dist/ssr/layout/index.js +3 -1
- package/dist/ssr/layout/index.js.map +1 -1
- package/dist/ssr/layout/scroll-fade.js +32 -5
- package/dist/ssr/layout/scroll-fade.js.map +1 -1
- package/dist/ssr/layout/settings-messages.js +26 -2
- package/dist/ssr/layout/settings-messages.js.map +1 -1
- package/dist/ssr/surfaces/Avatar.js +2 -14
- package/dist/ssr/surfaces/Avatar.js.map +1 -1
- package/dist/ssr/surfaces/DotStrip.js +31 -0
- package/dist/ssr/surfaces/DotStrip.js.map +1 -0
- package/dist/ssr/surfaces/NoticeCard.js +21 -12
- package/dist/ssr/surfaces/NoticeCard.js.map +1 -1
- package/dist/ssr/surfaces/ProgressRing.js +3 -0
- package/dist/ssr/surfaces/ProgressRing.js.map +1 -1
- package/dist/ssr/surfaces/ResourceCard.js +40 -15
- package/dist/ssr/surfaces/ResourceCard.js.map +1 -1
- package/dist/ssr/surfaces/index.js +2 -0
- package/dist/ssr/surfaces/index.js.map +1 -1
- package/dist/ssr/surfaces/person-color.js +18 -0
- package/dist/ssr/surfaces/person-color.js.map +1 -0
- package/dist/ssr/surfaces/resource-messages.js +8 -4
- package/dist/ssr/surfaces/resource-messages.js.map +1 -1
- package/dist/ssr/widgets/Widget.js +3 -2
- package/dist/ssr/widgets/Widget.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/types/actions/ContextMenu.d.ts +5 -0
- package/dist/types/actions/Dropdown.d.ts +19 -0
- package/dist/types/actions/FilterChip.d.ts +11 -0
- package/dist/types/actions/SelectionToolbar.d.ts +38 -0
- package/dist/types/actions/filter-messages.d.ts +12 -0
- package/dist/types/actions/index.d.ts +2 -0
- package/dist/types/actions/menu-rows.d.ts +7 -2
- package/dist/types/charts.d.ts +5 -0
- package/dist/types/chat/ChatComposer.d.ts +28 -0
- package/dist/types/chat/chat-messages.d.ts +9 -0
- package/dist/types/chat/composer-document.d.ts +5 -0
- package/dist/types/chat/index.d.ts +1 -1
- package/dist/types/content/Calendar.d.ts +7 -0
- package/dist/types/content/Chart.d.ts +2 -1
- package/dist/types/content/FileGrid.d.ts +43 -3
- package/dist/types/content/FileTypeIcon.d.ts +10 -0
- package/dist/types/content/FileView.d.ts +5 -1
- package/dist/types/content/MessageRow.d.ts +66 -2
- package/dist/types/content/calendar-day-scroll.d.ts +2 -1
- package/dist/types/content/chart-bar.d.ts +60 -0
- package/dist/types/content/file-list-messages.d.ts +24 -0
- package/dist/types/content/index.d.ts +4 -4
- package/dist/types/feedback/Tooltip.d.ts +8 -2
- package/dist/types/feedback/dialog-core.d.ts +3 -0
- package/dist/types/inputs/ChoiceList.d.ts +28 -0
- package/dist/types/inputs/ChoiceSlider.d.ts +36 -0
- package/dist/types/inputs/ChoiceTiles.d.ts +25 -0
- package/dist/types/inputs/ColorChoices.d.ts +19 -0
- package/dist/types/inputs/index.d.ts +8 -0
- package/dist/types/inputs/qr-messages.d.ts +1 -0
- package/dist/types/internal/radio-keys.d.ts +5 -0
- package/dist/types/intl/shortcut-messages.d.ts +2 -0
- package/dist/types/layout/AnchoredPopover.d.ts +31 -0
- package/dist/types/layout/AppWorkspace.d.ts +12 -0
- package/dist/types/layout/DetailPanel.d.ts +17 -0
- package/dist/types/layout/MobileShell.d.ts +55 -5
- package/dist/types/layout/Settings.d.ts +22 -0
- package/dist/types/layout/SidebarRailHint.d.ts +26 -0
- package/dist/types/layout/TabBar.d.ts +5 -0
- package/dist/types/layout/detail-panel-messages.d.ts +1 -0
- package/dist/types/layout/index.d.ts +5 -3
- package/dist/types/layout/scroll-fade.d.ts +5 -0
- package/dist/types/layout/settings-messages.d.ts +12 -0
- package/dist/types/surfaces/DotStrip.d.ts +22 -0
- package/dist/types/surfaces/NoticeCard.d.ts +3 -0
- package/dist/types/surfaces/ProgressRing.d.ts +2 -1
- package/dist/types/surfaces/ResourceCard.d.ts +9 -0
- package/dist/types/surfaces/index.d.ts +4 -1
- package/dist/types/surfaces/person-color.d.ts +10 -0
- package/dist/types/surfaces/resource-messages.d.ts +2 -0
- package/dist/types/widgets/Widget.d.ts +3 -2
- package/package.json +7 -2
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { template as _$template } from "solid-js/web";
|
|
2
|
+
import { delegateEvents as _$delegateEvents } from "solid-js/web";
|
|
3
|
+
import { setProperty as _$setProperty } from "solid-js/web";
|
|
4
|
+
import { runHydrationEvents as _$runHydrationEvents } from "solid-js/web";
|
|
5
|
+
import { getNextMarker as _$getNextMarker } from "solid-js/web";
|
|
6
|
+
import { memo as _$memo } from "solid-js/web";
|
|
7
|
+
import { setAttribute as _$setAttribute } from "solid-js/web";
|
|
8
|
+
import { effect as _$effect } from "solid-js/web";
|
|
9
|
+
import { getNextElement as _$getNextElement } from "solid-js/web";
|
|
10
|
+
import { insert as _$insert } from "solid-js/web";
|
|
11
|
+
import { createComponent as _$createComponent } from "solid-js/web";
|
|
12
|
+
import { use as _$use } from "solid-js/web";
|
|
13
|
+
var _tmpl$ = /*#__PURE__*/_$template(`<div class=k2b-choice-tiles__group role=radiogroup>`),
|
|
14
|
+
_tmpl$2 = /*#__PURE__*/_$template(`<button type=button role=radio class=k2b-choice-tiles__tile><span class=k2b-choice-tiles__visual aria-hidden=true></span><span class=k2b-choice-tiles__text><span class=k2b-choice-tiles__label></span><!$><!/>`),
|
|
15
|
+
_tmpl$3 = /*#__PURE__*/_$template(`<span class=k2b-choice-tiles__status>`);
|
|
16
|
+
import { For } from "solid-js";
|
|
17
|
+
import { createFieldMeta, Field, fieldDescribedBy } from "../internal/field";
|
|
18
|
+
import { radioKeyTarget } from "../internal/radio-keys";
|
|
19
|
+
import { resolveMaybeAccessor } from "./field-contract";
|
|
20
|
+
/**
|
|
21
|
+
* A labeled single choice shown as tiles with a preview, for picking how something looks: a face shape, a frame, a
|
|
22
|
+
* kind of picture. Every tile keeps its size when chosen, so nothing moves. Use `ChoiceChips` for text choices and
|
|
23
|
+
* `ColorChoices` for named colours.
|
|
24
|
+
*/
|
|
25
|
+
export function ChoiceTiles(props) {
|
|
26
|
+
const meta = createFieldMeta(props.id);
|
|
27
|
+
let group;
|
|
28
|
+
const value = () => resolveMaybeAccessor(props.value) ?? null;
|
|
29
|
+
const tabStop = (option, index) => value() === option.value || index === 0 && !props.options.some(candidate => candidate.value === value());
|
|
30
|
+
const choose = (index, focus = false) => {
|
|
31
|
+
const option = props.options[index];
|
|
32
|
+
if (!option || props.disabled) return;
|
|
33
|
+
props.onValueChange?.(option.value);
|
|
34
|
+
props.onValueCommit?.(option.value);
|
|
35
|
+
// The radios in their current DOM order: options may have been reordered since they rendered.
|
|
36
|
+
if (focus) queueMicrotask(() => group?.querySelectorAll('[role="radio"]')[index]?.focus());
|
|
37
|
+
};
|
|
38
|
+
return _$createComponent(Field, {
|
|
39
|
+
meta: meta,
|
|
40
|
+
labelFor: false,
|
|
41
|
+
get ["class"]() {
|
|
42
|
+
return props.class;
|
|
43
|
+
},
|
|
44
|
+
get label() {
|
|
45
|
+
return props.label;
|
|
46
|
+
},
|
|
47
|
+
get description() {
|
|
48
|
+
return props.description;
|
|
49
|
+
},
|
|
50
|
+
get error() {
|
|
51
|
+
return props.error;
|
|
52
|
+
},
|
|
53
|
+
get required() {
|
|
54
|
+
return props.required;
|
|
55
|
+
},
|
|
56
|
+
get disabled() {
|
|
57
|
+
return props.disabled;
|
|
58
|
+
},
|
|
59
|
+
get children() {
|
|
60
|
+
var _el$ = _$getNextElement(_tmpl$);
|
|
61
|
+
var _ref$ = group;
|
|
62
|
+
typeof _ref$ === "function" ? _$use(_ref$, _el$) : group = _el$;
|
|
63
|
+
_$insert(_el$, _$createComponent(For, {
|
|
64
|
+
get each() {
|
|
65
|
+
return props.options;
|
|
66
|
+
},
|
|
67
|
+
children: (option, index) => (() => {
|
|
68
|
+
var _el$2 = _$getNextElement(_tmpl$2),
|
|
69
|
+
_el$3 = _el$2.firstChild,
|
|
70
|
+
_el$4 = _el$3.nextSibling,
|
|
71
|
+
_el$5 = _el$4.firstChild,
|
|
72
|
+
_el$6 = _el$5.nextSibling,
|
|
73
|
+
[_el$7, _co$] = _$getNextMarker(_el$6.nextSibling);
|
|
74
|
+
_el$2.$$keydown = event => {
|
|
75
|
+
const target = radioKeyTarget(event.key, index(), props.options.length);
|
|
76
|
+
if (target === undefined) return;
|
|
77
|
+
event.preventDefault();
|
|
78
|
+
choose(target, true);
|
|
79
|
+
};
|
|
80
|
+
_el$2.$$click = () => choose(index());
|
|
81
|
+
_$insert(_el$3, () => option.visual);
|
|
82
|
+
_$insert(_el$5, () => option.label);
|
|
83
|
+
_$insert(_el$4, (() => {
|
|
84
|
+
var _c$ = _$memo(() => props.layout === "row");
|
|
85
|
+
return () => _c$() && (() => {
|
|
86
|
+
var _el$8 = _$getNextElement(_tmpl$3);
|
|
87
|
+
_$insert(_el$8, () => option.status ?? "");
|
|
88
|
+
return _el$8;
|
|
89
|
+
})();
|
|
90
|
+
})(), _el$7, _co$);
|
|
91
|
+
_$effect(_p$ => {
|
|
92
|
+
var _v$7 = value() === option.value,
|
|
93
|
+
_v$8 = props.disabled,
|
|
94
|
+
_v$9 = tabStop(option, index()) ? 0 : -1;
|
|
95
|
+
_v$7 !== _p$.e && _$setAttribute(_el$2, "aria-checked", _p$.e = _v$7);
|
|
96
|
+
_v$8 !== _p$.t && _$setProperty(_el$2, "disabled", _p$.t = _v$8);
|
|
97
|
+
_v$9 !== _p$.a && _$setAttribute(_el$2, "tabindex", _p$.a = _v$9);
|
|
98
|
+
return _p$;
|
|
99
|
+
}, {
|
|
100
|
+
e: undefined,
|
|
101
|
+
t: undefined,
|
|
102
|
+
a: undefined
|
|
103
|
+
});
|
|
104
|
+
_$runHydrationEvents();
|
|
105
|
+
return _el$2;
|
|
106
|
+
})()
|
|
107
|
+
}));
|
|
108
|
+
_$effect(_p$ => {
|
|
109
|
+
var _v$ = props.layout ?? "grid",
|
|
110
|
+
_v$2 = props.dimmed ? "true" : undefined,
|
|
111
|
+
_v$3 = props.label ? undefined : props["aria-label"],
|
|
112
|
+
_v$4 = props.label ? meta.labelId : undefined,
|
|
113
|
+
_v$5 = fieldDescribedBy(meta, props),
|
|
114
|
+
_v$6 = props.disabled ? "true" : undefined;
|
|
115
|
+
_v$ !== _p$.e && _$setAttribute(_el$, "data-layout", _p$.e = _v$);
|
|
116
|
+
_v$2 !== _p$.t && _$setAttribute(_el$, "data-dimmed", _p$.t = _v$2);
|
|
117
|
+
_v$3 !== _p$.a && _$setAttribute(_el$, "aria-label", _p$.a = _v$3);
|
|
118
|
+
_v$4 !== _p$.o && _$setAttribute(_el$, "aria-labelledby", _p$.o = _v$4);
|
|
119
|
+
_v$5 !== _p$.i && _$setAttribute(_el$, "aria-describedby", _p$.i = _v$5);
|
|
120
|
+
_v$6 !== _p$.n && _$setAttribute(_el$, "aria-disabled", _p$.n = _v$6);
|
|
121
|
+
return _p$;
|
|
122
|
+
}, {
|
|
123
|
+
e: undefined,
|
|
124
|
+
t: undefined,
|
|
125
|
+
a: undefined,
|
|
126
|
+
o: undefined,
|
|
127
|
+
i: undefined,
|
|
128
|
+
n: undefined
|
|
129
|
+
});
|
|
130
|
+
return _el$;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
_$delegateEvents(["click", "keydown"]);
|
|
135
|
+
//# sourceMappingURL=ChoiceTiles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sources":["inputs/ChoiceTiles.tsx"],"sourcesContent":["import { For, type JSX } from \"solid-js\";\nimport { createFieldMeta, Field, fieldDescribedBy } from \"../internal/field\";\nimport { radioKeyTarget } from \"../internal/radio-keys\";\nimport type { ValueFieldProps } from \"./field-contract\";\nimport { resolveMaybeAccessor } from \"./field-contract\";\n\nexport type ChoiceTileOption<T extends string = string> = {\n value: T;\n label: string;\n /** What the choice looks like, such as a preview image; decorative, the label names the choice. */\n visual: JSX.Element;\n /** A short state under the label, such as \"Active\"; keeps its line, so tiles never change height. */\n status?: string;\n};\n\nexport type ChoiceTilesProps<T extends string = string> = Omit<ValueFieldProps<T | null>, \"onValueChange\" | \"onValueCommit\"> & {\n options: readonly ChoiceTileOption<T>[];\n /** `grid`: preview over label in as many columns as fit. `row`: preview beside label in one row, stacked on phones. */\n layout?: \"grid\" | \"row\";\n /** The choice is kept but has no visible effect right now; previews are dimmed, choosing still works. */\n dimmed?: boolean;\n onValueChange?: (value: T) => void;\n onValueCommit?: (value: T) => void;\n};\n\n/**\n * A labeled single choice shown as tiles with a preview, for picking how something looks: a face shape, a frame, a\n * kind of picture. Every tile keeps its size when chosen, so nothing moves. Use `ChoiceChips` for text choices and\n * `ColorChoices` for named colours.\n */\nexport function ChoiceTiles<T extends string = string>(props: ChoiceTilesProps<T>): JSX.Element {\n const meta = createFieldMeta(props.id);\n let group: HTMLDivElement | undefined;\n const value = () => resolveMaybeAccessor(props.value) ?? null;\n const tabStop = (option: ChoiceTileOption<T>, index: number) =>\n value() === option.value || (index === 0 && !props.options.some((candidate) => candidate.value === value()));\n const choose = (index: number, focus = false) => {\n const option = props.options[index];\n if (!option || props.disabled) return;\n props.onValueChange?.(option.value);\n props.onValueCommit?.(option.value);\n // The radios in their current DOM order: options may have been reordered since they rendered.\n if (focus) queueMicrotask(() => group?.querySelectorAll<HTMLButtonElement>('[role=\"radio\"]')[index]?.focus());\n };\n return (\n <Field\n meta={meta}\n labelFor={false}\n class={props.class}\n label={props.label}\n description={props.description}\n error={props.error}\n required={props.required}\n disabled={props.disabled}\n >\n <div\n class=\"k2b-choice-tiles__group\"\n ref={group}\n role=\"radiogroup\"\n data-layout={props.layout ?? \"grid\"}\n data-dimmed={props.dimmed ? \"true\" : undefined}\n aria-label={props.label ? undefined : props[\"aria-label\"]}\n aria-labelledby={props.label ? meta.labelId : undefined}\n aria-describedby={fieldDescribedBy(meta, props)}\n aria-disabled={props.disabled ? \"true\" : undefined}\n >\n <For each={props.options}>\n {(option, index) => (\n <button\n type=\"button\"\n role=\"radio\"\n class=\"k2b-choice-tiles__tile\"\n aria-checked={value() === option.value}\n disabled={props.disabled}\n tabIndex={tabStop(option, index()) ? 0 : -1}\n onClick={() => choose(index())}\n onKeyDown={(event) => {\n const target = radioKeyTarget(event.key, index(), props.options.length);\n if (target === undefined) return;\n event.preventDefault();\n choose(target, true);\n }}\n >\n <span class=\"k2b-choice-tiles__visual\" aria-hidden=\"true\">\n {option.visual}\n </span>\n <span class=\"k2b-choice-tiles__text\">\n <span class=\"k2b-choice-tiles__label\">{option.label}</span>\n {props.layout === \"row\" && <span class=\"k2b-choice-tiles__status\">{option.status ?? \"\"}</span>}\n </span>\n </button>\n )}\n </For>\n </div>\n </Field>\n );\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { template as _$template } from "solid-js/web";
|
|
2
|
+
import { delegateEvents as _$delegateEvents } from "solid-js/web";
|
|
3
|
+
import { setProperty as _$setProperty } from "solid-js/web";
|
|
4
|
+
import { setStyleProperty as _$setStyleProperty } from "solid-js/web";
|
|
5
|
+
import { runHydrationEvents as _$runHydrationEvents } from "solid-js/web";
|
|
6
|
+
import { setAttribute as _$setAttribute } from "solid-js/web";
|
|
7
|
+
import { effect as _$effect } from "solid-js/web";
|
|
8
|
+
import { getNextElement as _$getNextElement } from "solid-js/web";
|
|
9
|
+
import { insert as _$insert } from "solid-js/web";
|
|
10
|
+
import { createComponent as _$createComponent } from "solid-js/web";
|
|
11
|
+
import { use as _$use } from "solid-js/web";
|
|
12
|
+
var _tmpl$ = /*#__PURE__*/_$template(`<div class=k2b-color-choices__group role=radiogroup>`),
|
|
13
|
+
_tmpl$2 = /*#__PURE__*/_$template(`<button type=button role=radio class=k2b-color-choices__swatch><i class="ti ti-check"aria-hidden=true>`);
|
|
14
|
+
import { For } from "solid-js";
|
|
15
|
+
import { createFieldMeta, Field, fieldDescribedBy } from "../internal/field";
|
|
16
|
+
import { radioKeyTarget } from "../internal/radio-keys";
|
|
17
|
+
import { resolveMaybeAccessor } from "./field-contract";
|
|
18
|
+
/**
|
|
19
|
+
* A labeled single choice from a fixed palette of named colours, shown as round swatches. Use `ColorInput` when any
|
|
20
|
+
* colour is allowed.
|
|
21
|
+
*/
|
|
22
|
+
export function ColorChoices(props) {
|
|
23
|
+
const meta = createFieldMeta(props.id);
|
|
24
|
+
let group;
|
|
25
|
+
const value = () => resolveMaybeAccessor(props.value) ?? null;
|
|
26
|
+
const tabStop = (option, index) => value() === option.value || index === 0 && !props.options.some(candidate => candidate.value === value());
|
|
27
|
+
const choose = (index, focus = false) => {
|
|
28
|
+
const option = props.options[index];
|
|
29
|
+
if (!option || props.disabled) return;
|
|
30
|
+
props.onValueChange?.(option.value);
|
|
31
|
+
props.onValueCommit?.(option.value);
|
|
32
|
+
// The radios in their current DOM order: options may have been reordered since they rendered.
|
|
33
|
+
if (focus) queueMicrotask(() => group?.querySelectorAll('[role="radio"]')[index]?.focus());
|
|
34
|
+
};
|
|
35
|
+
return _$createComponent(Field, {
|
|
36
|
+
meta: meta,
|
|
37
|
+
labelFor: false,
|
|
38
|
+
get ["class"]() {
|
|
39
|
+
return props.class;
|
|
40
|
+
},
|
|
41
|
+
get label() {
|
|
42
|
+
return props.label;
|
|
43
|
+
},
|
|
44
|
+
get description() {
|
|
45
|
+
return props.description;
|
|
46
|
+
},
|
|
47
|
+
get error() {
|
|
48
|
+
return props.error;
|
|
49
|
+
},
|
|
50
|
+
get required() {
|
|
51
|
+
return props.required;
|
|
52
|
+
},
|
|
53
|
+
get disabled() {
|
|
54
|
+
return props.disabled;
|
|
55
|
+
},
|
|
56
|
+
get children() {
|
|
57
|
+
var _el$ = _$getNextElement(_tmpl$);
|
|
58
|
+
var _ref$ = group;
|
|
59
|
+
typeof _ref$ === "function" ? _$use(_ref$, _el$) : group = _el$;
|
|
60
|
+
_$insert(_el$, _$createComponent(For, {
|
|
61
|
+
get each() {
|
|
62
|
+
return props.options;
|
|
63
|
+
},
|
|
64
|
+
children: (option, index) => (() => {
|
|
65
|
+
var _el$2 = _$getNextElement(_tmpl$2);
|
|
66
|
+
_el$2.$$keydown = event => {
|
|
67
|
+
const target = radioKeyTarget(event.key, index(), props.options.length);
|
|
68
|
+
if (target === undefined) return;
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
choose(target, true);
|
|
71
|
+
};
|
|
72
|
+
_el$2.$$click = () => choose(index());
|
|
73
|
+
_$effect(_p$ => {
|
|
74
|
+
var _v$5 = option.color,
|
|
75
|
+
_v$6 = value() === option.value,
|
|
76
|
+
_v$7 = option.label,
|
|
77
|
+
_v$8 = option.label,
|
|
78
|
+
_v$9 = props.disabled,
|
|
79
|
+
_v$0 = tabStop(option, index()) ? 0 : -1;
|
|
80
|
+
_v$5 !== _p$.e && _$setStyleProperty(_el$2, "--k2b-color-choice", _p$.e = _v$5);
|
|
81
|
+
_v$6 !== _p$.t && _$setAttribute(_el$2, "aria-checked", _p$.t = _v$6);
|
|
82
|
+
_v$7 !== _p$.a && _$setAttribute(_el$2, "aria-label", _p$.a = _v$7);
|
|
83
|
+
_v$8 !== _p$.o && _$setAttribute(_el$2, "title", _p$.o = _v$8);
|
|
84
|
+
_v$9 !== _p$.i && _$setProperty(_el$2, "disabled", _p$.i = _v$9);
|
|
85
|
+
_v$0 !== _p$.n && _$setAttribute(_el$2, "tabindex", _p$.n = _v$0);
|
|
86
|
+
return _p$;
|
|
87
|
+
}, {
|
|
88
|
+
e: undefined,
|
|
89
|
+
t: undefined,
|
|
90
|
+
a: undefined,
|
|
91
|
+
o: undefined,
|
|
92
|
+
i: undefined,
|
|
93
|
+
n: undefined
|
|
94
|
+
});
|
|
95
|
+
_$runHydrationEvents();
|
|
96
|
+
return _el$2;
|
|
97
|
+
})()
|
|
98
|
+
}));
|
|
99
|
+
_$effect(_p$ => {
|
|
100
|
+
var _v$ = props.label ? undefined : props["aria-label"],
|
|
101
|
+
_v$2 = props.label ? meta.labelId : undefined,
|
|
102
|
+
_v$3 = fieldDescribedBy(meta, props),
|
|
103
|
+
_v$4 = props.disabled ? "true" : undefined;
|
|
104
|
+
_v$ !== _p$.e && _$setAttribute(_el$, "aria-label", _p$.e = _v$);
|
|
105
|
+
_v$2 !== _p$.t && _$setAttribute(_el$, "aria-labelledby", _p$.t = _v$2);
|
|
106
|
+
_v$3 !== _p$.a && _$setAttribute(_el$, "aria-describedby", _p$.a = _v$3);
|
|
107
|
+
_v$4 !== _p$.o && _$setAttribute(_el$, "aria-disabled", _p$.o = _v$4);
|
|
108
|
+
return _p$;
|
|
109
|
+
}, {
|
|
110
|
+
e: undefined,
|
|
111
|
+
t: undefined,
|
|
112
|
+
a: undefined,
|
|
113
|
+
o: undefined
|
|
114
|
+
});
|
|
115
|
+
return _el$;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
_$delegateEvents(["click", "keydown"]);
|
|
120
|
+
//# sourceMappingURL=ColorChoices.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sources":["inputs/ColorChoices.tsx"],"sourcesContent":["import { For, type JSX } from \"solid-js\";\nimport { createFieldMeta, Field, fieldDescribedBy } from \"../internal/field\";\nimport { radioKeyTarget } from \"../internal/radio-keys\";\nimport type { ValueFieldProps } from \"./field-contract\";\nimport { resolveMaybeAccessor } from \"./field-contract\";\n\nexport type ColorChoiceOption<T extends string = string> = {\n value: T;\n /** The colour's name; screen readers and the tooltip use it, so colour is never the only signal. */\n label: string;\n /** Any CSS colour. */\n color: string;\n};\n\nexport type ColorChoicesProps<T extends string = string> = Omit<ValueFieldProps<T | null>, \"onValueChange\" | \"onValueCommit\"> & {\n options: readonly ColorChoiceOption<T>[];\n onValueChange?: (value: T) => void;\n onValueCommit?: (value: T) => void;\n};\n\n/**\n * A labeled single choice from a fixed palette of named colours, shown as round swatches. Use `ColorInput` when any\n * colour is allowed.\n */\nexport function ColorChoices<T extends string = string>(props: ColorChoicesProps<T>): JSX.Element {\n const meta = createFieldMeta(props.id);\n let group: HTMLDivElement | undefined;\n const value = () => resolveMaybeAccessor(props.value) ?? null;\n const tabStop = (option: ColorChoiceOption<T>, index: number) =>\n value() === option.value || (index === 0 && !props.options.some((candidate) => candidate.value === value()));\n const choose = (index: number, focus = false) => {\n const option = props.options[index];\n if (!option || props.disabled) return;\n props.onValueChange?.(option.value);\n props.onValueCommit?.(option.value);\n // The radios in their current DOM order: options may have been reordered since they rendered.\n if (focus) queueMicrotask(() => group?.querySelectorAll<HTMLButtonElement>('[role=\"radio\"]')[index]?.focus());\n };\n return (\n <Field\n meta={meta}\n labelFor={false}\n class={props.class}\n label={props.label}\n description={props.description}\n error={props.error}\n required={props.required}\n disabled={props.disabled}\n >\n <div\n class=\"k2b-color-choices__group\"\n ref={group}\n role=\"radiogroup\"\n aria-label={props.label ? undefined : props[\"aria-label\"]}\n aria-labelledby={props.label ? meta.labelId : undefined}\n aria-describedby={fieldDescribedBy(meta, props)}\n aria-disabled={props.disabled ? \"true\" : undefined}\n >\n <For each={props.options}>\n {(option, index) => (\n <button\n type=\"button\"\n role=\"radio\"\n class=\"k2b-color-choices__swatch\"\n style={{ \"--k2b-color-choice\": option.color }}\n aria-checked={value() === option.value}\n aria-label={option.label}\n title={option.label}\n disabled={props.disabled}\n tabIndex={tabStop(option, index()) ? 0 : -1}\n onClick={() => choose(index())}\n onKeyDown={(event) => {\n const target = radioKeyTarget(event.key, index(), props.options.length);\n if (target === undefined) return;\n event.preventDefault();\n choose(target, true);\n }}\n >\n <i class=\"ti ti-check\" aria-hidden=\"true\" />\n </button>\n )}\n </For>\n </div>\n </Field>\n );\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -3,15 +3,17 @@ import { setAttribute as _$setAttribute } from "solid-js/web";
|
|
|
3
3
|
import { className as _$className } from "solid-js/web";
|
|
4
4
|
import { effect as _$effect } from "solid-js/web";
|
|
5
5
|
import { getNextMarker as _$getNextMarker } from "solid-js/web";
|
|
6
|
-
import { createComponent as _$createComponent } from "solid-js/web";
|
|
7
6
|
import { getNextElement as _$getNextElement } from "solid-js/web";
|
|
7
|
+
import { createComponent as _$createComponent } from "solid-js/web";
|
|
8
8
|
import { insert as _$insert } from "solid-js/web";
|
|
9
9
|
import { memo as _$memo } from "solid-js/web";
|
|
10
10
|
import { use as _$use } from "solid-js/web";
|
|
11
11
|
var _tmpl$ = /*#__PURE__*/_$template(`<p class=k2b-qr-scanner__note>`),
|
|
12
|
-
_tmpl$2 = /*#__PURE__*/_$template(`<div><div class=k2b-qr-scanner__preview><video muted playsinline></video></div><p role=status></p
|
|
12
|
+
_tmpl$2 = /*#__PURE__*/_$template(`<div><div class=k2b-qr-scanner__preview><video autoplay muted playsinline></video></div><p role=status></p><!$><!/><!$><!/>`);
|
|
13
13
|
import { timed } from "@k2b/stdlib/solid";
|
|
14
14
|
import { createSignal, onCleanup, onMount, Show } from "solid-js";
|
|
15
|
+
import { Button } from "../actions/Button";
|
|
16
|
+
import { useUiMessages } from "../intl/messages";
|
|
15
17
|
import { prefersReducedMotion } from "../motion";
|
|
16
18
|
import { useQrMessages } from "./qr-messages";
|
|
17
19
|
|
|
@@ -93,6 +95,12 @@ const openCamera = async closed => {
|
|
|
93
95
|
error
|
|
94
96
|
};
|
|
95
97
|
};
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* How long the camera may take to show its first picture. A working camera shows one well within a second; after three
|
|
101
|
+
* the person has looked at a black square long enough to need a way to start it again.
|
|
102
|
+
*/
|
|
103
|
+
const firstPictureMs = 3000;
|
|
96
104
|
const stopStream = stream => {
|
|
97
105
|
for (const track of stream.getTracks()) track.stop();
|
|
98
106
|
};
|
|
@@ -104,7 +112,9 @@ const stopStream = stream => {
|
|
|
104
112
|
*/
|
|
105
113
|
export function QrScanner(props) {
|
|
106
114
|
const qrMessages = useQrMessages();
|
|
115
|
+
const uiMessages = useUiMessages();
|
|
107
116
|
const [starting, setStarting] = createSignal(true);
|
|
117
|
+
const [stalled, setStalled] = createSignal(false);
|
|
108
118
|
const [invalid, setInvalid] = createSignal(false);
|
|
109
119
|
let lastRejected;
|
|
110
120
|
const resetRejected = timed.debounce(() => {
|
|
@@ -113,17 +123,21 @@ export function QrScanner(props) {
|
|
|
113
123
|
const resetFeedback = timed.debounce(() => setInvalid(false), 1500);
|
|
114
124
|
let video;
|
|
115
125
|
let scanner;
|
|
126
|
+
let stream;
|
|
116
127
|
let closed = false;
|
|
117
|
-
|
|
128
|
+
const detach = [];
|
|
118
129
|
const destroy = () => {
|
|
119
130
|
closed = true;
|
|
120
|
-
|
|
131
|
+
for (const listener of detach.splice(0)) listener();
|
|
121
132
|
resetRejected.cancel();
|
|
122
133
|
resetFeedback.cancel();
|
|
123
|
-
// pause(true) stops the camera stream immediately.
|
|
134
|
+
// pause(true) stops the camera stream immediately while the engine runs; a stream it never started stops here.
|
|
124
135
|
void scanner?.pause(true);
|
|
125
136
|
scanner?.destroy();
|
|
126
137
|
scanner = undefined;
|
|
138
|
+
if (stream) stopStream(stream);
|
|
139
|
+
stream = undefined;
|
|
140
|
+
video.srcObject = null;
|
|
127
141
|
};
|
|
128
142
|
// Once closed, the host has unmounted the scanner or heard why it stopped; later failures stay silent.
|
|
129
143
|
const fail = reason => {
|
|
@@ -131,6 +145,40 @@ export function QrScanner(props) {
|
|
|
131
145
|
destroy();
|
|
132
146
|
props.onError(reason);
|
|
133
147
|
};
|
|
148
|
+
/**
|
|
149
|
+
* Waits for the camera picture. WebKit, above all in an installed web app, can leave a stream black: play() never
|
|
150
|
+
* settles or is refused. The scanner then offers to try again rather than wait in silence, and a picture that starts
|
|
151
|
+
* late still clears that offer.
|
|
152
|
+
*/
|
|
153
|
+
let attempt = 0;
|
|
154
|
+
const show = async play => {
|
|
155
|
+
const current = ++attempt;
|
|
156
|
+
const latest = () => !closed && current === attempt;
|
|
157
|
+
setStalled(false);
|
|
158
|
+
const playing = play().then(() => video.videoWidth > 0, () => false);
|
|
159
|
+
let timer;
|
|
160
|
+
const shown = await Promise.race([playing, new Promise(resolve => {
|
|
161
|
+
timer = setTimeout(resolve, firstPictureMs, false);
|
|
162
|
+
})]);
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
if (!latest()) return;
|
|
165
|
+
if (!shown) {
|
|
166
|
+
setStalled(true);
|
|
167
|
+
if (!(await playing) || !latest()) return;
|
|
168
|
+
}
|
|
169
|
+
setStalled(false);
|
|
170
|
+
setStarting(false);
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Attaching the stream again restarts a player that stalled on it, and play() runs within the click, the gesture a
|
|
174
|
+
* refused play() may need. The engine already runs from its first start and scans from the play event it hears.
|
|
175
|
+
*/
|
|
176
|
+
const retry = () => {
|
|
177
|
+
if (closed || !stream) return;
|
|
178
|
+
video.srcObject = null;
|
|
179
|
+
video.srcObject = stream;
|
|
180
|
+
void show(() => video.play());
|
|
181
|
+
};
|
|
134
182
|
onMount(() => {
|
|
135
183
|
const hidden = () => {
|
|
136
184
|
if (document.visibilityState !== "visible") props.onStop();
|
|
@@ -188,15 +236,30 @@ export function QrScanner(props) {
|
|
|
188
236
|
};
|
|
189
237
|
applyMotion();
|
|
190
238
|
motion.addEventListener("change", applyMotion);
|
|
191
|
-
|
|
239
|
+
detach.push(() => motion.removeEventListener("change", applyMotion));
|
|
240
|
+
// The engine places its scan region and frame on loadedmetadata, play, and a window resize. It misses a picture
|
|
241
|
+
// that turns, as when a tablet rotates, and a preview that changes size alone; replay loadedmetadata for both,
|
|
242
|
+
// or the region stays where the old picture had it and the frame sits off-centre.
|
|
243
|
+
const relayout = () => {
|
|
244
|
+
if (video.videoWidth > 0) video.dispatchEvent(new Event("loadedmetadata"));
|
|
245
|
+
};
|
|
246
|
+
video.addEventListener("resize", relayout);
|
|
247
|
+
const resized = new ResizeObserver(relayout);
|
|
248
|
+
resized.observe(video);
|
|
249
|
+
detach.push(() => {
|
|
250
|
+
video.removeEventListener("resize", relayout);
|
|
251
|
+
resized.disconnect();
|
|
252
|
+
});
|
|
192
253
|
const camera = await openCamera(() => closed);
|
|
193
254
|
if ("error" in camera) return fail(camera.error);
|
|
194
255
|
if (closed) return stopStream(camera.stream);
|
|
195
256
|
// qr-scanner plays a stream that is already set instead of opening its own.
|
|
196
|
-
|
|
257
|
+
stream = camera.stream;
|
|
258
|
+
video.srcObject = stream;
|
|
197
259
|
video.style.transform = camera.mirrored ? "scaleX(-1)" : "";
|
|
198
|
-
|
|
199
|
-
|
|
260
|
+
// start() awaits the engine's play() of this stream.
|
|
261
|
+
const engine = scanner;
|
|
262
|
+
await show(() => engine.start());
|
|
200
263
|
} catch {
|
|
201
264
|
fail("unavailable");
|
|
202
265
|
}
|
|
@@ -208,13 +271,32 @@ export function QrScanner(props) {
|
|
|
208
271
|
_el$3 = _el$2.firstChild,
|
|
209
272
|
_el$4 = _el$2.nextSibling,
|
|
210
273
|
_el$6 = _el$4.nextSibling,
|
|
211
|
-
[_el$7, _co$] = _$getNextMarker(_el$6.nextSibling)
|
|
274
|
+
[_el$7, _co$] = _$getNextMarker(_el$6.nextSibling),
|
|
275
|
+
_el$8 = _el$7.nextSibling,
|
|
276
|
+
[_el$9, _co$2] = _$getNextMarker(_el$8.nextSibling);
|
|
212
277
|
var _ref$ = video;
|
|
213
278
|
typeof _ref$ === "function" ? _$use(_ref$, _el$3) : video = _el$3;
|
|
214
279
|
_$insert(_el$4, (() => {
|
|
215
|
-
var _c$ = _$memo(() => !!
|
|
216
|
-
return () => _c$() ? qrMessages().qrCameraStarting : props.instructions ?? qrMessages().qrCameraInstructions;
|
|
280
|
+
var _c$ = _$memo(() => !!stalled());
|
|
281
|
+
return () => _c$() ? qrMessages().qrCameraStalled : _$memo(() => !!starting())() ? qrMessages().qrCameraStarting : props.instructions ?? qrMessages().qrCameraInstructions;
|
|
217
282
|
})());
|
|
283
|
+
_$insert(_el$, _$createComponent(Show, {
|
|
284
|
+
get when() {
|
|
285
|
+
return stalled();
|
|
286
|
+
},
|
|
287
|
+
get children() {
|
|
288
|
+
return _$createComponent(Button, {
|
|
289
|
+
type: "button",
|
|
290
|
+
size: "sm",
|
|
291
|
+
variant: "secondary",
|
|
292
|
+
"class": "k2b-qr-scanner__retry",
|
|
293
|
+
onClick: retry,
|
|
294
|
+
get children() {
|
|
295
|
+
return uiMessages().tryAgain;
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}), _el$7, _co$);
|
|
218
300
|
_$insert(_el$, _$createComponent(Show, {
|
|
219
301
|
get when() {
|
|
220
302
|
return !starting();
|
|
@@ -224,7 +306,7 @@ export function QrScanner(props) {
|
|
|
224
306
|
_$insert(_el$5, () => qrMessages().qrCameraPrivacy);
|
|
225
307
|
return _el$5;
|
|
226
308
|
}
|
|
227
|
-
}), _el$
|
|
309
|
+
}), _el$9, _co$2);
|
|
228
310
|
_$effect(_p$ => {
|
|
229
311
|
var _v$ = props.class ? `k2b-qr-scanner ${props.class}` : "k2b-qr-scanner",
|
|
230
312
|
_v$2 = invalid() || undefined,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":[],"sources":["inputs/QrScanner.tsx"],"sourcesContent":["import { timed } from \"@k2b/stdlib/solid\";\nimport type QrScannerEngine from \"qr-scanner\";\nimport { createSignal, type JSX, onCleanup, onMount, Show } from \"solid-js\";\nimport { prefersReducedMotion } from \"../motion\";\nimport { useQrMessages } from \"./qr-messages\";\n\n/**\n * Why the scanner could not start, as far as the browser tells: `\"denied\"` when camera access is off, `\"no-camera\"`\n * when the device has no camera, `\"in-use\"` when the camera is busy or could not start, most often because another app\n * holds it, and `\"unavailable\"` for anything else, including a scanning engine that cannot load.\n */\nexport type QrScannerError = \"denied\" | \"no-camera\" | \"in-use\" | \"unavailable\";\n\nexport type QrScannerProps = {\n /** Decoded text is untrusted. Return `true` to accept it and stop the camera, `false` to reject it and keep scanning. */\n onResult: (text: string) => boolean;\n /** The page was hidden or left; unmount the scanner. */\n onStop: () => void;\n /**\n * The camera or the scanning engine could not start; the scanner has stopped. Offer another way in. A frame that\n * cannot be decoded never ends here.\n */\n onError: (reason: QrScannerError) => void;\n /** What to point the camera at. Defaults to a generic QR code hint. */\n instructions?: string;\n class?: string;\n};\n\nconst sizes: MediaTrackConstraints[] = [{ width: { min: 1024 } }, { width: { min: 768 } }, {}];\n/** The rear camera at a resolution that keeps small codes readable, then any camera: the order qr-scanner tries. */\nconst cameraRequests: MediaTrackConstraints[] = [...sizes.map((size) => ({ ...size, facingMode: { exact: \"environment\" } })), ...sizes];\n\nconst errorName = (error: unknown): string => (typeof error === \"object\" && error !== null && \"name\" in error ? String(error.name) : \"\");\n\nconst cameraFailure = (name: string): QrScannerError => {\n if (name === \"NotAllowedError\" || name === \"SecurityError\") return \"denied\";\n if (name === \"NotFoundError\" || name === \"OverconstrainedError\") return \"no-camera\";\n if (name === \"NotReadableError\") return \"in-use\";\n return \"unavailable\";\n};\n\n/** The facing a track reports, or else the one its label names, as qr-scanner reads it. */\nconst facingOf = (track: MediaStreamTrack | undefined): string | undefined => {\n if (!track) return undefined;\n const { facingMode } = track.getSettings();\n if (facingMode) return facingMode;\n if (/rear|back|environment/i.test(track.label)) return \"environment\";\n if (/front|user|face/i.test(track.label)) return \"user\";\n return undefined;\n};\n\n/**\n * Opens the camera here rather than in qr-scanner, which turns every failure into \"Camera not found.\". A refusal ends\n * the search, because asking again could show the permission prompt again. Any other failure may belong to the request,\n * such as a camera that cannot start at the larger size, so the next, looser request follows, as in qr-scanner. When\n * none succeeds, a camera that exists but fails outweighs a request no camera could meet. A closed scanner asks no more.\n */\nconst openCamera = async (closed: () => boolean): Promise<{ stream: MediaStream; mirrored: boolean } | { error: QrScannerError }> => {\n if (!navigator.mediaDevices) return { error: \"unavailable\" };\n let error: QrScannerError = \"no-camera\";\n for (const video of cameraRequests) {\n if (closed()) break;\n try {\n const stream = await navigator.mediaDevices.getUserMedia({ video, audio: false });\n const facing = facingOf(stream.getVideoTracks()[0]);\n // Like qr-scanner, mirror a front camera, and a camera of unknown facing that the rear request did not find.\n return { stream, mirrored: facing ? facing === \"user\" : !video.facingMode };\n } catch (failure) {\n const reason = cameraFailure(errorName(failure));\n if (reason === \"denied\") return { error: reason };\n if (reason !== \"no-camera\") error = reason;\n }\n }\n return { error };\n};\n\nconst stopStream = (stream: MediaStream) => {\n for (const track of stream.getTracks()) track.stop();\n};\n\n/**\n * Scans a QR code with the rear camera. Each mount owns exactly one camera session, which stops when the scanner\n * unmounts, the page is hidden, or a result is accepted. The scanning engine loads only when the scanner mounts.\n * Decoded content is never logged.\n */\nexport function QrScanner(props: QrScannerProps): JSX.Element {\n const qrMessages = useQrMessages();\n const [starting, setStarting] = createSignal(true);\n const [invalid, setInvalid] = createSignal(false);\n let lastRejected: string | undefined;\n const resetRejected = timed.debounce(() => {\n lastRejected = undefined;\n }, 1000);\n const resetFeedback = timed.debounce(() => setInvalid(false), 1500);\n let video!: HTMLVideoElement;\n let scanner: QrScannerEngine | undefined;\n let closed = false;\n let cleanupMotion: (() => void) | undefined;\n const destroy = () => {\n closed = true;\n cleanupMotion?.();\n resetRejected.cancel();\n resetFeedback.cancel();\n // pause(true) stops the camera stream immediately.\n void scanner?.pause(true);\n scanner?.destroy();\n scanner = undefined;\n };\n // Once closed, the host has unmounted the scanner or heard why it stopped; later failures stay silent.\n const fail = (reason: QrScannerError) => {\n if (closed) return;\n destroy();\n props.onError(reason);\n };\n onMount(() => {\n const hidden = () => {\n if (document.visibilityState !== \"visible\") props.onStop();\n };\n const leaving = () => props.onStop();\n document.addEventListener(\"visibilitychange\", hidden);\n window.addEventListener(\"pagehide\", leaving);\n onCleanup(() => {\n destroy();\n document.removeEventListener(\"visibilitychange\", hidden);\n window.removeEventListener(\"pagehide\", leaving);\n });\n void (async () => {\n try {\n const { default: Engine } = await import(\"qr-scanner\");\n if (closed || document.visibilityState !== \"visible\") return;\n scanner = new Engine(\n video,\n (result) => {\n if (closed) return;\n // Rearm only after a full second without a decoded QR, not between individual frames.\n resetRejected.debouncedFn();\n if (result.data === lastRejected) return;\n if (props.onResult(result.data)) {\n destroy();\n return;\n }\n lastRejected = result.data;\n setInvalid(true);\n resetFeedback.debouncedFn();\n },\n {\n highlightScanRegion: true,\n // A frame that cannot be decoded never ends the session: the camera runs, and the next frame may read.\n // An engine error other than an empty frame, which the native BarcodeDetector reports as \"Scanner error:\n // No QR code found\", means the detector cannot read on this device. Fall back to the bundled worker for\n // the page through the flag qr-scanner 1.4.2 sets itself for \"not implemented\" and \"service unavailable\";\n // it swaps engines after this frame. Worker errors carry the same prefix; the flag changes nothing there.\n // qr-scanner reports frames only as strings: an Error means the engine itself did not load, such as a\n // decoder chunk gone after a deploy or a worker that cannot start, and no later frame can read. Never log\n // decoded content or frame data.\n onDecodeError: (error) => {\n if (typeof error !== \"string\") return fail(\"unavailable\");\n if (error.startsWith(\"Scanner error: \") && !error.endsWith(Engine.NO_QR_CODE_FOUND)) {\n Reflect.set(Engine, \"_disableBarcodeDetector\", true);\n }\n },\n },\n );\n const motion = matchMedia(\"(prefers-reduced-motion: reduce)\");\n const applyMotion = () => {\n for (const animation of scanner?.$overlay?.getAnimations({ subtree: true }) ?? []) {\n if (prefersReducedMotion()) animation.pause();\n else animation.play();\n }\n };\n applyMotion();\n motion.addEventListener(\"change\", applyMotion);\n cleanupMotion = () => motion.removeEventListener(\"change\", applyMotion);\n const camera = await openCamera(() => closed);\n if (\"error\" in camera) return fail(camera.error);\n if (closed) return stopStream(camera.stream);\n // qr-scanner plays a stream that is already set instead of opening its own.\n video.srcObject = camera.stream;\n video.style.transform = camera.mirrored ? \"scaleX(-1)\" : \"\";\n await scanner.start();\n if (!closed) setStarting(false);\n } catch {\n fail(\"unavailable\");\n }\n })();\n });\n return (\n <div class={props.class ? `k2b-qr-scanner ${props.class}` : \"k2b-qr-scanner\"}>\n <div class=\"k2b-qr-scanner__preview\" data-invalid={invalid() || undefined}>\n {/* Live camera preview contains no prerecorded speech or audio. */}\n <video ref={video} muted playsinline aria-label={qrMessages().qrCameraPreview} />\n </div>\n <p role=\"status\">{starting() ? qrMessages().qrCameraStarting : (props.instructions ?? qrMessages().qrCameraInstructions)}</p>\n <Show when={!starting()}>\n <p class=\"k2b-qr-scanner__note\">{qrMessages().qrCameraPrivacy}</p>\n </Show>\n </div>\n );\n}\n\nexport default QrScanner;\n"],"mappings":";;;;;;;;;;;;AAAA,SAAS,KAAK,QAAQ,mBAAmB;AAEzC,SAAS,YAAY,EAAY,SAAS,EAAE,OAAO,EAAE,IAAI,QAAQ,UAAU;AAC3E,SAAS,oBAAoB,QAAQ,WAAW;AAChD,SAAS,aAAa,QAAQ,eAAe;;AAE7C;AACA;AACA;AACA;AACA;;AAkBA,MAAM,KAA8B,GAAG,CAAC;EAAE,KAAK,EAAE;IAAE,GAAG,EAAE;EAAK;AAAE,CAAC,EAAE;EAAE,KAAK,EAAE;IAAE,GAAG,EAAE;EAAI;AAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAC9F;AACA,MAAM,cAAuC,GAAG,CAAC,GAAG,KAAK,CAAC,GAAG,CAAE,IAAI,KAAM;EAAE,GAAG,IAAI;EAAE,UAAU,EAAE;IAAE,KAAK,EAAE;EAAc;AAAE,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,CAAC;AAEvI,MAAM,SAAS,GAAI,KAAc,IAAc,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,MAAM,IAAI,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAG;AAExI,MAAM,aAAa,GAAI,IAAY,IAAqB;EACtD,IAAI,IAAI,KAAK,iBAAiB,IAAI,IAAI,KAAK,eAAe,EAAE,OAAO,QAAQ;EAC3E,IAAI,IAAI,KAAK,eAAe,IAAI,IAAI,KAAK,sBAAsB,EAAE,OAAO,WAAW;EACnF,IAAI,IAAI,KAAK,kBAAkB,EAAE,OAAO,QAAQ;EAChD,OAAO,aAAa;AACtB,CAAC;;AAED;AACA,MAAM,QAAQ,GAAI,KAAmC,IAAyB;EAC5E,IAAI,CAAC,KAAK,EAAE,OAAO,SAAS;EAC5B,MAAM;IAAE;EAAW,CAAC,GAAG,KAAK,CAAC,WAAW,CAAC,CAAC;EAC1C,IAAI,UAAU,EAAE,OAAO,UAAU;EACjC,IAAI,wBAAwB,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,OAAO,aAAa;EACpE,IAAI,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,OAAO,MAAM;EACvD,OAAO,SAAS;AAClB,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA;AACA,MAAM,UAAU,GAAG,MAAO,MAAqB,IAAsF;EACnI,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,OAAO;IAAE,KAAK,EAAE;EAAc,CAAC;EAC5D,IAAI,KAAqB,GAAG,WAAW;EACvC,KAAK,MAAM,KAAK,IAAI,cAAc,EAAE;IAClC,IAAI,MAAM,CAAC,CAAC,EAAE;IACd,IAAI;MACF,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,YAAY,CAAC,YAAY,CAAC;QAAE,KAAK;QAAE,KAAK,EAAE;MAAM,CAAC,CAAC;MACjF,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;MACnD;MACA,OAAO;QAAE,MAAM;QAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,KAAK,MAAM,GAAG,CAAC,KAAK,CAAC;MAAW,CAAC;IAC7E,CAAC,CAAC,OAAO,OAAO,EAAE;MAChB,MAAM,MAAM,GAAG,aAAa,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;MAChD,IAAI,MAAM,KAAK,QAAQ,EAAE,OAAO;QAAE,KAAK,EAAE;MAAO,CAAC;MACjD,IAAI,MAAM,KAAK,WAAW,EAAE,KAAK,GAAG,MAAM;IAC5C;EACF;EACA,OAAO;IAAE;EAAM,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,GAAI,MAAmB,IAAK;EAC1C,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,SAAS,CAAC,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;AACtD,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA,OAAO,SAAS,SAAS,CAAC,KAAqB,EAAe;EAC5D,MAAM,UAAU,GAAG,aAAa,CAAC,CAAC;EAClC,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,YAAY,CAAC,IAAI,CAAC;EAClD,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,YAAY,CAAC,KAAK,CAAC;EACjD,IAAI,YAAgC;EACpC,MAAM,aAAa,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM;IACzC,YAAY,GAAG,SAAS;EAC1B,CAAC,EAAE,IAAI,CAAC;EACR,MAAM,aAAa,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,UAAU,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC;EACnE,IAAI,KAAwB;EAC5B,IAAI,OAAoC;EACxC,IAAI,MAAM,GAAG,KAAK;EAClB,IAAI,aAAuC;EAC3C,MAAM,OAAO,GAAG,MAAM;IACpB,MAAM,GAAG,IAAI;IACb,aAAa,GAAG,CAAC;IACjB,aAAa,CAAC,MAAM,CAAC,CAAC;IACtB,aAAa,CAAC,MAAM,CAAC,CAAC;IACtB;IACA,KAAK,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC,CAAC;IAClB,OAAO,GAAG,SAAS;EACrB,CAAC;EACD;EACA,MAAM,IAAI,GAAI,MAAsB,IAAK;IACvC,IAAI,MAAM,EAAE;IACZ,OAAO,CAAC,CAAC;IACT,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;EACvB,CAAC;EACD,OAAO,CAAC,MAAM;IACZ,MAAM,MAAM,GAAG,MAAM;MACnB,IAAI,QAAQ,CAAC,eAAe,KAAK,SAAS,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC5D,CAAC;IACD,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,CAAC;IACpC,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACrD,MAAM,CAAC,gBAAgB,CAAC,UAAU,EAAE,OAAO,CAAC;IAC5C,SAAS,CAAC,MAAM;MACd,OAAO,CAAC,CAAC;MACT,QAAQ,CAAC,mBAAmB,CAAC,kBAAkB,EAAE,MAAM,CAAC;MACxD,MAAM,CAAC,mBAAmB,CAAC,UAAU,EAAE,OAAO,CAAC;IACjD,CAAC,CAAC;IACF,KAAK,CAAC,YAAY;MAChB,IAAI;QACF,MAAM;UAAE,OAAO,EAAE;QAAO,CAAC,GAAG,MAAM,OAAO,YAAY,CAAC;QACtD,IAAI,MAAM,IAAI,QAAQ,CAAC,eAAe,KAAK,SAAS,EAAE;QACtD,OAAO,GAAG,IAAI,MAAM,CAClB,KAAK,EACJ,MAAM,IAAK;UACV,IAAI,MAAM,EAAE;UACZ;UACA,aAAa,CAAC,WAAW,CAAC,CAAC;UAC3B,IAAI,MAAM,CAAC,IAAI,KAAK,YAAY,EAAE;UAClC,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE;YAC/B,OAAO,CAAC,CAAC;YACT;UACF;UACA,YAAY,GAAG,MAAM,CAAC,IAAI;UAC1B,UAAU,CAAC,IAAI,CAAC;UAChB,aAAa,CAAC,WAAW,CAAC,CAAC;QAC7B,CAAC,EACD;UACE,mBAAmB,EAAE,IAAI;UACzB;UACA;UACA;UACA;UACA;UACA;UACA;UACA;UACA,aAAa,EAAG,KAAK,IAAK;YACxB,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,OAAO,IAAI,CAAC,aAAa,CAAC;YACzD,IAAI,KAAK,CAAC,UAAU,CAAC,iBAAiB,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,gBAAgB,CAAC,EAAE;cACnF,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,yBAAyB,EAAE,IAAI,CAAC;YACtD;UACF;QACF,CACF,CAAC;QACD,MAAM,MAAM,GAAG,UAAU,CAAC,kCAAkC,CAAC;QAC7D,MAAM,WAAW,GAAG,MAAM;UACxB,KAAK,MAAM,SAAS,IAAI,OAAO,EAAE,QAAQ,EAAE,aAAa,CAAC;YAAE,OAAO,EAAE;UAAK,CAAC,CAAC,IAAI,EAAE,EAAE;YACjF,IAAI,oBAAoB,CAAC,CAAC,EAAE,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,KACzC,SAAS,CAAC,IAAI,CAAC,CAAC;UACvB;QACF,CAAC;QACD,WAAW,CAAC,CAAC;QACb,MAAM,CAAC,gBAAgB,CAAC,QAAQ,EAAE,WAAW,CAAC;QAC9C,aAAa,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,QAAQ,EAAE,WAAW,CAAC;QACvE,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,MAAM,MAAM,CAAC;QAC7C,IAAI,OAAO,IAAI,MAAM,EAAE,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC;QAChD,IAAI,MAAM,EAAE,OAAO,UAAU,CAAC,MAAM,CAAC,MAAM,CAAC;QAC5C;QACA,KAAK,CAAC,SAAS,GAAG,MAAM,CAAC,MAAM;QAC/B,KAAK,CAAC,KAAK,CAAC,SAAS,GAAG,MAAM,CAAC,QAAQ,GAAG,YAAY,GAAG,EAAE;QAC3D,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC;QACrB,IAAI,CAAC,MAAM,EAAE,WAAW,CAAC,KAAK,CAAC;MACjC,CAAC,CAAC,MAAM;QACN,IAAI,CAAC,aAAa,CAAC;MACrB;IACF,CAAC,EAAE,CAAC;EACN,CAAC,CAAC;EACF;IAAA;MAAA;MAAA;MAAA;MAAA;MAAA;IAAA,YAIkB,KAAK;IAAA,oDAAL,KAAK;IAAA;MAAA,yBAED,QAAQ,CAAC,CAAC;MAAA,aAAV,QAAa,UAAU,CAAC,CAAC,CAAC,gBAAgB,GAAI,KAAK,CAAC,YAAY,IAAI,UAAU,CAAC,CAAC,CAAC,oBAAqB;IAAA;IAAA,iCACvH,IAAI;MAAA,IAAC,IAAI;QAAA,OAAE,CAAC,QAAQ,CAAC,CAAC;MAAA;MAAA;QAAA;QAAA,sBACY,UAAU,CAAC,CAAC,CAAC,eAAe;QAAA;MAAA;IAAA;IAAA;MAAA,UAPrD,KAAK,CAAC,KAAK,GAAG,kBAAkB,KAAK,CAAC,KAAK,EAAE,GAAG,gBAAgB;QAAA,OACvB,OAAO,CAAC,CAAC,IAAI,SAAS;QAAA,OAEtB,UAAU,CAAC,CAAC,CAAC,eAAe;MAAA;MAAA;MAAA;MAAA;IAAA;MAAA;MAAA;MAAA;IAAA;IAAA;EAAA;AAQrF;AAEA,eAAe,SAAS","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":[],"sources":["inputs/QrScanner.tsx"],"sourcesContent":["import { timed } from \"@k2b/stdlib/solid\";\nimport type QrScannerEngine from \"qr-scanner\";\nimport { createSignal, type JSX, onCleanup, onMount, Show } from \"solid-js\";\nimport { Button } from \"../actions/Button\";\nimport { useUiMessages } from \"../intl/messages\";\nimport { prefersReducedMotion } from \"../motion\";\nimport { useQrMessages } from \"./qr-messages\";\n\n/**\n * Why the scanner could not start, as far as the browser tells: `\"denied\"` when camera access is off, `\"no-camera\"`\n * when the device has no camera, `\"in-use\"` when the camera is busy or could not start, most often because another app\n * holds it, and `\"unavailable\"` for anything else, including a scanning engine that cannot load.\n */\nexport type QrScannerError = \"denied\" | \"no-camera\" | \"in-use\" | \"unavailable\";\n\nexport type QrScannerProps = {\n /** Decoded text is untrusted. Return `true` to accept it and stop the camera, `false` to reject it and keep scanning. */\n onResult: (text: string) => boolean;\n /** The page was hidden or left; unmount the scanner. */\n onStop: () => void;\n /**\n * The camera or the scanning engine could not start; the scanner has stopped. Offer another way in. A frame that\n * cannot be decoded never ends here.\n */\n onError: (reason: QrScannerError) => void;\n /** What to point the camera at. Defaults to a generic QR code hint. */\n instructions?: string;\n class?: string;\n};\n\nconst sizes: MediaTrackConstraints[] = [{ width: { min: 1024 } }, { width: { min: 768 } }, {}];\n/** The rear camera at a resolution that keeps small codes readable, then any camera: the order qr-scanner tries. */\nconst cameraRequests: MediaTrackConstraints[] = [...sizes.map((size) => ({ ...size, facingMode: { exact: \"environment\" } })), ...sizes];\n\nconst errorName = (error: unknown): string => (typeof error === \"object\" && error !== null && \"name\" in error ? String(error.name) : \"\");\n\nconst cameraFailure = (name: string): QrScannerError => {\n if (name === \"NotAllowedError\" || name === \"SecurityError\") return \"denied\";\n if (name === \"NotFoundError\" || name === \"OverconstrainedError\") return \"no-camera\";\n if (name === \"NotReadableError\") return \"in-use\";\n return \"unavailable\";\n};\n\n/** The facing a track reports, or else the one its label names, as qr-scanner reads it. */\nconst facingOf = (track: MediaStreamTrack | undefined): string | undefined => {\n if (!track) return undefined;\n const { facingMode } = track.getSettings();\n if (facingMode) return facingMode;\n if (/rear|back|environment/i.test(track.label)) return \"environment\";\n if (/front|user|face/i.test(track.label)) return \"user\";\n return undefined;\n};\n\n/**\n * Opens the camera here rather than in qr-scanner, which turns every failure into \"Camera not found.\". A refusal ends\n * the search, because asking again could show the permission prompt again. Any other failure may belong to the request,\n * such as a camera that cannot start at the larger size, so the next, looser request follows, as in qr-scanner. When\n * none succeeds, a camera that exists but fails outweighs a request no camera could meet. A closed scanner asks no more.\n */\nconst openCamera = async (closed: () => boolean): Promise<{ stream: MediaStream; mirrored: boolean } | { error: QrScannerError }> => {\n if (!navigator.mediaDevices) return { error: \"unavailable\" };\n let error: QrScannerError = \"no-camera\";\n for (const video of cameraRequests) {\n if (closed()) break;\n try {\n const stream = await navigator.mediaDevices.getUserMedia({ video, audio: false });\n const facing = facingOf(stream.getVideoTracks()[0]);\n // Like qr-scanner, mirror a front camera, and a camera of unknown facing that the rear request did not find.\n return { stream, mirrored: facing ? facing === \"user\" : !video.facingMode };\n } catch (failure) {\n const reason = cameraFailure(errorName(failure));\n if (reason === \"denied\") return { error: reason };\n if (reason !== \"no-camera\") error = reason;\n }\n }\n return { error };\n};\n\n/**\n * How long the camera may take to show its first picture. A working camera shows one well within a second; after three\n * the person has looked at a black square long enough to need a way to start it again.\n */\nconst firstPictureMs = 3000;\n\nconst stopStream = (stream: MediaStream) => {\n for (const track of stream.getTracks()) track.stop();\n};\n\n/**\n * Scans a QR code with the rear camera. Each mount owns exactly one camera session, which stops when the scanner\n * unmounts, the page is hidden, or a result is accepted. The scanning engine loads only when the scanner mounts.\n * Decoded content is never logged.\n */\nexport function QrScanner(props: QrScannerProps): JSX.Element {\n const qrMessages = useQrMessages();\n const uiMessages = useUiMessages();\n const [starting, setStarting] = createSignal(true);\n const [stalled, setStalled] = createSignal(false);\n const [invalid, setInvalid] = createSignal(false);\n let lastRejected: string | undefined;\n const resetRejected = timed.debounce(() => {\n lastRejected = undefined;\n }, 1000);\n const resetFeedback = timed.debounce(() => setInvalid(false), 1500);\n let video!: HTMLVideoElement;\n let scanner: QrScannerEngine | undefined;\n let stream: MediaStream | undefined;\n let closed = false;\n const detach: (() => void)[] = [];\n const destroy = () => {\n closed = true;\n for (const listener of detach.splice(0)) listener();\n resetRejected.cancel();\n resetFeedback.cancel();\n // pause(true) stops the camera stream immediately while the engine runs; a stream it never started stops here.\n void scanner?.pause(true);\n scanner?.destroy();\n scanner = undefined;\n if (stream) stopStream(stream);\n stream = undefined;\n video.srcObject = null;\n };\n // Once closed, the host has unmounted the scanner or heard why it stopped; later failures stay silent.\n const fail = (reason: QrScannerError) => {\n if (closed) return;\n destroy();\n props.onError(reason);\n };\n /**\n * Waits for the camera picture. WebKit, above all in an installed web app, can leave a stream black: play() never\n * settles or is refused. The scanner then offers to try again rather than wait in silence, and a picture that starts\n * late still clears that offer.\n */\n let attempt = 0;\n const show = async (play: () => Promise<void>) => {\n const current = ++attempt;\n const latest = () => !closed && current === attempt;\n setStalled(false);\n const playing = play().then(\n () => video.videoWidth > 0,\n () => false,\n );\n let timer: ReturnType<typeof setTimeout> | undefined;\n const shown = await Promise.race([\n playing,\n new Promise<false>((resolve) => {\n timer = setTimeout(resolve, firstPictureMs, false);\n }),\n ]);\n clearTimeout(timer);\n if (!latest()) return;\n if (!shown) {\n setStalled(true);\n if (!(await playing) || !latest()) return;\n }\n setStalled(false);\n setStarting(false);\n };\n /**\n * Attaching the stream again restarts a player that stalled on it, and play() runs within the click, the gesture a\n * refused play() may need. The engine already runs from its first start and scans from the play event it hears.\n */\n const retry = () => {\n if (closed || !stream) return;\n video.srcObject = null;\n video.srcObject = stream;\n void show(() => video.play());\n };\n onMount(() => {\n const hidden = () => {\n if (document.visibilityState !== \"visible\") props.onStop();\n };\n const leaving = () => props.onStop();\n document.addEventListener(\"visibilitychange\", hidden);\n window.addEventListener(\"pagehide\", leaving);\n onCleanup(() => {\n destroy();\n document.removeEventListener(\"visibilitychange\", hidden);\n window.removeEventListener(\"pagehide\", leaving);\n });\n void (async () => {\n try {\n const { default: Engine } = await import(\"qr-scanner\");\n if (closed || document.visibilityState !== \"visible\") return;\n scanner = new Engine(\n video,\n (result) => {\n if (closed) return;\n // Rearm only after a full second without a decoded QR, not between individual frames.\n resetRejected.debouncedFn();\n if (result.data === lastRejected) return;\n if (props.onResult(result.data)) {\n destroy();\n return;\n }\n lastRejected = result.data;\n setInvalid(true);\n resetFeedback.debouncedFn();\n },\n {\n highlightScanRegion: true,\n // A frame that cannot be decoded never ends the session: the camera runs, and the next frame may read.\n // An engine error other than an empty frame, which the native BarcodeDetector reports as \"Scanner error:\n // No QR code found\", means the detector cannot read on this device. Fall back to the bundled worker for\n // the page through the flag qr-scanner 1.4.2 sets itself for \"not implemented\" and \"service unavailable\";\n // it swaps engines after this frame. Worker errors carry the same prefix; the flag changes nothing there.\n // qr-scanner reports frames only as strings: an Error means the engine itself did not load, such as a\n // decoder chunk gone after a deploy or a worker that cannot start, and no later frame can read. Never log\n // decoded content or frame data.\n onDecodeError: (error) => {\n if (typeof error !== \"string\") return fail(\"unavailable\");\n if (error.startsWith(\"Scanner error: \") && !error.endsWith(Engine.NO_QR_CODE_FOUND)) {\n Reflect.set(Engine, \"_disableBarcodeDetector\", true);\n }\n },\n },\n );\n const motion = matchMedia(\"(prefers-reduced-motion: reduce)\");\n const applyMotion = () => {\n for (const animation of scanner?.$overlay?.getAnimations({ subtree: true }) ?? []) {\n if (prefersReducedMotion()) animation.pause();\n else animation.play();\n }\n };\n applyMotion();\n motion.addEventListener(\"change\", applyMotion);\n detach.push(() => motion.removeEventListener(\"change\", applyMotion));\n // The engine places its scan region and frame on loadedmetadata, play, and a window resize. It misses a picture\n // that turns, as when a tablet rotates, and a preview that changes size alone; replay loadedmetadata for both,\n // or the region stays where the old picture had it and the frame sits off-centre.\n const relayout = () => {\n if (video.videoWidth > 0) video.dispatchEvent(new Event(\"loadedmetadata\"));\n };\n video.addEventListener(\"resize\", relayout);\n const resized = new ResizeObserver(relayout);\n resized.observe(video);\n detach.push(() => {\n video.removeEventListener(\"resize\", relayout);\n resized.disconnect();\n });\n const camera = await openCamera(() => closed);\n if (\"error\" in camera) return fail(camera.error);\n if (closed) return stopStream(camera.stream);\n // qr-scanner plays a stream that is already set instead of opening its own.\n stream = camera.stream;\n video.srcObject = stream;\n video.style.transform = camera.mirrored ? \"scaleX(-1)\" : \"\";\n // start() awaits the engine's play() of this stream.\n const engine = scanner;\n await show(() => engine.start());\n } catch {\n fail(\"unavailable\");\n }\n })();\n });\n return (\n <div class={props.class ? `k2b-qr-scanner ${props.class}` : \"k2b-qr-scanner\"}>\n <div class=\"k2b-qr-scanner__preview\" data-invalid={invalid() || undefined}>\n {/* Live camera preview contains no prerecorded speech or audio. */}\n <video ref={video} autoplay muted playsinline aria-label={qrMessages().qrCameraPreview} />\n </div>\n <p role=\"status\">\n {stalled()\n ? qrMessages().qrCameraStalled\n : starting()\n ? qrMessages().qrCameraStarting\n : (props.instructions ?? qrMessages().qrCameraInstructions)}\n </p>\n <Show when={stalled()}>\n <Button type=\"button\" size=\"sm\" variant=\"secondary\" class=\"k2b-qr-scanner__retry\" onClick={retry}>\n {uiMessages().tryAgain}\n </Button>\n </Show>\n <Show when={!starting()}>\n <p class=\"k2b-qr-scanner__note\">{qrMessages().qrCameraPrivacy}</p>\n </Show>\n </div>\n );\n}\n\nexport default QrScanner;\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -30,7 +30,7 @@ import { MarkdownEditor } from "./markdown/MarkdownEditor";
|
|
|
30
30
|
export function TextInput(props) {
|
|
31
31
|
const messages = useUiMessages();
|
|
32
32
|
const inputMessages = useInputMessages();
|
|
33
|
-
const [local, rest] = splitProps(props, ["activeIcon", "aria-describedby", "aria-label", "class", "clearLabel", "clearable", "completions", "description", "error", "externalErrorId", "icon", "id", "label", "lines", "markdown", "minLength", "maxLength", "monospace", "multiline", "onClear", "onValueCommit", "onSubmit", "onValueChange", "password", "prefix", "required", "suffix", "type", "value", "variant", "abbreviations"]);
|
|
33
|
+
const [local, rest] = splitProps(props, ["activeIcon", "aria-describedby", "aria-label", "class", "clearLabel", "clearable", "completions", "description", "error", "externalErrorId", "icon", "id", "label", "lines", "markdown", "minLength", "maxLength", "onKeyDown", "monospace", "multiline", "onClear", "onValueCommit", "onSubmit", "onValueChange", "password", "prefix", "required", "suffix", "type", "value", "variant", "abbreviations"]);
|
|
34
34
|
const meta = createFieldMeta(local.id);
|
|
35
35
|
const value = () => resolveMaybeAccessor(local.value) ?? "";
|
|
36
36
|
const error = () => resolveMaybeAccessor(local.error);
|
|
@@ -55,7 +55,22 @@ export function TextInput(props) {
|
|
|
55
55
|
const verbatim = () => local.password || local.type === "email" || local.type === "url" || /\b(username|email|url)\b/.test(String(rest.autocomplete ?? ""));
|
|
56
56
|
const input = next => local.onValueChange?.(next);
|
|
57
57
|
const commit = next => local.onValueCommit?.(next);
|
|
58
|
-
const
|
|
58
|
+
const submitOnEnter = event => {
|
|
59
|
+
if (local.onSubmit && event.key === "Enter" && !event.shiftKey && !event.metaKey) {
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
local.onSubmit();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
/** The caller's key handler runs first; Enter submits unless it handled the key. */
|
|
65
|
+
const keyDown = event => {
|
|
66
|
+
const handler = local.onKeyDown;
|
|
67
|
+
if (typeof handler === "function") handler(event);else if (Array.isArray(handler)) handler[0](handler[1], event);
|
|
68
|
+
if (!event.defaultPrevented) submitOnEnter(event);
|
|
69
|
+
};
|
|
70
|
+
const clear = event => {
|
|
71
|
+
// Clearing removes the button. Return to the field before notifying the caller, whose handler
|
|
72
|
+
// may deliberately navigate or move focus somewhere else.
|
|
73
|
+
event.currentTarget.ownerDocument.getElementById(meta.controlId)?.focus();
|
|
59
74
|
if (local.onClear) {
|
|
60
75
|
local.onClear();
|
|
61
76
|
return;
|
|
@@ -177,12 +192,7 @@ export function TextInput(props) {
|
|
|
177
192
|
input(event.currentTarget.value);
|
|
178
193
|
},
|
|
179
194
|
"onChange": event => commit(event.currentTarget.value),
|
|
180
|
-
"onKeyDown":
|
|
181
|
-
if (local.onSubmit && event.key === "Enter" && !event.shiftKey && !event.metaKey) {
|
|
182
|
-
event.preventDefault();
|
|
183
|
-
local.onSubmit();
|
|
184
|
-
}
|
|
185
|
-
}
|
|
195
|
+
"onKeyDown": keyDown
|
|
186
196
|
}), false, false);
|
|
187
197
|
_$runHydrationEvents();
|
|
188
198
|
return _el$21;
|
|
@@ -215,12 +225,7 @@ export function TextInput(props) {
|
|
|
215
225
|
input(event.currentTarget.value);
|
|
216
226
|
},
|
|
217
227
|
"onChange": event => commit(event.currentTarget.value),
|
|
218
|
-
"onKeyDown":
|
|
219
|
-
if (local.onSubmit && event.key === "Enter" && !event.shiftKey && !event.metaKey) {
|
|
220
|
-
event.preventDefault();
|
|
221
|
-
local.onSubmit();
|
|
222
|
-
}
|
|
223
|
-
}
|
|
228
|
+
"onKeyDown": submitOnEnter
|
|
224
229
|
}), false, true);
|
|
225
230
|
_$insert(_el$6, () => isServer ? `\n${value()}` : undefined);
|
|
226
231
|
_$runHydrationEvents();
|