@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 @@
|
|
|
1
|
+
{"version":3,"names":[],"sources":["inputs/ChoiceList.tsx"],"sourcesContent":["import { Index, type JSX, Show } 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 ChoiceListOption<T extends string = string> = {\n value: T;\n label: string;\n /** One sentence on what the choice means. */\n description?: string;\n /** A quiet tag after the label, such as \"Default\" for the choice that applies without one. */\n marker?: string;\n};\n\nexport type ChoiceListProps<T extends string = string> = Omit<ValueFieldProps<T | null>, \"onValueChange\" | \"onValueCommit\"> & {\n options: readonly ChoiceListOption<T>[];\n /** The chosen value is being saved: a spinner beside its label, and the group is busy. Choosing again is still possible. */\n busy?: boolean;\n /** Busy spinner's name for screen readers, such as \"Saving\". */\n busyLabel?: string;\n /** Follow-up controls for the chosen option, shown under it and indented to its text, such as a duration. */\n details?: JSX.Element;\n onValueChange?: (value: T) => void;\n onValueCommit?: (value: T) => void;\n};\n\n/**\n * A labeled single choice shown as a short list of rows, each with a radio, a label and one sentence on what it\n * means, for a few choices whose consequences need words. Choosing changes only paint, so no row moves; `details`\n * opens under the chosen row. Use `ChoiceChips` for quick picks without explanation, `RichSelect` for longer lists\n * in a dropdown, and `ChoiceTiles` when a preview decides.\n */\nexport function ChoiceList<T extends string = string>(props: ChoiceListProps<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: ChoiceListOption<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 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-list\"\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 aria-busy={props.busy ? \"true\" : undefined}\n >\n {/* Rows stay by position, so options rebuilt with new objects keep their buttons and the focus on them. */}\n <Index each={props.options}>\n {(option, index) => {\n const checked = () => value() === option().value;\n const descriptionId = `${meta.controlId}-option-${index}`;\n return (\n <>\n <button\n type=\"button\"\n role=\"radio\"\n class=\"k2b-choice-list__option\"\n aria-checked={checked()}\n aria-describedby={option().description ? descriptionId : undefined}\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-list__radio\" aria-hidden=\"true\" />\n <span class=\"k2b-choice-list__text\">\n <span class=\"k2b-choice-list__label\">\n {option().label}\n <Show when={option().marker}>{(marker) => <span class=\"k2b-choice-list__marker\">{marker()}</span>}</Show>\n {/* The spinner's place is always there, so a save starting or ending moves nothing. */}\n <span class=\"k2b-choice-list__busy\">\n <Show when={checked() && props.busy}>\n <i class=\"ti ti-loader-2 k2b-spin\" role=\"img\" aria-label={props.busyLabel} />\n </Show>\n </span>\n </span>\n <Show when={option().description}>\n <span class=\"k2b-choice-list__description\" id={descriptionId}>\n {option().description}\n </span>\n </Show>\n </span>\n </button>\n <Show when={checked() && props.details}>\n <div class=\"k2b-choice-list__details\">{props.details}</div>\n </Show>\n </>\n );\n }}\n </Index>\n </div>\n </Field>\n );\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { ssrStyleProperty as _$ssrStyleProperty } from "solid-js/web";
|
|
2
|
+
import { createComponent as _$createComponent } from "solid-js/web";
|
|
3
|
+
import { ssr as _$ssr } from "solid-js/web";
|
|
4
|
+
import { ssrAttribute as _$ssrAttribute } from "solid-js/web";
|
|
5
|
+
import { escape as _$escape } from "solid-js/web";
|
|
6
|
+
var _tmpl$ = "<i class=\"ti ti-alert-triangle\" aria-hidden=\"true\"></i>",
|
|
7
|
+
_tmpl$2 = ["<div", " class=\"k2b-choice-slider__track\" role=\"slider\"", " aria-valuemin=\"1\"", ">", "", "<span class=\"k2b-choice-slider__thumb\"", "", " style=\"", "\"></span></div>"],
|
|
8
|
+
_tmpl$3 = ["<div", "><p class=\"k2b-choice-slider__line\"", " aria-live=\"polite\">", "<span>", "</span></p>", "</div>"],
|
|
9
|
+
_tmpl$4 = ["<span class=\"k2b-choice-slider__segment\"", " style=\"", "\"></span>"],
|
|
10
|
+
_tmpl$5 = ["<span class=\"k2b-choice-slider__stop\"", " style=\"", "\"></span>"];
|
|
11
|
+
import { createEffect, createMemo, createSignal, For, on, Show } from "solid-js";
|
|
12
|
+
import { resolveMaybeAccessor } from "./field-contract";
|
|
13
|
+
const position = (index, count) => count < 2 ? 0 : index / (count - 1) * 100;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A slider over a few named stops. The fixed line above the track always shows
|
|
17
|
+
* the active stop's description, or why a stop cannot be chosen; its height
|
|
18
|
+
* never changes. Unavailable stops stay visible and are skipped.
|
|
19
|
+
*/
|
|
20
|
+
export function ChoiceSlider(props) {
|
|
21
|
+
// The previewed stop by value, so a change of the options can never leave it pointing at another stop.
|
|
22
|
+
const [previewValue, setPreviewValue] = createSignal(null);
|
|
23
|
+
const [notice, setNotice] = createSignal(null);
|
|
24
|
+
let track;
|
|
25
|
+
let drag = null;
|
|
26
|
+
const count = () => props.options.length;
|
|
27
|
+
const chosen = createMemo(() => {
|
|
28
|
+
const value = resolveMaybeAccessor(props.value);
|
|
29
|
+
return value === null ? -1 : props.options.findIndex(option => option.value === value);
|
|
30
|
+
});
|
|
31
|
+
const preview = createMemo(() => {
|
|
32
|
+
const value = previewValue();
|
|
33
|
+
const index = value === null ? -1 : props.options.findIndex(option => option.value === value);
|
|
34
|
+
return index < 0 ? null : index;
|
|
35
|
+
});
|
|
36
|
+
const setPreview = index => setPreviewValue(index === null ? null : props.options[index]?.value ?? null);
|
|
37
|
+
const shown = () => preview() ?? chosen();
|
|
38
|
+
const option = () => props.options[shown()];
|
|
39
|
+
const available = index => props.options[index] !== undefined && !props.options[index]?.disabledReason;
|
|
40
|
+
const warning = () => notice() ?? (preview() === null ? option()?.disabledReason ?? null : null);
|
|
41
|
+
const line = () => warning() ?? option()?.description ?? (shown() < 0 ? props.description : undefined) ?? "";
|
|
42
|
+
createEffect(on(chosen, () => {
|
|
43
|
+
setPreview(null);
|
|
44
|
+
setNotice(null);
|
|
45
|
+
}));
|
|
46
|
+
// A drag ends without a choice when its stop disappears, becomes unavailable, or the slider is disabled.
|
|
47
|
+
createEffect(() => {
|
|
48
|
+
const index = preview();
|
|
49
|
+
if (previewValue() !== null && (index === null || !available(index) || props.disabled)) {
|
|
50
|
+
drag = null;
|
|
51
|
+
setPreviewValue(null);
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
createEffect(on(preview, index => props.onActiveChange?.(index === null ? null : props.options[index]?.value ?? null), {
|
|
55
|
+
defer: true
|
|
56
|
+
}));
|
|
57
|
+
const valueText = () => {
|
|
58
|
+
const current = option();
|
|
59
|
+
if (!current) return props.description ?? "";
|
|
60
|
+
return current.valueText ?? [current.label, warning() ?? current.description].filter(Boolean).join(", ");
|
|
61
|
+
};
|
|
62
|
+
const nearestAvailable = at => {
|
|
63
|
+
let best = null;
|
|
64
|
+
for (let index = 0; index < count(); index += 1) {
|
|
65
|
+
if (available(index) && (best === null || Math.abs(index - at) < Math.abs(best - at))) best = index;
|
|
66
|
+
}
|
|
67
|
+
return best;
|
|
68
|
+
};
|
|
69
|
+
const commit = index => {
|
|
70
|
+
const next = props.options[index];
|
|
71
|
+
setPreview(null);
|
|
72
|
+
if (next && !props.disabled && available(index) && index !== chosen()) props.onValueChange?.(next.value);
|
|
73
|
+
};
|
|
74
|
+
const step = direction => {
|
|
75
|
+
const from = shown() < 0 ? direction > 0 ? -1 : count() : shown();
|
|
76
|
+
for (let index = from + direction; index >= 0 && index < count(); index += direction) {
|
|
77
|
+
if (available(index)) {
|
|
78
|
+
setNotice(null);
|
|
79
|
+
commit(index);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const neighbour = props.options[from + direction];
|
|
84
|
+
if (neighbour?.disabledReason) setNotice(neighbour.disabledReason);
|
|
85
|
+
};
|
|
86
|
+
const edge = end => {
|
|
87
|
+
const indexes = props.options.map((_, index) => index).filter(available);
|
|
88
|
+
const target = end ? indexes.at(-1) : indexes[0];
|
|
89
|
+
if (target !== undefined) {
|
|
90
|
+
setNotice(null);
|
|
91
|
+
commit(target);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
const onKeyDown = event => {
|
|
95
|
+
if (props.disabled) return;
|
|
96
|
+
const keys = {
|
|
97
|
+
ArrowRight: () => step(1),
|
|
98
|
+
ArrowUp: () => step(1),
|
|
99
|
+
PageUp: () => step(1),
|
|
100
|
+
ArrowLeft: () => step(-1),
|
|
101
|
+
ArrowDown: () => step(-1),
|
|
102
|
+
PageDown: () => step(-1),
|
|
103
|
+
Home: () => edge(false),
|
|
104
|
+
End: () => edge(true)
|
|
105
|
+
};
|
|
106
|
+
const action = keys[event.key];
|
|
107
|
+
if (!action) return;
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
action();
|
|
110
|
+
};
|
|
111
|
+
const at = clientX => {
|
|
112
|
+
const rect = track?.getBoundingClientRect();
|
|
113
|
+
if (!rect || rect.width === 0) return 0;
|
|
114
|
+
return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)) * (count() - 1);
|
|
115
|
+
};
|
|
116
|
+
const onPointerDown = event => {
|
|
117
|
+
// One pointer owns a drag; a second finger waits until it ends.
|
|
118
|
+
if (props.disabled || count() < 2 || event.button !== 0 || drag) return;
|
|
119
|
+
event.preventDefault();
|
|
120
|
+
track?.focus({
|
|
121
|
+
preventScroll: true
|
|
122
|
+
});
|
|
123
|
+
track?.setPointerCapture?.(event.pointerId);
|
|
124
|
+
const near = Math.round(at(event.clientX));
|
|
125
|
+
const pressed = props.options[near];
|
|
126
|
+
if (pressed?.disabledReason) {
|
|
127
|
+
drag = {
|
|
128
|
+
pointerId: event.pointerId,
|
|
129
|
+
blocked: near,
|
|
130
|
+
startX: event.clientX
|
|
131
|
+
};
|
|
132
|
+
setNotice(pressed.disabledReason);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
drag = {
|
|
136
|
+
pointerId: event.pointerId,
|
|
137
|
+
blocked: null,
|
|
138
|
+
startX: event.clientX
|
|
139
|
+
};
|
|
140
|
+
setNotice(null);
|
|
141
|
+
setPreview(nearestAvailable(at(event.clientX)));
|
|
142
|
+
};
|
|
143
|
+
const onPointerMove = event => {
|
|
144
|
+
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
145
|
+
if (drag.blocked !== null && Math.abs(event.clientX - drag.startX) <= 2) return;
|
|
146
|
+
drag.blocked = null;
|
|
147
|
+
const next = nearestAvailable(at(event.clientX));
|
|
148
|
+
if (next !== null) {
|
|
149
|
+
setNotice(null);
|
|
150
|
+
setPreview(next);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/** Only a release chooses; a cancel or a lost capture puts the slider back on its value. */
|
|
155
|
+
const onPointerEnd = event => {
|
|
156
|
+
if (!drag || drag.pointerId !== event.pointerId) return;
|
|
157
|
+
drag = null;
|
|
158
|
+
const index = preview();
|
|
159
|
+
if (event.type !== "pointerup" || index === null) {
|
|
160
|
+
setPreview(null);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
commit(index);
|
|
164
|
+
};
|
|
165
|
+
return _$ssr(_tmpl$3, _$ssrAttribute("class", _$escape(["k2b-choice-slider", props.class].filter(Boolean).join(" "), true), false) + _$ssrAttribute("data-size", _$escape(props.size ?? "md", true), false), _$ssrAttribute("data-tone", warning() ? "warning" : _$escape(undefined, true), false), _$escape(_$createComponent(Show, {
|
|
166
|
+
get when() {
|
|
167
|
+
return warning();
|
|
168
|
+
},
|
|
169
|
+
get children() {
|
|
170
|
+
return _$ssr(_tmpl$);
|
|
171
|
+
}
|
|
172
|
+
})), _$escape(line()), _$escape(_$createComponent(Show, {
|
|
173
|
+
get when() {
|
|
174
|
+
return count() >= 2;
|
|
175
|
+
},
|
|
176
|
+
get children() {
|
|
177
|
+
return _$ssr(_tmpl$2, _$ssrAttribute("id", _$escape(props.id, true), false), _$ssrAttribute("tabindex", props.disabled ? -1 : 0, false) + _$ssrAttribute("aria-label", _$escape(props["aria-label"], true), false) + _$ssrAttribute("aria-labelledby", _$escape(props["aria-labelledby"], true), false), _$ssrAttribute("aria-valuemax", _$escape(count(), true), false) + _$ssrAttribute("aria-valuenow", _$escape(Math.max(0, shown()), true) + 1, false) + _$ssrAttribute("aria-valuetext", _$escape(valueText(), true), false) + _$ssrAttribute("aria-disabled", props.disabled ? "true" : _$escape(undefined, true), false), _$escape(_$createComponent(For, {
|
|
178
|
+
get each() {
|
|
179
|
+
return props.options.slice(1);
|
|
180
|
+
},
|
|
181
|
+
children: (_, index) => _$ssr(_tmpl$4, _$ssrAttribute("data-on", shown() > index() ? "" : _$escape(undefined, true), false), _$ssrStyleProperty("left:", `calc(${_$escape(position(index(), count()), true)}% + 8px)`) + _$ssrStyleProperty(";width:", `calc(${_$escape(position(index() + 1, count()), true) - _$escape(position(index(), count()), true)}% - 16px)`))
|
|
182
|
+
})), _$escape(_$createComponent(For, {
|
|
183
|
+
get each() {
|
|
184
|
+
return props.options;
|
|
185
|
+
},
|
|
186
|
+
children: (stop, index) => _$ssr(_tmpl$5, _$ssrAttribute("data-on", shown() > index() ? "" : _$escape(undefined, true), false) + _$ssrAttribute("data-unavailable", stop.disabledReason ? "" : _$escape(undefined, true), false) + _$ssrAttribute("data-current", index() === shown() ? "" : _$escape(undefined, true), false), _$ssrStyleProperty("left:", `${_$escape(position(index(), count()), true)}%`))
|
|
187
|
+
})), _$ssrAttribute("data-tone", warning() && preview() === null && option()?.disabledReason ? "warning" : _$escape(undefined, true), false), _$ssrAttribute("hidden", shown() < 0, true), _$ssrStyleProperty("left:", `${_$escape(position(Math.max(0, shown()), count()), true)}%`));
|
|
188
|
+
}
|
|
189
|
+
})));
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=ChoiceSlider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sources":["inputs/ChoiceSlider.tsx"],"sourcesContent":["import { createEffect, createMemo, createSignal, For, type JSX, on, Show } from \"solid-js\";\nimport { type MaybeAccessor, resolveMaybeAccessor } from \"./field-contract\";\n\nexport type ChoiceSliderOption = {\n value: string;\n label: string;\n /** One short line shown below the label line while this stop is active. */\n description?: string;\n /** Makes the stop unavailable. The text replaces the description when someone reaches the stop. */\n disabledReason?: string;\n /** Spoken value; defaults to the label and the description or reason. */\n valueText?: string;\n};\n\nexport type ChoiceSliderProps = {\n options: readonly ChoiceSliderOption[];\n /** The chosen stop. `null` shows no thumb, for a choice that is none of the stops. */\n value: MaybeAccessor<string | null>;\n /** A person released the thumb on a stop or moved to one with the keyboard. */\n onValueChange?: (value: string) => void;\n /** The stop the slider shows while someone drags or arrows through it; `null` when it shows `value` again. */\n onActiveChange?: (value: string | null) => void;\n /** Line text while no stop is chosen. */\n description?: string;\n \"aria-label\"?: string;\n \"aria-labelledby\"?: string;\n id?: string;\n class?: string;\n disabled?: boolean;\n /** `lg` gives a 44 px track and a 28 px thumb for touch sheets. */\n size?: \"md\" | \"lg\";\n};\n\nconst position = (index: number, count: number) => (count < 2 ? 0 : (index / (count - 1)) * 100);\n\n/**\n * A slider over a few named stops. The fixed line above the track always shows\n * the active stop's description, or why a stop cannot be chosen; its height\n * never changes. Unavailable stops stay visible and are skipped.\n */\nexport function ChoiceSlider(props: ChoiceSliderProps): JSX.Element {\n // The previewed stop by value, so a change of the options can never leave it pointing at another stop.\n const [previewValue, setPreviewValue] = createSignal<string | null>(null);\n const [notice, setNotice] = createSignal<string | null>(null);\n let track: HTMLDivElement | undefined;\n let drag: { pointerId: number; blocked: number | null; startX: number } | null = null;\n\n const count = () => props.options.length;\n const chosen = createMemo(() => {\n const value = resolveMaybeAccessor(props.value);\n return value === null ? -1 : props.options.findIndex((option) => option.value === value);\n });\n const preview = createMemo(() => {\n const value = previewValue();\n const index = value === null ? -1 : props.options.findIndex((option) => option.value === value);\n return index < 0 ? null : index;\n });\n const setPreview = (index: number | null) => setPreviewValue(index === null ? null : (props.options[index]?.value ?? null));\n const shown = () => preview() ?? chosen();\n const option = () => props.options[shown()];\n const available = (index: number) => props.options[index] !== undefined && !props.options[index]?.disabledReason;\n const warning = () => notice() ?? (preview() === null ? (option()?.disabledReason ?? null) : null);\n const line = () => warning() ?? option()?.description ?? (shown() < 0 ? props.description : undefined) ?? \"\";\n\n createEffect(\n on(chosen, () => {\n setPreview(null);\n setNotice(null);\n }),\n );\n // A drag ends without a choice when its stop disappears, becomes unavailable, or the slider is disabled.\n createEffect(() => {\n const index = preview();\n if (previewValue() !== null && (index === null || !available(index) || props.disabled)) {\n drag = null;\n setPreviewValue(null);\n }\n });\n createEffect(\n on(preview, (index) => props.onActiveChange?.(index === null ? null : (props.options[index]?.value ?? null)), { defer: true }),\n );\n\n const valueText = () => {\n const current = option();\n if (!current) return props.description ?? \"\";\n return current.valueText ?? [current.label, warning() ?? current.description].filter(Boolean).join(\", \");\n };\n\n const nearestAvailable = (at: number) => {\n let best: number | null = null;\n for (let index = 0; index < count(); index += 1) {\n if (available(index) && (best === null || Math.abs(index - at) < Math.abs(best - at))) best = index;\n }\n return best;\n };\n\n const commit = (index: number) => {\n const next = props.options[index];\n setPreview(null);\n if (next && !props.disabled && available(index) && index !== chosen()) props.onValueChange?.(next.value);\n };\n\n const step = (direction: 1 | -1) => {\n const from = shown() < 0 ? (direction > 0 ? -1 : count()) : shown();\n for (let index = from + direction; index >= 0 && index < count(); index += direction) {\n if (available(index)) {\n setNotice(null);\n commit(index);\n return;\n }\n }\n const neighbour = props.options[from + direction];\n if (neighbour?.disabledReason) setNotice(neighbour.disabledReason);\n };\n\n const edge = (end: boolean) => {\n const indexes = props.options.map((_, index) => index).filter(available);\n const target = end ? indexes.at(-1) : indexes[0];\n if (target !== undefined) {\n setNotice(null);\n commit(target);\n }\n };\n\n const onKeyDown = (event: KeyboardEvent) => {\n if (props.disabled) return;\n const keys: Record<string, () => void> = {\n ArrowRight: () => step(1),\n ArrowUp: () => step(1),\n PageUp: () => step(1),\n ArrowLeft: () => step(-1),\n ArrowDown: () => step(-1),\n PageDown: () => step(-1),\n Home: () => edge(false),\n End: () => edge(true),\n };\n const action = keys[event.key];\n if (!action) return;\n event.preventDefault();\n action();\n };\n\n const at = (clientX: number) => {\n const rect = track?.getBoundingClientRect();\n if (!rect || rect.width === 0) return 0;\n return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width)) * (count() - 1);\n };\n\n const onPointerDown = (event: PointerEvent) => {\n // One pointer owns a drag; a second finger waits until it ends.\n if (props.disabled || count() < 2 || event.button !== 0 || drag) return;\n event.preventDefault();\n track?.focus({ preventScroll: true });\n track?.setPointerCapture?.(event.pointerId);\n const near = Math.round(at(event.clientX));\n const pressed = props.options[near];\n if (pressed?.disabledReason) {\n drag = { pointerId: event.pointerId, blocked: near, startX: event.clientX };\n setNotice(pressed.disabledReason);\n return;\n }\n drag = { pointerId: event.pointerId, blocked: null, startX: event.clientX };\n setNotice(null);\n setPreview(nearestAvailable(at(event.clientX)));\n };\n\n const onPointerMove = (event: PointerEvent) => {\n if (!drag || drag.pointerId !== event.pointerId) return;\n if (drag.blocked !== null && Math.abs(event.clientX - drag.startX) <= 2) return;\n drag.blocked = null;\n const next = nearestAvailable(at(event.clientX));\n if (next !== null) {\n setNotice(null);\n setPreview(next);\n }\n };\n\n /** Only a release chooses; a cancel or a lost capture puts the slider back on its value. */\n const onPointerEnd = (event: PointerEvent) => {\n if (!drag || drag.pointerId !== event.pointerId) return;\n drag = null;\n const index = preview();\n if (event.type !== \"pointerup\" || index === null) {\n setPreview(null);\n return;\n }\n commit(index);\n };\n\n return (\n <div class={[\"k2b-choice-slider\", props.class].filter(Boolean).join(\" \")} data-size={props.size ?? \"md\"}>\n <p class=\"k2b-choice-slider__line\" data-tone={warning() ? \"warning\" : undefined} aria-live=\"polite\">\n <Show when={warning()}>\n <i class=\"ti ti-alert-triangle\" aria-hidden=\"true\" />\n </Show>\n <span>{line()}</span>\n </p>\n <Show when={count() >= 2}>\n <div\n ref={track}\n id={props.id}\n class=\"k2b-choice-slider__track\"\n role=\"slider\"\n tabIndex={props.disabled ? -1 : 0}\n aria-label={props[\"aria-label\"]}\n aria-labelledby={props[\"aria-labelledby\"]}\n aria-valuemin={1}\n aria-valuemax={count()}\n aria-valuenow={Math.max(0, shown()) + 1}\n aria-valuetext={valueText()}\n aria-disabled={props.disabled ? \"true\" : undefined}\n onKeyDown={onKeyDown}\n onPointerDown={onPointerDown}\n onPointerMove={onPointerMove}\n onPointerUp={onPointerEnd}\n onPointerCancel={onPointerEnd}\n onLostPointerCapture={onPointerEnd}\n >\n <For each={props.options.slice(1)}>\n {(_, index) => (\n <span\n class=\"k2b-choice-slider__segment\"\n data-on={shown() > index() ? \"\" : undefined}\n style={{\n left: `calc(${position(index(), count())}% + 8px)`,\n width: `calc(${position(index() + 1, count()) - position(index(), count())}% - 16px)`,\n }}\n />\n )}\n </For>\n <For each={props.options}>\n {(stop, index) => (\n <span\n class=\"k2b-choice-slider__stop\"\n data-on={shown() > index() ? \"\" : undefined}\n data-unavailable={stop.disabledReason ? \"\" : undefined}\n data-current={index() === shown() ? \"\" : undefined}\n style={{ left: `${position(index(), count())}%` }}\n />\n )}\n </For>\n <span\n class=\"k2b-choice-slider__thumb\"\n data-tone={warning() && preview() === null && option()?.disabledReason ? \"warning\" : undefined}\n hidden={shown() < 0}\n style={{ left: `${position(Math.max(0, shown()), count())}%` }}\n />\n </div>\n </Show>\n </div>\n );\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ssr as _$ssr } from "solid-js/web";
|
|
2
|
+
import { createComponent as _$createComponent } from "solid-js/web";
|
|
3
|
+
import { ssrAttribute as _$ssrAttribute } from "solid-js/web";
|
|
4
|
+
import { escape as _$escape } from "solid-js/web";
|
|
5
|
+
var _tmpl$ = ["<div class=\"k2b-choice-tiles__group\" role=\"radiogroup\"", ">", "</div>"],
|
|
6
|
+
_tmpl$2 = ["<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>", "</span></button>"],
|
|
7
|
+
_tmpl$3 = ["<span class=\"k2b-choice-tiles__status\">", "</span>"];
|
|
8
|
+
import { For } from "solid-js";
|
|
9
|
+
import { createFieldMeta, Field, fieldDescribedBy } from "../internal/field";
|
|
10
|
+
import { radioKeyTarget } from "../internal/radio-keys";
|
|
11
|
+
import { resolveMaybeAccessor } from "./field-contract";
|
|
12
|
+
/**
|
|
13
|
+
* A labeled single choice shown as tiles with a preview, for picking how something looks: a face shape, a frame, a
|
|
14
|
+
* kind of picture. Every tile keeps its size when chosen, so nothing moves. Use `ChoiceChips` for text choices and
|
|
15
|
+
* `ColorChoices` for named colours.
|
|
16
|
+
*/
|
|
17
|
+
export function ChoiceTiles(props) {
|
|
18
|
+
const meta = createFieldMeta(props.id);
|
|
19
|
+
let group;
|
|
20
|
+
const value = () => resolveMaybeAccessor(props.value) ?? null;
|
|
21
|
+
const tabStop = (option, index) => value() === option.value || index === 0 && !props.options.some(candidate => candidate.value === value());
|
|
22
|
+
const choose = (index, focus = false) => {
|
|
23
|
+
const option = props.options[index];
|
|
24
|
+
if (!option || props.disabled) return;
|
|
25
|
+
props.onValueChange?.(option.value);
|
|
26
|
+
props.onValueCommit?.(option.value);
|
|
27
|
+
// The radios in their current DOM order: options may have been reordered since they rendered.
|
|
28
|
+
if (focus) queueMicrotask(() => group?.querySelectorAll('[role="radio"]')[index]?.focus());
|
|
29
|
+
};
|
|
30
|
+
return _$createComponent(Field, {
|
|
31
|
+
meta: meta,
|
|
32
|
+
labelFor: false,
|
|
33
|
+
get ["class"]() {
|
|
34
|
+
return props.class;
|
|
35
|
+
},
|
|
36
|
+
get label() {
|
|
37
|
+
return props.label;
|
|
38
|
+
},
|
|
39
|
+
get description() {
|
|
40
|
+
return props.description;
|
|
41
|
+
},
|
|
42
|
+
get error() {
|
|
43
|
+
return props.error;
|
|
44
|
+
},
|
|
45
|
+
get required() {
|
|
46
|
+
return props.required;
|
|
47
|
+
},
|
|
48
|
+
get disabled() {
|
|
49
|
+
return props.disabled;
|
|
50
|
+
},
|
|
51
|
+
get children() {
|
|
52
|
+
return _$ssr(_tmpl$, _$ssrAttribute("data-layout", _$escape(props.layout ?? "grid", true), false) + _$ssrAttribute("data-dimmed", props.dimmed ? "true" : _$escape(undefined, true), false) + _$ssrAttribute("aria-label", props.label ? _$escape(undefined, true) : _$escape(props["aria-label"], true), false) + _$ssrAttribute("aria-labelledby", props.label ? _$escape(meta.labelId, true) : _$escape(undefined, true), false) + _$ssrAttribute("aria-describedby", _$escape(fieldDescribedBy(meta, props), true), false) + _$ssrAttribute("aria-disabled", props.disabled ? "true" : _$escape(undefined, true), false), _$escape(_$createComponent(For, {
|
|
53
|
+
get each() {
|
|
54
|
+
return props.options;
|
|
55
|
+
},
|
|
56
|
+
children: (option, index) => _$ssr(_tmpl$2, _$ssrAttribute("aria-checked", _$escape(value(), true) === _$escape(option.value, true), false), _$ssrAttribute("disabled", props.disabled, true) + _$ssrAttribute("tabindex", tabStop(option, index()) ? 0 : -1, false), _$escape(option.visual), _$escape(option.label), props.layout === "row" && _$ssr(_tmpl$3, _$escape(option.status ?? "")))
|
|
57
|
+
})));
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
//# 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":";;;;;;;AAAA,SAAS,GAAG,QAAkB,UAAU;AACxC,SAAS,eAAe,EAAE,KAAK,EAAE,gBAAgB,QAAQ,mBAAmB;AAC5E,SAAS,cAAc,QAAQ,wBAAwB;AAEvD,SAAS,oBAAoB,QAAQ,kBAAkB;AAqBvD;AACA;AACA;AACA;AACA;AACA,OAAO,SAAS,WAAW,CAA4B,KAA0B,EAAe;EAC9F,MAAM,IAAI,GAAG,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC;EACtC,IAAI,KAAiC;EACrC,MAAM,KAAK,GAAG,MAAM,oBAAoB,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,IAAI;EAC7D,MAAM,OAAO,GAAG,CAAC,MAA2B,EAAE,KAAa,KACzD,KAAK,CAAC,CAAC,KAAK,MAAM,CAAC,KAAK,IAAK,KAAK,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAE,SAAS,IAAK,SAAS,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,CAAE;EAC9G,MAAM,MAAM,GAAG,CAAC,KAAa,EAAE,KAAK,GAAG,KAAK,KAAK;IAC/C,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;IACnC,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,QAAQ,EAAE;IAC/B,KAAK,CAAC,aAAa,GAAG,MAAM,CAAC,KAAK,CAAC;IACnC,KAAK,CAAC,aAAa,GAAG,MAAM,CAAC,KAAK,CAAC;IACnC;IACA,IAAI,KAAK,EAAE,cAAc,CAAC,MAAM,KAAK,EAAE,gBAAgB,CAAoB,gBAAgB,CAAC,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;EAC/G,CAAC;EACD,yBACG,KAAK;IACJ,IAAI,EAAE,IAAI;IACV,QAAQ,EAAE,KAAK;IAAA;MAAA,OACR,KAAK,CAAC,KAAK;IAAA;IAAA,IAClB,KAAK;MAAA,OAAE,KAAK,CAAC,KAAK;IAAA;IAAA,IAClB,WAAW;MAAA,OAAE,KAAK,CAAC,WAAW;IAAA;IAAA,IAC9B,KAAK;MAAA,OAAE,KAAK,CAAC,KAAK;IAAA;IAAA,IAClB,QAAQ;MAAA,OAAE,KAAK,CAAC,QAAQ;IAAA;IAAA,IACxB,QAAQ;MAAA,OAAE,KAAK,CAAC,QAAQ;IAAA;IAAA;MAAA,4DAMT,KAAK,CAAC,MAAM,IAAI,MAAM,gDACtB,KAAK,CAAC,MAAM,GAAG,MAAM,YAAG,SAAS,+CAClC,KAAK,CAAC,KAAK,YAAG,SAAS,mBAAG,KAAK,CAAC,YAAY,CAAC,oDACxC,KAAK,CAAC,KAAK,YAAG,IAAI,CAAC,OAAO,mBAAG,SAAS,8DACrC,gBAAgB,CAAC,IAAI,EAAE,KAAK,CAAC,kDAChC,KAAK,CAAC,QAAQ,GAAG,MAAM,YAAG,SAAS,4CAEjD,GAAG;QAAA,IAAC,IAAI;UAAA,OAAE,KAAK,CAAC,OAAO;QAAA;QAAA,UACrB,CAAC,MAAM,EAAE,KAAK,mDAKG,cAAK,CAAC,CAAC,qBAAK,MAAM,CAAC,KAAK,4CAC5B,KAAK,CAAC,QAAQ,qCACd,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,mBAUxC,MAAM,CAAC,MAAM,YAGyB,MAAM,CAAC,KAAK,GAClD,KAAK,CAAC,MAAM,KAAK,KAAK,4BAA4C,MAAM,CAAC,MAAM,IAAI,EAAE,EAAQ;MAGnG;IAAA;EAAA;AAKX","ignoreList":[]}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { ssrStyleProperty as _$ssrStyleProperty } from "solid-js/web";
|
|
2
|
+
import { ssr as _$ssr } from "solid-js/web";
|
|
3
|
+
import { createComponent as _$createComponent } from "solid-js/web";
|
|
4
|
+
import { ssrAttribute as _$ssrAttribute } from "solid-js/web";
|
|
5
|
+
import { escape as _$escape } from "solid-js/web";
|
|
6
|
+
var _tmpl$ = ["<div class=\"k2b-color-choices__group\" role=\"radiogroup\"", ">", "</div>"],
|
|
7
|
+
_tmpl$2 = ["<button type=\"button\" role=\"radio\" class=\"k2b-color-choices__swatch\" style=\"", "\"", "", "><i class=\"ti ti-check\" aria-hidden=\"true\"></i></button>"];
|
|
8
|
+
import { For } from "solid-js";
|
|
9
|
+
import { createFieldMeta, Field, fieldDescribedBy } from "../internal/field";
|
|
10
|
+
import { radioKeyTarget } from "../internal/radio-keys";
|
|
11
|
+
import { resolveMaybeAccessor } from "./field-contract";
|
|
12
|
+
/**
|
|
13
|
+
* A labeled single choice from a fixed palette of named colours, shown as round swatches. Use `ColorInput` when any
|
|
14
|
+
* colour is allowed.
|
|
15
|
+
*/
|
|
16
|
+
export function ColorChoices(props) {
|
|
17
|
+
const meta = createFieldMeta(props.id);
|
|
18
|
+
let group;
|
|
19
|
+
const value = () => resolveMaybeAccessor(props.value) ?? null;
|
|
20
|
+
const tabStop = (option, index) => value() === option.value || index === 0 && !props.options.some(candidate => candidate.value === value());
|
|
21
|
+
const choose = (index, focus = false) => {
|
|
22
|
+
const option = props.options[index];
|
|
23
|
+
if (!option || props.disabled) return;
|
|
24
|
+
props.onValueChange?.(option.value);
|
|
25
|
+
props.onValueCommit?.(option.value);
|
|
26
|
+
// The radios in their current DOM order: options may have been reordered since they rendered.
|
|
27
|
+
if (focus) queueMicrotask(() => group?.querySelectorAll('[role="radio"]')[index]?.focus());
|
|
28
|
+
};
|
|
29
|
+
return _$createComponent(Field, {
|
|
30
|
+
meta: meta,
|
|
31
|
+
labelFor: false,
|
|
32
|
+
get ["class"]() {
|
|
33
|
+
return props.class;
|
|
34
|
+
},
|
|
35
|
+
get label() {
|
|
36
|
+
return props.label;
|
|
37
|
+
},
|
|
38
|
+
get description() {
|
|
39
|
+
return props.description;
|
|
40
|
+
},
|
|
41
|
+
get error() {
|
|
42
|
+
return props.error;
|
|
43
|
+
},
|
|
44
|
+
get required() {
|
|
45
|
+
return props.required;
|
|
46
|
+
},
|
|
47
|
+
get disabled() {
|
|
48
|
+
return props.disabled;
|
|
49
|
+
},
|
|
50
|
+
get children() {
|
|
51
|
+
return _$ssr(_tmpl$, _$ssrAttribute("aria-label", props.label ? _$escape(undefined, true) : _$escape(props["aria-label"], true), false) + _$ssrAttribute("aria-labelledby", props.label ? _$escape(meta.labelId, true) : _$escape(undefined, true), false) + _$ssrAttribute("aria-describedby", _$escape(fieldDescribedBy(meta, props), true), false) + _$ssrAttribute("aria-disabled", props.disabled ? "true" : _$escape(undefined, true), false), _$escape(_$createComponent(For, {
|
|
52
|
+
get each() {
|
|
53
|
+
return props.options;
|
|
54
|
+
},
|
|
55
|
+
children: (option, index) => _$ssr(_tmpl$2, _$ssrStyleProperty("--k2b-color-choice:", _$escape(option.color, true)), _$ssrAttribute("aria-checked", _$escape(value(), true) === _$escape(option.value, true), false) + _$ssrAttribute("aria-label", _$escape(option.label, true), false) + _$ssrAttribute("title", _$escape(option.label, true), false), _$ssrAttribute("disabled", props.disabled, true) + _$ssrAttribute("tabindex", tabStop(option, index()) ? 0 : -1, false))
|
|
56
|
+
})));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
//# 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":";;;;;;;AAAA,SAAS,GAAG,QAAkB,UAAU;AACxC,SAAS,eAAe,EAAE,KAAK,EAAE,gBAAgB,QAAQ,mBAAmB;AAC5E,SAAS,cAAc,QAAQ,wBAAwB;AAEvD,SAAS,oBAAoB,QAAQ,kBAAkB;AAgBvD;AACA;AACA;AACA;AACA,OAAO,SAAS,YAAY,CAA4B,KAA2B,EAAe;EAChG,MAAM,IAAI,GAAG,eAAe,CAAC,KAAK,CAAC,EAAE,CAAC;EACtC,IAAI,KAAiC;EACrC,MAAM,KAAK,GAAG,MAAM,oBAAoB,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,IAAI;EAC7D,MAAM,OAAO,GAAG,CAAC,MAA4B,EAAE,KAAa,KAC1D,KAAK,CAAC,CAAC,KAAK,MAAM,CAAC,KAAK,IAAK,KAAK,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAE,SAAS,IAAK,SAAS,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,CAAE;EAC9G,MAAM,MAAM,GAAG,CAAC,KAAa,EAAE,KAAK,GAAG,KAAK,KAAK;IAC/C,MAAM,MAAM,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;IACnC,IAAI,CAAC,MAAM,IAAI,KAAK,CAAC,QAAQ,EAAE;IAC/B,KAAK,CAAC,aAAa,GAAG,MAAM,CAAC,KAAK,CAAC;IACnC,KAAK,CAAC,aAAa,GAAG,MAAM,CAAC,KAAK,CAAC;IACnC;IACA,IAAI,KAAK,EAAE,cAAc,CAAC,MAAM,KAAK,EAAE,gBAAgB,CAAoB,gBAAgB,CAAC,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;EAC/G,CAAC;EACD,yBACG,KAAK;IACJ,IAAI,EAAE,IAAI;IACV,QAAQ,EAAE,KAAK;IAAA;MAAA,OACR,KAAK,CAAC,KAAK;IAAA;IAAA,IAClB,KAAK;MAAA,OAAE,KAAK,CAAC,KAAK;IAAA;IAAA,IAClB,WAAW;MAAA,OAAE,KAAK,CAAC,WAAW;IAAA;IAAA,IAC9B,KAAK;MAAA,OAAE,KAAK,CAAC,KAAK;IAAA;IAAA,IAClB,QAAQ;MAAA,OAAE,KAAK,CAAC,QAAQ;IAAA;IAAA,IACxB,QAAQ;MAAA,OAAE,KAAK,CAAC,QAAQ;IAAA;IAAA;MAAA,kDAMV,KAAK,CAAC,KAAK,YAAG,SAAS,mBAAG,KAAK,CAAC,YAAY,CAAC,oDACxC,KAAK,CAAC,KAAK,YAAG,IAAI,CAAC,OAAO,mBAAG,SAAS,8DACrC,gBAAgB,CAAC,IAAI,EAAE,KAAK,CAAC,kDAChC,KAAK,CAAC,QAAQ,GAAG,MAAM,YAAG,SAAS,4CAEjD,GAAG;QAAA,IAAC,IAAI;UAAA,OAAE,KAAK,CAAC,OAAO;QAAA;QAAA,UACrB,CAAC,MAAM,EAAE,KAAK,uEAKoB,MAAM,CAAC,KAAK,yCAC7B,cAAK,CAAC,CAAC,qBAAK,MAAM,CAAC,KAAK,wDAC1B,MAAM,CAAC,KAAK,mDACjB,MAAM,CAAC,KAAK,4CACT,KAAK,CAAC,QAAQ,qCACd,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;MAW9C;IAAA;EAAA;AAKX","ignoreList":[]}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import { createComponent as _$createComponent } from "solid-js/web";
|
|
2
1
|
import { ssr as _$ssr } from "solid-js/web";
|
|
2
|
+
import { createComponent as _$createComponent } from "solid-js/web";
|
|
3
3
|
import { ssrAttribute as _$ssrAttribute } from "solid-js/web";
|
|
4
4
|
import { escape as _$escape } from "solid-js/web";
|
|
5
5
|
var _tmpl$ = ["<p class=\"k2b-qr-scanner__note\">", "</p>"],
|
|
6
|
-
_tmpl$2 = ["<div", "><div class=\"k2b-qr-scanner__preview\"", "><video muted playsinline", "></video></div><p role=\"status\">", "</p>", "</div>"];
|
|
6
|
+
_tmpl$2 = ["<div", "><div class=\"k2b-qr-scanner__preview\"", "><video autoplay muted playsinline", "></video></div><p role=\"status\">", "</p>", "", "</div>"];
|
|
7
7
|
import { timed } from "@k2b/stdlib/solid";
|
|
8
8
|
import { createSignal, onCleanup, onMount, Show } from "solid-js";
|
|
9
|
+
import { Button } from "../actions/Button";
|
|
10
|
+
import { useUiMessages } from "../intl/messages";
|
|
9
11
|
import { prefersReducedMotion } from "../motion";
|
|
10
12
|
import { useQrMessages } from "./qr-messages";
|
|
11
13
|
|
|
@@ -87,6 +89,12 @@ const openCamera = async closed => {
|
|
|
87
89
|
error
|
|
88
90
|
};
|
|
89
91
|
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* How long the camera may take to show its first picture. A working camera shows one well within a second; after three
|
|
95
|
+
* the person has looked at a black square long enough to need a way to start it again.
|
|
96
|
+
*/
|
|
97
|
+
const firstPictureMs = 3000;
|
|
90
98
|
const stopStream = stream => {
|
|
91
99
|
for (const track of stream.getTracks()) track.stop();
|
|
92
100
|
};
|
|
@@ -98,7 +106,9 @@ const stopStream = stream => {
|
|
|
98
106
|
*/
|
|
99
107
|
export function QrScanner(props) {
|
|
100
108
|
const qrMessages = useQrMessages();
|
|
109
|
+
const uiMessages = useUiMessages();
|
|
101
110
|
const [starting, setStarting] = createSignal(true);
|
|
111
|
+
const [stalled, setStalled] = createSignal(false);
|
|
102
112
|
const [invalid, setInvalid] = createSignal(false);
|
|
103
113
|
let lastRejected;
|
|
104
114
|
const resetRejected = timed.debounce(() => {
|
|
@@ -107,17 +117,21 @@ export function QrScanner(props) {
|
|
|
107
117
|
const resetFeedback = timed.debounce(() => setInvalid(false), 1500);
|
|
108
118
|
let video;
|
|
109
119
|
let scanner;
|
|
120
|
+
let stream;
|
|
110
121
|
let closed = false;
|
|
111
|
-
|
|
122
|
+
const detach = [];
|
|
112
123
|
const destroy = () => {
|
|
113
124
|
closed = true;
|
|
114
|
-
|
|
125
|
+
for (const listener of detach.splice(0)) listener();
|
|
115
126
|
resetRejected.cancel();
|
|
116
127
|
resetFeedback.cancel();
|
|
117
|
-
// pause(true) stops the camera stream immediately.
|
|
128
|
+
// pause(true) stops the camera stream immediately while the engine runs; a stream it never started stops here.
|
|
118
129
|
void scanner?.pause(true);
|
|
119
130
|
scanner?.destroy();
|
|
120
131
|
scanner = undefined;
|
|
132
|
+
if (stream) stopStream(stream);
|
|
133
|
+
stream = undefined;
|
|
134
|
+
video.srcObject = null;
|
|
121
135
|
};
|
|
122
136
|
// Once closed, the host has unmounted the scanner or heard why it stopped; later failures stay silent.
|
|
123
137
|
const fail = reason => {
|
|
@@ -125,6 +139,40 @@ export function QrScanner(props) {
|
|
|
125
139
|
destroy();
|
|
126
140
|
props.onError(reason);
|
|
127
141
|
};
|
|
142
|
+
/**
|
|
143
|
+
* Waits for the camera picture. WebKit, above all in an installed web app, can leave a stream black: play() never
|
|
144
|
+
* settles or is refused. The scanner then offers to try again rather than wait in silence, and a picture that starts
|
|
145
|
+
* late still clears that offer.
|
|
146
|
+
*/
|
|
147
|
+
let attempt = 0;
|
|
148
|
+
const show = async play => {
|
|
149
|
+
const current = ++attempt;
|
|
150
|
+
const latest = () => !closed && current === attempt;
|
|
151
|
+
setStalled(false);
|
|
152
|
+
const playing = play().then(() => video.videoWidth > 0, () => false);
|
|
153
|
+
let timer;
|
|
154
|
+
const shown = await Promise.race([playing, new Promise(resolve => {
|
|
155
|
+
timer = setTimeout(resolve, firstPictureMs, false);
|
|
156
|
+
})]);
|
|
157
|
+
clearTimeout(timer);
|
|
158
|
+
if (!latest()) return;
|
|
159
|
+
if (!shown) {
|
|
160
|
+
setStalled(true);
|
|
161
|
+
if (!(await playing) || !latest()) return;
|
|
162
|
+
}
|
|
163
|
+
setStalled(false);
|
|
164
|
+
setStarting(false);
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Attaching the stream again restarts a player that stalled on it, and play() runs within the click, the gesture a
|
|
168
|
+
* refused play() may need. The engine already runs from its first start and scans from the play event it hears.
|
|
169
|
+
*/
|
|
170
|
+
const retry = () => {
|
|
171
|
+
if (closed || !stream) return;
|
|
172
|
+
video.srcObject = null;
|
|
173
|
+
video.srcObject = stream;
|
|
174
|
+
void show(() => video.play());
|
|
175
|
+
};
|
|
128
176
|
onMount(() => {
|
|
129
177
|
const hidden = () => {
|
|
130
178
|
if (document.visibilityState !== "visible") props.onStop();
|
|
@@ -182,21 +230,52 @@ export function QrScanner(props) {
|
|
|
182
230
|
};
|
|
183
231
|
applyMotion();
|
|
184
232
|
motion.addEventListener("change", applyMotion);
|
|
185
|
-
|
|
233
|
+
detach.push(() => motion.removeEventListener("change", applyMotion));
|
|
234
|
+
// The engine places its scan region and frame on loadedmetadata, play, and a window resize. It misses a picture
|
|
235
|
+
// that turns, as when a tablet rotates, and a preview that changes size alone; replay loadedmetadata for both,
|
|
236
|
+
// or the region stays where the old picture had it and the frame sits off-centre.
|
|
237
|
+
const relayout = () => {
|
|
238
|
+
if (video.videoWidth > 0) video.dispatchEvent(new Event("loadedmetadata"));
|
|
239
|
+
};
|
|
240
|
+
video.addEventListener("resize", relayout);
|
|
241
|
+
const resized = new ResizeObserver(relayout);
|
|
242
|
+
resized.observe(video);
|
|
243
|
+
detach.push(() => {
|
|
244
|
+
video.removeEventListener("resize", relayout);
|
|
245
|
+
resized.disconnect();
|
|
246
|
+
});
|
|
186
247
|
const camera = await openCamera(() => closed);
|
|
187
248
|
if ("error" in camera) return fail(camera.error);
|
|
188
249
|
if (closed) return stopStream(camera.stream);
|
|
189
250
|
// qr-scanner plays a stream that is already set instead of opening its own.
|
|
190
|
-
|
|
251
|
+
stream = camera.stream;
|
|
252
|
+
video.srcObject = stream;
|
|
191
253
|
video.style.transform = camera.mirrored ? "scaleX(-1)" : "";
|
|
192
|
-
|
|
193
|
-
|
|
254
|
+
// start() awaits the engine's play() of this stream.
|
|
255
|
+
const engine = scanner;
|
|
256
|
+
await show(() => engine.start());
|
|
194
257
|
} catch {
|
|
195
258
|
fail("unavailable");
|
|
196
259
|
}
|
|
197
260
|
})();
|
|
198
261
|
});
|
|
199
|
-
return _$ssr(_tmpl$2, _$ssrAttribute("class", props.class ? `k2b-qr-scanner ${_$escape(props.class, true)}` : "k2b-qr-scanner", false), _$ssrAttribute("data-invalid", _$escape(invalid() || undefined, true), false), _$ssrAttribute("aria-label", _$escape(qrMessages().qrCameraPreview, true), false), starting() ? _$escape(qrMessages().qrCameraStarting) : _$escape(props.instructions ?? qrMessages().qrCameraInstructions), _$escape(_$createComponent(Show, {
|
|
262
|
+
return _$ssr(_tmpl$2, _$ssrAttribute("class", props.class ? `k2b-qr-scanner ${_$escape(props.class, true)}` : "k2b-qr-scanner", false), _$ssrAttribute("data-invalid", _$escape(invalid() || undefined, true), false), _$ssrAttribute("aria-label", _$escape(qrMessages().qrCameraPreview, true), false), stalled() ? _$escape(qrMessages().qrCameraStalled) : starting() ? _$escape(qrMessages().qrCameraStarting) : _$escape(props.instructions ?? qrMessages().qrCameraInstructions), _$escape(_$createComponent(Show, {
|
|
263
|
+
get when() {
|
|
264
|
+
return stalled();
|
|
265
|
+
},
|
|
266
|
+
get children() {
|
|
267
|
+
return _$createComponent(Button, {
|
|
268
|
+
type: "button",
|
|
269
|
+
size: "sm",
|
|
270
|
+
variant: "secondary",
|
|
271
|
+
"class": "k2b-qr-scanner__retry",
|
|
272
|
+
onClick: retry,
|
|
273
|
+
get children() {
|
|
274
|
+
return uiMessages().tryAgain;
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
})), _$escape(_$createComponent(Show, {
|
|
200
279
|
get when() {
|
|
201
280
|
return !starting();
|
|
202
281
|
},
|