@happyvertical/smrt-ui 0.52.0 → 0.53.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/AGENTS.md +11 -1
- package/README.md +300 -2
- package/dist/components/data/DataTable.svelte +60 -15
- package/dist/components/data/DataTable.svelte.d.ts.map +1 -1
- package/dist/components/data/DataTableResponsive.d.ts +4 -0
- package/dist/components/data/DataTableResponsive.d.ts.map +1 -0
- package/dist/components/data/DataTableResponsive.js +22 -0
- package/dist/components/data/__tests__/DataTableResponsive.test.js +80 -0
- package/dist/components/data/__tests__/responsive-table.fixture.svelte +29 -0
- package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts +7 -0
- package/dist/components/data/__tests__/responsive-table.fixture.svelte.d.ts.map +1 -0
- package/dist/components/data/types.d.ts +8 -0
- package/dist/components/data/types.d.ts.map +1 -1
- package/dist/components/display/DateDisplay.svelte +76 -14
- package/dist/components/display/DateDisplay.svelte.d.ts +2 -0
- package/dist/components/display/DateDisplay.svelte.d.ts.map +1 -1
- package/dist/components/display/Icon.svelte +3 -16
- package/dist/components/display/Icon.svelte.d.ts +0 -9
- package/dist/components/display/Icon.svelte.d.ts.map +1 -1
- package/dist/components/display/StatusBadge.svelte +29 -2
- package/dist/components/display/StatusBadge.svelte.d.ts +4 -2
- package/dist/components/display/StatusBadge.svelte.d.ts.map +1 -1
- package/dist/components/display/__tests__/DateDisplay.test.js +82 -1
- package/dist/components/display/__tests__/Icon.test.js +132 -2
- package/dist/components/display/__tests__/StatusBadge.test.js +78 -0
- package/dist/components/display/icons.svelte.d.ts +11 -0
- package/dist/components/display/icons.svelte.d.ts.map +1 -0
- package/dist/components/display/icons.svelte.js +60 -0
- package/dist/components/display/index.d.ts +3 -1
- package/dist/components/display/index.d.ts.map +1 -1
- package/dist/components/display/index.js +1 -0
- package/dist/components/display/types.d.ts +2 -0
- package/dist/components/display/types.d.ts.map +1 -1
- package/dist/components/feedback/ConfirmDialog.svelte +59 -48
- package/dist/components/feedback/ConfirmDialog.svelte.d.ts +14 -2
- package/dist/components/feedback/ConfirmDialog.svelte.d.ts.map +1 -1
- package/dist/components/feedback/ToastViewport.svelte +156 -8
- package/dist/components/feedback/ToastViewport.svelte.d.ts +5 -1
- package/dist/components/feedback/ToastViewport.svelte.d.ts.map +1 -1
- package/dist/components/feedback/__tests__/ConfirmDialog.test.js +38 -20
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte +9 -0
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts +19 -0
- package/dist/components/feedback/__tests__/confirm-message.fixture.svelte.d.ts.map +1 -0
- package/dist/components/feedback/__tests__/toast.test.js +142 -1
- package/dist/components/feedback/index.d.ts +1 -1
- package/dist/components/feedback/index.d.ts.map +1 -1
- package/dist/components/feedback/toast.d.ts +13 -1
- package/dist/components/feedback/toast.d.ts.map +1 -1
- package/dist/components/feedback/toast.js +51 -10
- package/dist/components/forms/CameraCapture.svelte +619 -0
- package/dist/components/forms/CameraCapture.svelte.d.ts +42 -0
- package/dist/components/forms/CameraCapture.svelte.d.ts.map +1 -0
- package/dist/components/forms/Checkbox.svelte +8 -1
- package/dist/components/forms/Checkbox.svelte.d.ts +2 -0
- package/dist/components/forms/Checkbox.svelte.d.ts.map +1 -1
- package/dist/components/forms/Combobox.svelte +40 -0
- package/dist/components/forms/Combobox.svelte.d.ts.map +1 -1
- package/dist/components/forms/Form.svelte +5 -0
- package/dist/components/forms/Form.svelte.d.ts +1 -0
- package/dist/components/forms/Form.svelte.d.ts.map +1 -1
- package/dist/components/forms/Input.svelte +8 -1
- package/dist/components/forms/Input.svelte.d.ts +2 -0
- package/dist/components/forms/Input.svelte.d.ts.map +1 -1
- package/dist/components/forms/Listbox.svelte +39 -1
- package/dist/components/forms/Listbox.svelte.d.ts.map +1 -1
- package/dist/components/forms/MultiSelect.svelte +41 -1
- package/dist/components/forms/MultiSelect.svelte.d.ts.map +1 -1
- package/dist/components/forms/Radio.svelte +1 -0
- package/dist/components/forms/RadioGroup.svelte +7 -1
- package/dist/components/forms/RadioGroup.svelte.d.ts +2 -0
- package/dist/components/forms/RadioGroup.svelte.d.ts.map +1 -1
- package/dist/components/forms/SegmentedControl.svelte +152 -11
- package/dist/components/forms/SegmentedControl.svelte.d.ts +4 -0
- package/dist/components/forms/SegmentedControl.svelte.d.ts.map +1 -1
- package/dist/components/forms/Select.svelte +8 -1
- package/dist/components/forms/Select.svelte.d.ts +2 -0
- package/dist/components/forms/Select.svelte.d.ts.map +1 -1
- package/dist/components/forms/SignaturePad.svelte +436 -0
- package/dist/components/forms/SignaturePad.svelte.d.ts +32 -0
- package/dist/components/forms/SignaturePad.svelte.d.ts.map +1 -0
- package/dist/components/forms/Switch.svelte +8 -1
- package/dist/components/forms/Switch.svelte.d.ts +2 -0
- package/dist/components/forms/Switch.svelte.d.ts.map +1 -1
- package/dist/components/forms/Textarea.svelte +8 -1
- package/dist/components/forms/Textarea.svelte.d.ts +2 -0
- package/dist/components/forms/Textarea.svelte.d.ts.map +1 -1
- package/dist/components/forms/__tests__/CameraCapture.test.js +504 -0
- package/dist/components/forms/__tests__/Combobox.test.js +279 -12
- package/dist/components/forms/__tests__/Form.test.js +46 -0
- package/dist/components/forms/__tests__/Listbox.test.js +303 -0
- package/dist/components/forms/__tests__/MultiSelect.test.js +281 -0
- package/dist/components/forms/__tests__/SignaturePad.test.js +338 -0
- package/dist/components/forms/__tests__/camera-capture-session.test.js +207 -0
- package/dist/components/forms/__tests__/capture-form-retry.test.js +166 -0
- package/dist/components/forms/__tests__/capture-form.fixture.svelte +38 -0
- package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts +10 -0
- package/dist/components/forms/__tests__/capture-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/capture-test-env.d.ts +70 -0
- package/dist/components/forms/__tests__/capture-test-env.d.ts.map +1 -0
- package/dist/components/forms/__tests__/capture-test-env.js +122 -0
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte +15 -0
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/capture-touch.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte +29 -0
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts +14 -0
- package/dist/components/forms/__tests__/combobox-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte +28 -0
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts +11 -0
- package/dist/components/forms/__tests__/form-attachment.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte +31 -0
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts +13 -0
- package/dist/components/forms/__tests__/form-retry-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/form-retry-form.test.js +107 -0
- package/dist/components/forms/__tests__/hidden-controls-form-retry.test.js +69 -0
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte +29 -0
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts +9 -0
- package/dist/components/forms/__tests__/hidden-controls-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte +27 -0
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts +13 -0
- package/dist/components/forms/__tests__/listbox-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte +27 -0
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts +13 -0
- package/dist/components/forms/__tests__/multiselect-form.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/native-file-field.test.js +177 -0
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte +28 -0
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts +12 -0
- package/dist/components/forms/__tests__/segmented-posting.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/segmented-posting.test.js +92 -0
- package/dist/components/forms/__tests__/signature-pad-logic.test.js +54 -0
- package/dist/components/forms/__tests__/touch-density.fixture.svelte +34 -0
- package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts +8 -0
- package/dist/components/forms/__tests__/touch-density.fixture.svelte.d.ts.map +1 -0
- package/dist/components/forms/__tests__/touch-density.test.js +66 -0
- package/dist/components/forms/camera-capture-session.d.ts +81 -0
- package/dist/components/forms/camera-capture-session.d.ts.map +1 -0
- package/dist/components/forms/camera-capture-session.js +130 -0
- package/dist/components/forms/form-retry/__tests__/boundaries.test.js +23 -0
- package/dist/components/forms/form-retry/__tests__/content-digest.test.js +70 -0
- package/dist/components/forms/form-retry/__tests__/controller.test.js +802 -0
- package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts +82 -0
- package/dist/components/forms/form-retry/__tests__/fake-kit.d.ts.map +1 -0
- package/dist/components/forms/form-retry/__tests__/fake-kit.js +180 -0
- package/dist/components/forms/form-retry/__tests__/form-draft.test.js +169 -0
- package/dist/components/forms/form-retry/__tests__/ssr.test.js +37 -0
- package/dist/components/forms/form-retry/__tests__/storage.d.ts +26 -0
- package/dist/components/forms/form-retry/__tests__/storage.d.ts.map +1 -0
- package/dist/components/forms/form-retry/__tests__/storage.js +63 -0
- package/dist/components/forms/form-retry/__tests__/submission-key.test.js +236 -0
- package/dist/components/forms/form-retry/__tests__/submitted-draft.test.js +334 -0
- package/dist/components/forms/form-retry/content-digest.d.ts +54 -0
- package/dist/components/forms/form-retry/content-digest.d.ts.map +1 -0
- package/dist/components/forms/form-retry/content-digest.js +169 -0
- package/dist/components/forms/form-retry/controller.d.ts +214 -0
- package/dist/components/forms/form-retry/controller.d.ts.map +1 -0
- package/dist/components/forms/form-retry/controller.js +404 -0
- package/dist/components/forms/form-retry/form-draft.d.ts +68 -0
- package/dist/components/forms/form-retry/form-draft.d.ts.map +1 -0
- package/dist/components/forms/form-retry/form-draft.js +161 -0
- package/dist/components/forms/form-retry/index.d.ts +19 -0
- package/dist/components/forms/form-retry/index.d.ts.map +1 -0
- package/dist/components/forms/form-retry/index.js +16 -0
- package/dist/components/forms/form-retry/submission-key.d.ts +244 -0
- package/dist/components/forms/form-retry/submission-key.d.ts.map +1 -0
- package/dist/components/forms/form-retry/submission-key.js +342 -0
- package/dist/components/forms/form-retry/submitted-draft.d.ts +112 -0
- package/dist/components/forms/form-retry/submitted-draft.d.ts.map +1 -0
- package/dist/components/forms/form-retry/submitted-draft.js +270 -0
- package/dist/components/forms/form-retry/types.d.ts +84 -0
- package/dist/components/forms/form-retry/types.d.ts.map +1 -0
- package/dist/components/forms/form-retry/types.js +13 -0
- package/dist/components/forms/index.d.ts +7 -1
- package/dist/components/forms/index.d.ts.map +1 -1
- package/dist/components/forms/index.js +8 -0
- package/dist/components/forms/native-file-field.d.ts +94 -0
- package/dist/components/forms/native-file-field.d.ts.map +1 -0
- package/dist/components/forms/native-file-field.js +147 -0
- package/dist/components/forms/signature-pad-logic.d.ts +50 -0
- package/dist/components/forms/signature-pad-logic.d.ts.map +1 -0
- package/dist/components/forms/signature-pad-logic.js +48 -0
- package/dist/components/forms/types.d.ts +41 -0
- package/dist/components/forms/types.d.ts.map +1 -1
- package/dist/components/nav/FilterChips.svelte +8 -1
- package/dist/components/nav/FilterChips.svelte.d.ts +2 -0
- package/dist/components/nav/FilterChips.svelte.d.ts.map +1 -1
- package/dist/components/ui/Button.svelte +9 -2
- package/dist/components/ui/Button.svelte.d.ts +2 -0
- package/dist/components/ui/Button.svelte.d.ts.map +1 -1
- package/dist/i18n/strings.ui.d.ts +24 -0
- package/dist/i18n/strings.ui.d.ts.map +1 -1
- package/dist/i18n/strings.ui.js +26 -0
- package/dist/styles/tokens.css +2 -0
- package/dist/svelte/__tests__/playground.test.js +2 -1
- package/dist/svelte/playground/CapturePreview.svelte +56 -0
- package/dist/svelte/playground/CapturePreview.svelte.d.ts +4 -0
- package/dist/svelte/playground/CapturePreview.svelte.d.ts.map +1 -0
- package/dist/svelte/playground.d.ts.map +1 -1
- package/dist/svelte/playground.js +10 -0
- package/dist/themes/ThemeProvider.svelte +9 -0
- package/dist/themes/ThemeProvider.svelte.d.ts +2 -0
- package/dist/themes/ThemeProvider.svelte.d.ts.map +1 -1
- package/dist/themes/css-generator.d.ts.map +1 -1
- package/dist/themes/css-generator.js +3 -0
- package/dist/themes/styles/glass.css +4 -0
- package/dist/themes/styles/happyvertical.css +4 -0
- package/dist/themes/styles/material.css +4 -0
- package/dist/themes/styles/smrt.css +4 -0
- package/dist/themes/styles/studio.css +4 -0
- package/package.json +8 -2
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Live camera capture through `getUserMedia`, for photos taken inside a form.
|
|
3
|
+
|
|
4
|
+
The component owns the whole flow: request the camera, preview the live
|
|
5
|
+
stream, capture a still, and let the user review and retake before the photo
|
|
6
|
+
is committed. A committed photo leaves through `onCapture({ blob, dataUrl })`
|
|
7
|
+
and, when `name` is set, is posted by a plain native form as a file field
|
|
8
|
+
(`native-file-field.ts` explains the DataTransfer and `formdata`-event
|
|
9
|
+
strategies). A reset of the owning form discards the photo, as it empties a
|
|
10
|
+
native file input. Permission denied, no camera, unsupported browser and
|
|
11
|
+
other errors are distinct rendered states.
|
|
12
|
+
|
|
13
|
+
The request/classify/release lifecycle lives in the framework-free
|
|
14
|
+
`camera-capture-session.ts`. The `$effect` below depends only on `disabled`
|
|
15
|
+
and `facingMode`, never on the rendered state: the state is written from the
|
|
16
|
+
session's async continuation, so reading it there would tear the stream down
|
|
17
|
+
and restart it in a loop. Its cleanup stops every track on `disabled` and on
|
|
18
|
+
unmount alike, so the camera light never stays on.
|
|
19
|
+
|
|
20
|
+
`<input type="file" capture>` is NOT the default path: it hands control to the
|
|
21
|
+
OS picker (no live preview, gallery access, no in-app review). It is available
|
|
22
|
+
only through the opt-in `fileInputFallback`, and only for browsers without
|
|
23
|
+
`getUserMedia`.
|
|
24
|
+
|
|
25
|
+
Ported from teamworks-os `CameraCapture.svelte`, smrt#3290.
|
|
26
|
+
-->
|
|
27
|
+
<script lang="ts">
|
|
28
|
+
import { onMount, untrack } from 'svelte';
|
|
29
|
+
import { M, useI18n } from '../../i18n/index.js';
|
|
30
|
+
import {
|
|
31
|
+
type CameraFacingMode,
|
|
32
|
+
createCameraSession,
|
|
33
|
+
} from './camera-capture-session.js';
|
|
34
|
+
import {
|
|
35
|
+
focusControl,
|
|
36
|
+
highlightControl,
|
|
37
|
+
revealControl,
|
|
38
|
+
} from './control-dom.js';
|
|
39
|
+
import type { ControlInteractionOptions } from './control-interaction.js';
|
|
40
|
+
import { tryGetControlInteractionContext } from './control-interaction-context.js';
|
|
41
|
+
import {
|
|
42
|
+
assignFileWithDataTransfer,
|
|
43
|
+
attachFormDataFallback,
|
|
44
|
+
attachFormResetListener,
|
|
45
|
+
clearFileInput,
|
|
46
|
+
detectNativeFileFieldStrategy,
|
|
47
|
+
fileNameForType,
|
|
48
|
+
type NativeFileFieldStrategy,
|
|
49
|
+
} from './native-file-field.js';
|
|
50
|
+
import type {
|
|
51
|
+
CameraCaptureLabels,
|
|
52
|
+
CameraCaptureState,
|
|
53
|
+
CaptureResult,
|
|
54
|
+
} from './types.js';
|
|
55
|
+
import { useControlRegistration } from './use-control-registration.svelte.js';
|
|
56
|
+
|
|
57
|
+
export interface Props {
|
|
58
|
+
/** Which camera to request. Re-requested on change while live. */
|
|
59
|
+
facingMode?: CameraFacingMode;
|
|
60
|
+
/**
|
|
61
|
+
* Stops the camera and disables every action; a held photo is kept and,
|
|
62
|
+
* when committed, still posts with the form.
|
|
63
|
+
*/
|
|
64
|
+
disabled?: boolean;
|
|
65
|
+
/** Called when the user commits a photo with "Use photo". */
|
|
66
|
+
onCapture?: (result: CaptureResult) => void;
|
|
67
|
+
/** Called when a committed photo is discarded by "Retake" or a form reset. */
|
|
68
|
+
onClear?: () => void;
|
|
69
|
+
/** Field name: the committed photo posts as a file in a native form. */
|
|
70
|
+
name?: string;
|
|
71
|
+
/** Posted file name. Defaults to `photo.<ext>` for the image type. */
|
|
72
|
+
fileName?: string;
|
|
73
|
+
/** Encoded image type. */
|
|
74
|
+
imageType?: 'image/jpeg' | 'image/png' | 'image/webp';
|
|
75
|
+
/** Encoder quality for lossy types, 0 to 1. */
|
|
76
|
+
quality?: number;
|
|
77
|
+
/**
|
|
78
|
+
* Opt in to `<input type="file" capture>` for browsers without
|
|
79
|
+
* `getUserMedia`. Off by default: without it those browsers render the
|
|
80
|
+
* unsupported state.
|
|
81
|
+
*/
|
|
82
|
+
fileInputFallback?: boolean;
|
|
83
|
+
/** Text overrides; defaults come from the `ui` i18n catalog. */
|
|
84
|
+
labels?: Partial<CameraCaptureLabels>;
|
|
85
|
+
/** DOM id of the component root. */
|
|
86
|
+
id?: string;
|
|
87
|
+
/** CSS class to apply to the component root. */
|
|
88
|
+
class?: string;
|
|
89
|
+
/** Interaction options or false to disable registration. */
|
|
90
|
+
interaction?: ControlInteractionOptions | false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let {
|
|
94
|
+
facingMode = 'environment',
|
|
95
|
+
disabled = false,
|
|
96
|
+
onCapture,
|
|
97
|
+
onClear,
|
|
98
|
+
name,
|
|
99
|
+
fileName,
|
|
100
|
+
imageType = 'image/jpeg',
|
|
101
|
+
quality = 0.92,
|
|
102
|
+
fileInputFallback = false,
|
|
103
|
+
labels,
|
|
104
|
+
id,
|
|
105
|
+
class: className = '',
|
|
106
|
+
interaction,
|
|
107
|
+
}: Props = $props();
|
|
108
|
+
|
|
109
|
+
const { t } = useI18n();
|
|
110
|
+
const text = $derived<CameraCaptureLabels>({
|
|
111
|
+
region: t(M['ui.camera_capture.region']),
|
|
112
|
+
preview: t(M['ui.camera_capture.preview']),
|
|
113
|
+
starting: t(M['ui.camera_capture.starting']),
|
|
114
|
+
off: t(M['ui.camera_capture.off']),
|
|
115
|
+
unsupported: t(M['ui.camera_capture.unsupported']),
|
|
116
|
+
permissionDenied: t(M['ui.camera_capture.permission_denied']),
|
|
117
|
+
noCamera: t(M['ui.camera_capture.no_camera']),
|
|
118
|
+
error: t(M['ui.camera_capture.error']),
|
|
119
|
+
retry: t(M['ui.camera_capture.retry']),
|
|
120
|
+
capture: t(M['ui.camera_capture.capture']),
|
|
121
|
+
retake: t(M['ui.camera_capture.retake']),
|
|
122
|
+
usePhoto: t(M['ui.camera_capture.use_photo']),
|
|
123
|
+
reviewAlt: t(M['ui.camera_capture.review_alt']),
|
|
124
|
+
committedAlt: t(M['ui.camera_capture.committed_alt']),
|
|
125
|
+
committed: t(M['ui.camera_capture.committed']),
|
|
126
|
+
choosePhoto: t(M['ui.camera_capture.choose_photo']),
|
|
127
|
+
...labels,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const instanceId = $props.id();
|
|
131
|
+
const interactionContext = tryGetControlInteractionContext();
|
|
132
|
+
const resolvedId = $derived(id ?? `smrt-camera-${instanceId}`);
|
|
133
|
+
const controlId = $derived(
|
|
134
|
+
interaction === false ? undefined : (interaction?.id ?? name ?? resolvedId),
|
|
135
|
+
);
|
|
136
|
+
const resolvedFileName = $derived(
|
|
137
|
+
fileName ?? fileNameForType('photo', imageType),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
let cameraState = $state<CameraCaptureState>(
|
|
141
|
+
untrack(() => disabled) ? 'off' : 'starting',
|
|
142
|
+
);
|
|
143
|
+
let previewUrl = $state('');
|
|
144
|
+
let unsupportedApi = $state(false);
|
|
145
|
+
let strategy = $state<NativeFileFieldStrategy>('data-transfer');
|
|
146
|
+
let rootEl = $state<HTMLDivElement | null>(null);
|
|
147
|
+
let videoEl = $state<HTMLVideoElement | null>(null);
|
|
148
|
+
let fieldEl = $state<HTMLInputElement | null>(null);
|
|
149
|
+
let pickerEl = $state<HTMLInputElement | null>(null);
|
|
150
|
+
let pending: CaptureResult | null = null;
|
|
151
|
+
let committedFile: File | null = null;
|
|
152
|
+
let fallbackReads = 0;
|
|
153
|
+
// Set while a captured frame encodes; a form reset supersedes the request.
|
|
154
|
+
let capturing = false;
|
|
155
|
+
let captureRequest = 0;
|
|
156
|
+
|
|
157
|
+
const fallbackActive = $derived(unsupportedApi && fileInputFallback);
|
|
158
|
+
const message = $derived.by(() => {
|
|
159
|
+
switch (cameraState) {
|
|
160
|
+
case 'off':
|
|
161
|
+
return text.off;
|
|
162
|
+
case 'starting':
|
|
163
|
+
return text.starting;
|
|
164
|
+
case 'unsupported':
|
|
165
|
+
return text.unsupported;
|
|
166
|
+
case 'permission-denied':
|
|
167
|
+
return text.permissionDenied;
|
|
168
|
+
case 'no-camera':
|
|
169
|
+
return text.noCamera;
|
|
170
|
+
case 'error':
|
|
171
|
+
return text.error;
|
|
172
|
+
case 'committed':
|
|
173
|
+
return text.committed;
|
|
174
|
+
default:
|
|
175
|
+
return '';
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
const isProblem = $derived(
|
|
179
|
+
cameraState === 'unsupported' ||
|
|
180
|
+
cameraState === 'permission-denied' ||
|
|
181
|
+
cameraState === 'no-camera' ||
|
|
182
|
+
cameraState === 'error',
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const session = createCameraSession({
|
|
186
|
+
mediaDevices:
|
|
187
|
+
typeof navigator === 'undefined' ? undefined : navigator.mediaDevices,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
function holdsPhoto(): boolean {
|
|
191
|
+
return cameraState === 'reviewing' || cameraState === 'committed';
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function startCamera(): Promise<void> {
|
|
195
|
+
cameraState = 'starting';
|
|
196
|
+
const result = await session.start(facingMode);
|
|
197
|
+
if (!result.ok) {
|
|
198
|
+
// `cancelled`: a stop() (unmount, `disabled`, a newer start) superseded
|
|
199
|
+
// this request and the session already released what it acquired.
|
|
200
|
+
if (result.kind === 'cancelled') return;
|
|
201
|
+
if (result.kind === 'unsupported') {
|
|
202
|
+
unsupportedApi = true;
|
|
203
|
+
cameraState = fileInputFallback ? 'fallback' : 'unsupported';
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
cameraState = result.kind;
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const stream = result.stream;
|
|
210
|
+
if (videoEl) {
|
|
211
|
+
videoEl.srcObject = stream;
|
|
212
|
+
try {
|
|
213
|
+
await videoEl.play();
|
|
214
|
+
} catch {
|
|
215
|
+
// Autoplay policy (or a test DOM) can reject play(); the stream is live.
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// A stop() during play() released this stream; do not resurrect the state.
|
|
219
|
+
if (session.stream !== stream) return;
|
|
220
|
+
cameraState = 'streaming';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function stopStream(): void {
|
|
224
|
+
session.stop();
|
|
225
|
+
if (videoEl) videoEl.srcObject = null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
$effect(() => {
|
|
229
|
+
const off = disabled;
|
|
230
|
+
void facingMode; // tracked: a new facing mode re-requests the camera
|
|
231
|
+
if (off) {
|
|
232
|
+
untrack(() => {
|
|
233
|
+
if (!holdsPhoto() && !unsupportedApi) cameraState = 'off';
|
|
234
|
+
});
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
untrack(() => {
|
|
238
|
+
if (!holdsPhoto() && !unsupportedApi) void startCamera();
|
|
239
|
+
});
|
|
240
|
+
return () => stopStream();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
onMount(() => {
|
|
244
|
+
strategy = detectNativeFileFieldStrategy();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
$effect(() => {
|
|
248
|
+
if (strategy !== 'formdata-event' || typeof document === 'undefined') return;
|
|
249
|
+
return attachFormDataFallback(document, () =>
|
|
250
|
+
name && fieldEl && !fallbackActive
|
|
251
|
+
? { form: fieldEl.form, name, file: committedFile, source: fieldEl }
|
|
252
|
+
: null,
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
function writeField(file: File | null): void {
|
|
257
|
+
if (!fieldEl || !name || strategy !== 'data-transfer') return;
|
|
258
|
+
if (!file) {
|
|
259
|
+
clearFileInput(fieldEl);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (!assignFileWithDataTransfer(fieldEl, file)) {
|
|
263
|
+
// The probe passed but this engine refused the real assignment: drop to
|
|
264
|
+
// the next strategy, which reads `committedFile` at submit time.
|
|
265
|
+
strategy = detectNativeFileFieldStrategy({
|
|
266
|
+
FormDataEvent: (globalThis as Record<string, unknown>).FormDataEvent,
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function capture(): void {
|
|
272
|
+
const video = videoEl;
|
|
273
|
+
if (disabled || !video || !session.stream) return;
|
|
274
|
+
const canvas = document.createElement('canvas');
|
|
275
|
+
canvas.width = video.videoWidth || 640;
|
|
276
|
+
canvas.height = video.videoHeight || 480;
|
|
277
|
+
const context = canvas.getContext('2d');
|
|
278
|
+
if (!context) {
|
|
279
|
+
stopStream();
|
|
280
|
+
cameraState = 'error';
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
284
|
+
const dataUrl = canvas.toDataURL(imageType, quality);
|
|
285
|
+
// The frame is on the canvas: release the camera now, not after encoding.
|
|
286
|
+
stopStream();
|
|
287
|
+
capturing = true;
|
|
288
|
+
const request = ++captureRequest;
|
|
289
|
+
canvas.toBlob(
|
|
290
|
+
(blob) => {
|
|
291
|
+
// A form reset during encoding superseded this frame.
|
|
292
|
+
if (request !== captureRequest) return;
|
|
293
|
+
capturing = false;
|
|
294
|
+
if (!blob) {
|
|
295
|
+
cameraState = 'error';
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
pending = { blob, dataUrl };
|
|
299
|
+
previewUrl = dataUrl;
|
|
300
|
+
cameraState = 'reviewing';
|
|
301
|
+
},
|
|
302
|
+
imageType,
|
|
303
|
+
quality,
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function usePhoto(): void {
|
|
308
|
+
if (disabled || !pending) return;
|
|
309
|
+
const result = pending;
|
|
310
|
+
committedFile = new File([result.blob], resolvedFileName, {
|
|
311
|
+
type: result.blob.type || imageType,
|
|
312
|
+
});
|
|
313
|
+
writeField(committedFile);
|
|
314
|
+
cameraState = 'committed';
|
|
315
|
+
onCapture?.(result);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function retake(): void {
|
|
319
|
+
if (disabled) return;
|
|
320
|
+
const wasCommitted = cameraState === 'committed';
|
|
321
|
+
pending = null;
|
|
322
|
+
previewUrl = '';
|
|
323
|
+
if (wasCommitted) {
|
|
324
|
+
committedFile = null;
|
|
325
|
+
writeField(null);
|
|
326
|
+
onClear?.();
|
|
327
|
+
}
|
|
328
|
+
void startCamera();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* The owning form reset. A native file input is emptied by its form's reset,
|
|
333
|
+
* so the photo goes too, committed or still under review (a stale review would
|
|
334
|
+
* invite attaching the previous entry's photo to the next one), and the
|
|
335
|
+
* component returns to its initial flow: the live camera, `off` while
|
|
336
|
+
* `disabled`, or the emptied fallback picker. It
|
|
337
|
+
* runs even while `disabled`, as a reset empties a disabled native input. With
|
|
338
|
+
* nothing held a reset changes nothing, so it never re-prompts for a camera.
|
|
339
|
+
*/
|
|
340
|
+
function resetCapture(): void {
|
|
341
|
+
const wasCommitted = cameraState === 'committed';
|
|
342
|
+
const wasCapturing = capturing;
|
|
343
|
+
captureRequest += 1;
|
|
344
|
+
capturing = false;
|
|
345
|
+
// Empty the fallback picker as the browser's reset will (the same either
|
|
346
|
+
// way, and still so if a later listener cancels the reset), and drop a read
|
|
347
|
+
// still in flight.
|
|
348
|
+
fallbackReads += 1;
|
|
349
|
+
if (pickerEl) clearFileInput(pickerEl);
|
|
350
|
+
if (!holdsPhoto() && !wasCapturing) return;
|
|
351
|
+
pending = null;
|
|
352
|
+
previewUrl = '';
|
|
353
|
+
if (wasCommitted) {
|
|
354
|
+
committedFile = null;
|
|
355
|
+
writeField(null);
|
|
356
|
+
}
|
|
357
|
+
if (fallbackActive) cameraState = 'fallback';
|
|
358
|
+
else if (disabled) cameraState = 'off';
|
|
359
|
+
else void startCamera();
|
|
360
|
+
if (wasCommitted) onClear?.();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
$effect(() => {
|
|
364
|
+
if (typeof document === 'undefined') return;
|
|
365
|
+
return attachFormResetListener(
|
|
366
|
+
document,
|
|
367
|
+
() => fieldEl?.form ?? pickerEl?.form ?? rootEl?.closest('form'),
|
|
368
|
+
resetCapture,
|
|
369
|
+
);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
function readDataUrl(file: Blob): Promise<string> {
|
|
373
|
+
return new Promise((resolve, reject) => {
|
|
374
|
+
const reader = new FileReader();
|
|
375
|
+
reader.onload = () => resolve(String(reader.result ?? ''));
|
|
376
|
+
reader.onerror = () => reject(reader.error);
|
|
377
|
+
reader.readAsDataURL(file);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* The fallback picker is the field that posts, so `disabled` must not set the
|
|
383
|
+
* native attribute: a disabled control is left out of the submission, and a
|
|
384
|
+
* committed photo still on screen would silently stop posting. Interaction is
|
|
385
|
+
* blocked here instead (a label click and keyboard activation both arrive as
|
|
386
|
+
* a click on the input), with `aria-disabled` and no tab stop.
|
|
387
|
+
*/
|
|
388
|
+
function blockPickerWhileDisabled(event: MouseEvent): void {
|
|
389
|
+
if (disabled) event.preventDefault();
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async function handleFallbackChange(
|
|
393
|
+
event: Event & { currentTarget: HTMLInputElement },
|
|
394
|
+
): Promise<void> {
|
|
395
|
+
const file = event.currentTarget.files?.[0] ?? null;
|
|
396
|
+
const read = ++fallbackReads;
|
|
397
|
+
if (!file) {
|
|
398
|
+
if (cameraState === 'committed') {
|
|
399
|
+
committedFile = null;
|
|
400
|
+
previewUrl = '';
|
|
401
|
+
cameraState = 'fallback';
|
|
402
|
+
onClear?.();
|
|
403
|
+
}
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
let dataUrl = '';
|
|
407
|
+
try {
|
|
408
|
+
dataUrl = await readDataUrl(file);
|
|
409
|
+
} catch {
|
|
410
|
+
// A preview is a convenience; the chosen file still posts and is reported.
|
|
411
|
+
}
|
|
412
|
+
if (read !== fallbackReads) return;
|
|
413
|
+
committedFile = file;
|
|
414
|
+
previewUrl = dataUrl;
|
|
415
|
+
cameraState = 'committed';
|
|
416
|
+
onCapture?.({ blob: file, dataUrl });
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
useControlRegistration(() => {
|
|
420
|
+
const root = rootEl;
|
|
421
|
+
if (!root || interaction === false) return false;
|
|
422
|
+
return {
|
|
423
|
+
controlId,
|
|
424
|
+
subject: interaction?.subject,
|
|
425
|
+
metadata: {
|
|
426
|
+
kind: 'file',
|
|
427
|
+
label: text.region,
|
|
428
|
+
description: interaction?.description,
|
|
429
|
+
sensitivity: interaction?.sensitivity ?? 'sensitive',
|
|
430
|
+
readable: false,
|
|
431
|
+
writable: false,
|
|
432
|
+
capabilities: ['focus', 'reveal', 'highlight', 'explain'],
|
|
433
|
+
},
|
|
434
|
+
focus: () => focusControl(root),
|
|
435
|
+
reveal: () => revealControl(root),
|
|
436
|
+
highlight: (durationMs) => highlightControl(root, durationMs),
|
|
437
|
+
getState: () => ({ disabled }),
|
|
438
|
+
};
|
|
439
|
+
});
|
|
440
|
+
</script>
|
|
441
|
+
|
|
442
|
+
<div
|
|
443
|
+
bind:this={rootEl}
|
|
444
|
+
id={resolvedId}
|
|
445
|
+
class="camera-capture {className}"
|
|
446
|
+
class:disabled
|
|
447
|
+
role="group"
|
|
448
|
+
aria-label={text.region}
|
|
449
|
+
data-state={cameraState}
|
|
450
|
+
data-smrt-file-field={name ? (fallbackActive ? 'file-input' : strategy) : undefined}
|
|
451
|
+
data-smrt-control={controlId}
|
|
452
|
+
data-smrt-form={interactionContext?.formId}
|
|
453
|
+
data-smrt-subject-type={interaction === false ? undefined : interaction?.subject?.type}
|
|
454
|
+
data-smrt-subject-id={interaction === false ? undefined : interaction?.subject?.id}
|
|
455
|
+
>
|
|
456
|
+
<div class="stage" class:empty={cameraState !== 'streaming' && !previewUrl}>
|
|
457
|
+
<video
|
|
458
|
+
bind:this={videoEl}
|
|
459
|
+
class="media"
|
|
460
|
+
class:hidden={cameraState !== 'streaming'}
|
|
461
|
+
aria-label={text.preview}
|
|
462
|
+
autoplay
|
|
463
|
+
playsinline
|
|
464
|
+
muted
|
|
465
|
+
></video>
|
|
466
|
+
{#if previewUrl && (cameraState === 'reviewing' || cameraState === 'committed')}
|
|
467
|
+
<img
|
|
468
|
+
class="media"
|
|
469
|
+
src={previewUrl}
|
|
470
|
+
alt={cameraState === 'committed' ? text.committedAlt : text.reviewAlt}
|
|
471
|
+
/>
|
|
472
|
+
{/if}
|
|
473
|
+
</div>
|
|
474
|
+
|
|
475
|
+
<p class="message" class:problem={isProblem} role="status">{message}</p>
|
|
476
|
+
|
|
477
|
+
<div class="actions">
|
|
478
|
+
{#if cameraState === 'permission-denied' || cameraState === 'no-camera' || cameraState === 'error'}
|
|
479
|
+
<button type="button" class="action" {disabled} onclick={() => void startCamera()}>
|
|
480
|
+
{text.retry}
|
|
481
|
+
</button>
|
|
482
|
+
{:else if cameraState === 'streaming'}
|
|
483
|
+
<button type="button" class="action primary" {disabled} onclick={capture}>
|
|
484
|
+
{text.capture}
|
|
485
|
+
</button>
|
|
486
|
+
{:else if cameraState === 'reviewing'}
|
|
487
|
+
<button type="button" class="action" {disabled} onclick={retake}>{text.retake}</button>
|
|
488
|
+
<button type="button" class="action primary" {disabled} onclick={usePhoto}>
|
|
489
|
+
{text.usePhoto}
|
|
490
|
+
</button>
|
|
491
|
+
{:else if cameraState === 'committed' && !fallbackActive}
|
|
492
|
+
<button type="button" class="action" {disabled} onclick={retake}>{text.retake}</button>
|
|
493
|
+
{/if}
|
|
494
|
+
{#if fallbackActive}
|
|
495
|
+
<label class="action primary picker" class:disabled>
|
|
496
|
+
<input
|
|
497
|
+
bind:this={pickerEl}
|
|
498
|
+
class="visually-hidden"
|
|
499
|
+
type="file"
|
|
500
|
+
accept="image/*"
|
|
501
|
+
capture={facingMode}
|
|
502
|
+
{name}
|
|
503
|
+
aria-disabled={disabled || undefined}
|
|
504
|
+
tabindex={disabled ? -1 : undefined}
|
|
505
|
+
onclick={blockPickerWhileDisabled}
|
|
506
|
+
onchange={handleFallbackChange}
|
|
507
|
+
/>
|
|
508
|
+
<span>{cameraState === 'committed' ? text.retake : text.choosePhoto}</span>
|
|
509
|
+
</label>
|
|
510
|
+
{/if}
|
|
511
|
+
</div>
|
|
512
|
+
|
|
513
|
+
{#if name && !fallbackActive}
|
|
514
|
+
<input
|
|
515
|
+
bind:this={fieldEl}
|
|
516
|
+
class="field"
|
|
517
|
+
type="file"
|
|
518
|
+
hidden
|
|
519
|
+
tabindex="-1"
|
|
520
|
+
name={strategy === 'data-transfer' ? name : undefined}
|
|
521
|
+
data-smrt-capture-field
|
|
522
|
+
/>
|
|
523
|
+
{/if}
|
|
524
|
+
</div>
|
|
525
|
+
|
|
526
|
+
<style>
|
|
527
|
+
.camera-capture {
|
|
528
|
+
display: grid;
|
|
529
|
+
gap: var(--smrt-spacing-3);
|
|
530
|
+
width: 100%;
|
|
531
|
+
max-width: 40rem;
|
|
532
|
+
color: var(--smrt-color-on-surface);
|
|
533
|
+
}
|
|
534
|
+
.stage {
|
|
535
|
+
position: relative;
|
|
536
|
+
display: grid;
|
|
537
|
+
overflow: hidden;
|
|
538
|
+
aspect-ratio: 4 / 3;
|
|
539
|
+
border-radius: var(--smrt-radius-medium);
|
|
540
|
+
background: var(--smrt-color-scrim);
|
|
541
|
+
}
|
|
542
|
+
.stage.empty {
|
|
543
|
+
background: var(--smrt-color-surface-container);
|
|
544
|
+
}
|
|
545
|
+
.media {
|
|
546
|
+
grid-area: 1 / 1;
|
|
547
|
+
width: 100%;
|
|
548
|
+
height: 100%;
|
|
549
|
+
object-fit: cover;
|
|
550
|
+
}
|
|
551
|
+
.media.hidden,
|
|
552
|
+
.field {
|
|
553
|
+
display: none;
|
|
554
|
+
}
|
|
555
|
+
.message {
|
|
556
|
+
margin: 0;
|
|
557
|
+
font: var(--smrt-typography-body-large-font);
|
|
558
|
+
}
|
|
559
|
+
.message:empty {
|
|
560
|
+
display: none;
|
|
561
|
+
}
|
|
562
|
+
.message.problem {
|
|
563
|
+
color: var(--smrt-color-error);
|
|
564
|
+
}
|
|
565
|
+
.actions {
|
|
566
|
+
display: flex;
|
|
567
|
+
flex-wrap: wrap;
|
|
568
|
+
gap: var(--smrt-spacing-3);
|
|
569
|
+
}
|
|
570
|
+
.actions:empty {
|
|
571
|
+
display: none;
|
|
572
|
+
}
|
|
573
|
+
.action {
|
|
574
|
+
position: relative;
|
|
575
|
+
flex: 1 1 auto;
|
|
576
|
+
display: inline-flex;
|
|
577
|
+
align-items: center;
|
|
578
|
+
justify-content: center;
|
|
579
|
+
/* Capture is a touch flow: its actions are touch targets at every
|
|
580
|
+
density, on the shared scale (smrt#3252). */
|
|
581
|
+
min-width: var(--smrt-touch-target-min, 48px);
|
|
582
|
+
min-height: var(--smrt-touch-target-min, 48px);
|
|
583
|
+
padding: 0 var(--smrt-spacing-4);
|
|
584
|
+
border: 1px solid var(--smrt-color-outline);
|
|
585
|
+
border-radius: var(--smrt-radius-small);
|
|
586
|
+
background: var(--smrt-color-surface);
|
|
587
|
+
color: var(--smrt-color-on-surface);
|
|
588
|
+
font: var(--smrt-typography-label-large-font);
|
|
589
|
+
cursor: pointer;
|
|
590
|
+
}
|
|
591
|
+
.action.primary {
|
|
592
|
+
flex-grow: 2;
|
|
593
|
+
border-color: var(--smrt-color-primary);
|
|
594
|
+
background: var(--smrt-color-primary);
|
|
595
|
+
color: var(--smrt-color-on-primary);
|
|
596
|
+
}
|
|
597
|
+
.action:focus-visible,
|
|
598
|
+
.picker:has(input:focus-visible) {
|
|
599
|
+
outline: 2px solid var(--smrt-color-primary);
|
|
600
|
+
outline-offset: 2px;
|
|
601
|
+
}
|
|
602
|
+
.action:disabled,
|
|
603
|
+
.picker.disabled {
|
|
604
|
+
opacity: 0.5;
|
|
605
|
+
cursor: not-allowed;
|
|
606
|
+
}
|
|
607
|
+
.visually-hidden {
|
|
608
|
+
position: absolute;
|
|
609
|
+
width: 1px;
|
|
610
|
+
height: 1px;
|
|
611
|
+
overflow: hidden;
|
|
612
|
+
clip-path: inset(50%);
|
|
613
|
+
white-space: nowrap;
|
|
614
|
+
}
|
|
615
|
+
:global(.camera-capture[data-smrt-highlighted='true']) {
|
|
616
|
+
outline: 3px solid var(--smrt-color-tertiary);
|
|
617
|
+
outline-offset: 4px;
|
|
618
|
+
}
|
|
619
|
+
</style>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { type CameraFacingMode } from './camera-capture-session.js';
|
|
2
|
+
import type { ControlInteractionOptions } from './control-interaction.js';
|
|
3
|
+
import type { CameraCaptureLabels, CaptureResult } from './types.js';
|
|
4
|
+
export interface Props {
|
|
5
|
+
/** Which camera to request. Re-requested on change while live. */
|
|
6
|
+
facingMode?: CameraFacingMode;
|
|
7
|
+
/**
|
|
8
|
+
* Stops the camera and disables every action; a held photo is kept and,
|
|
9
|
+
* when committed, still posts with the form.
|
|
10
|
+
*/
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
/** Called when the user commits a photo with "Use photo". */
|
|
13
|
+
onCapture?: (result: CaptureResult) => void;
|
|
14
|
+
/** Called when a committed photo is discarded by "Retake" or a form reset. */
|
|
15
|
+
onClear?: () => void;
|
|
16
|
+
/** Field name: the committed photo posts as a file in a native form. */
|
|
17
|
+
name?: string;
|
|
18
|
+
/** Posted file name. Defaults to `photo.<ext>` for the image type. */
|
|
19
|
+
fileName?: string;
|
|
20
|
+
/** Encoded image type. */
|
|
21
|
+
imageType?: 'image/jpeg' | 'image/png' | 'image/webp';
|
|
22
|
+
/** Encoder quality for lossy types, 0 to 1. */
|
|
23
|
+
quality?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Opt in to `<input type="file" capture>` for browsers without
|
|
26
|
+
* `getUserMedia`. Off by default: without it those browsers render the
|
|
27
|
+
* unsupported state.
|
|
28
|
+
*/
|
|
29
|
+
fileInputFallback?: boolean;
|
|
30
|
+
/** Text overrides; defaults come from the `ui` i18n catalog. */
|
|
31
|
+
labels?: Partial<CameraCaptureLabels>;
|
|
32
|
+
/** DOM id of the component root. */
|
|
33
|
+
id?: string;
|
|
34
|
+
/** CSS class to apply to the component root. */
|
|
35
|
+
class?: string;
|
|
36
|
+
/** Interaction options or false to disable registration. */
|
|
37
|
+
interaction?: ControlInteractionOptions | false;
|
|
38
|
+
}
|
|
39
|
+
declare const CameraCapture: import("svelte").Component<Props, {}, "">;
|
|
40
|
+
type CameraCapture = ReturnType<typeof CameraCapture>;
|
|
41
|
+
export default CameraCapture;
|
|
42
|
+
//# sourceMappingURL=CameraCapture.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"CameraCapture.svelte.d.ts","sourceRoot":"","sources":["../../../src/components/forms/CameraCapture.svelte.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,KAAK,gBAAgB,EAEtB,MAAM,6BAA6B,CAAC;AAMrC,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,0BAA0B,CAAC;AAW1E,OAAO,KAAK,EACV,mBAAmB,EAEnB,aAAa,EACd,MAAM,YAAY,CAAC;AAIpB,MAAM,WAAW,KAAK;IACpB,kEAAkE;IAClE,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAC5C,8EAA8E;IAC9E,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,0BAA0B;IAC1B,SAAS,CAAC,EAAE,YAAY,GAAG,WAAW,GAAG,YAAY,CAAC;IACtD,+CAA+C;IAC/C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,gEAAgE;IAChE,MAAM,CAAC,EAAE,OAAO,CAAC,mBAAmB,CAAC,CAAC;IACtC,oCAAoC;IACpC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,gDAAgD;IAChD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4DAA4D;IAC5D,WAAW,CAAC,EAAE,yBAAyB,GAAG,KAAK,CAAC;CACjD;AAuZD,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -17,6 +17,8 @@ import { useControlRegistration } from './use-control-registration.svelte.js';
|
|
|
17
17
|
|
|
18
18
|
export interface Props
|
|
19
19
|
extends Omit<HTMLInputAttributes, 'type' | 'class' | 'checked'> {
|
|
20
|
+
/** Target density; omit to inherit the ThemeProvider setting. */
|
|
21
|
+
density?: 'comfortable' | 'touch';
|
|
20
22
|
/** Whether the checkbox is checked (bindable). */
|
|
21
23
|
checked?: boolean;
|
|
22
24
|
/** Whether the checkbox shows indeterminate state. */
|
|
@@ -43,6 +45,7 @@ let {
|
|
|
43
45
|
onchange,
|
|
44
46
|
'aria-label': ariaLabel,
|
|
45
47
|
'aria-describedby': ariaDescribedby,
|
|
48
|
+
density,
|
|
46
49
|
...rest
|
|
47
50
|
}: Props = $props();
|
|
48
51
|
|
|
@@ -121,7 +124,7 @@ useControlRegistration(() => {
|
|
|
121
124
|
</script>
|
|
122
125
|
|
|
123
126
|
<label
|
|
124
|
-
class="checkbox {className}"
|
|
127
|
+
data-density={density} class="checkbox {className}"
|
|
125
128
|
class:checkbox--disabled={disabled}
|
|
126
129
|
data-smrt-hit-target
|
|
127
130
|
data-smrt-control={controlId}
|
|
@@ -184,4 +187,8 @@ useControlRegistration(() => {
|
|
|
184
187
|
:global(:is(td, th):has(> [data-smrt-hit-target]:only-child)) > .checkbox::before { inset: 0; inline-size: auto; block-size: auto; margin: 0; }
|
|
185
188
|
|
|
186
189
|
@media (prefers-reduced-motion: reduce) { .checkbox__box { transition: none; } }
|
|
190
|
+
|
|
191
|
+
.checkbox[data-density='touch'] { --smrt-control-target-min: var(--smrt-touch-target-min, 48px); }
|
|
192
|
+
.checkbox[data-density='comfortable'] { --smrt-control-target-min: 0px; }
|
|
193
|
+
.checkbox { min-height: var(--smrt-control-target-min, 0px); min-width: var(--smrt-control-target-min, 0px); }
|
|
187
194
|
</style>
|