@multiplatform.one/forms 7.5.0 → 7.6.1
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/cjs/Button/index.cjs +6 -10
- package/dist/cjs/Button/index.native.js +6 -10
- package/dist/cjs/Button/index.native.js.map +1 -1
- package/dist/cjs/ContextualSaveBar/UnsavedChangesDialog.cjs +0 -1
- package/dist/cjs/ContextualSaveBar/UnsavedChangesDialog.native.js +0 -1
- package/dist/cjs/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
- package/dist/cjs/FieldDisplay/index.cjs +2 -1
- package/dist/cjs/FieldDisplay/index.native.js +9 -2
- package/dist/cjs/FieldDisplay/index.native.js.map +1 -1
- package/dist/cjs/FloatingPanel/index.cjs +8 -1
- package/dist/cjs/InputParts/index.cjs +64 -2
- package/dist/cjs/InputParts/index.native.js +70 -4
- package/dist/cjs/InputParts/index.native.js.map +1 -1
- package/dist/cjs/fields/Barcode/BarcodePreview.cjs +15 -9
- package/dist/cjs/fields/Barcode/BarcodePreview.native.js +15 -9
- package/dist/cjs/fields/Barcode/BarcodePreview.native.js.map +1 -1
- package/dist/cjs/fields/Combobox/index.cjs +14 -11
- package/dist/cjs/fields/Combobox/index.native.js +16 -11
- package/dist/cjs/fields/Combobox/index.native.js.map +1 -1
- package/dist/cjs/fields/FileUpload/index.cjs +66 -5
- package/dist/cjs/fields/FileUpload/index.native.js +85 -5
- package/dist/cjs/fields/FileUpload/index.native.js.map +1 -1
- package/dist/cjs/fields/Geolocation/geoValue.cjs +122 -0
- package/dist/cjs/fields/Geolocation/geoValue.native.js +151 -0
- package/dist/cjs/fields/Geolocation/geoValue.native.js.map +1 -0
- package/dist/cjs/fields/Geolocation/index.cjs +16 -29
- package/dist/cjs/fields/Geolocation/index.native.js +16 -29
- package/dist/cjs/fields/Geolocation/index.native.js.map +1 -1
- package/dist/cjs/fields/Icon/index.cjs +1 -0
- package/dist/cjs/fields/Icon/index.native.js +1 -0
- package/dist/cjs/fields/Icon/index.native.js.map +1 -1
- package/dist/cjs/fields/Input/index.cjs +175 -95
- package/dist/cjs/fields/Input/index.native.js +83 -4
- package/dist/cjs/fields/Input/index.native.js.map +1 -1
- package/dist/cjs/fields/MentionInput/index.cjs +6 -1
- package/dist/cjs/fields/MentionInput/index.native.js +6 -1
- package/dist/cjs/fields/MentionInput/index.native.js.map +1 -1
- package/dist/cjs/fields/OTPInput/index.cjs +7 -2
- package/dist/cjs/fields/OTPInput/index.native.js +7 -2
- package/dist/cjs/fields/OTPInput/index.native.js.map +1 -1
- package/dist/cjs/fields/PhoneInput/index.native.js +1 -0
- package/dist/cjs/fields/PhoneInput/index.native.js.map +1 -1
- package/dist/cjs/fields/SearchInput/index.native.js.map +1 -1
- package/dist/cjs/shared/utils.cjs +4 -14
- package/dist/cjs/shared/utils.native.js +6 -14
- package/dist/cjs/shared/utils.native.js.map +1 -1
- package/dist/esm/Button/index.mjs +6 -9
- package/dist/esm/Button/index.mjs.map +1 -1
- package/dist/esm/Button/index.native.js +6 -9
- package/dist/esm/Button/index.native.js.map +1 -1
- package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.mjs +0 -1
- package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.mjs.map +1 -1
- package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.native.js +0 -1
- package/dist/esm/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
- package/dist/esm/FieldDisplay/index.mjs +2 -1
- package/dist/esm/FieldDisplay/index.mjs.map +1 -1
- package/dist/esm/FieldDisplay/index.native.js +9 -2
- package/dist/esm/FieldDisplay/index.native.js.map +1 -1
- package/dist/esm/FloatingPanel/index.mjs +8 -1
- package/dist/esm/FloatingPanel/index.mjs.map +1 -1
- package/dist/esm/InputParts/index.mjs +64 -2
- package/dist/esm/InputParts/index.mjs.map +1 -1
- package/dist/esm/InputParts/index.native.js +70 -4
- package/dist/esm/InputParts/index.native.js.map +1 -1
- package/dist/esm/fields/Barcode/BarcodePreview.mjs +15 -9
- package/dist/esm/fields/Barcode/BarcodePreview.mjs.map +1 -1
- package/dist/esm/fields/Barcode/BarcodePreview.native.js +15 -9
- package/dist/esm/fields/Barcode/BarcodePreview.native.js.map +1 -1
- package/dist/esm/fields/Combobox/index.mjs +14 -11
- package/dist/esm/fields/Combobox/index.mjs.map +1 -1
- package/dist/esm/fields/Combobox/index.native.js +16 -11
- package/dist/esm/fields/Combobox/index.native.js.map +1 -1
- package/dist/esm/fields/FileUpload/index.mjs +66 -5
- package/dist/esm/fields/FileUpload/index.mjs.map +1 -1
- package/dist/esm/fields/FileUpload/index.native.js +85 -5
- package/dist/esm/fields/FileUpload/index.native.js.map +1 -1
- package/dist/esm/fields/Geolocation/geoValue.mjs +95 -0
- package/dist/esm/fields/Geolocation/geoValue.mjs.map +1 -0
- package/dist/esm/fields/Geolocation/geoValue.native.js +121 -0
- package/dist/esm/fields/Geolocation/geoValue.native.js.map +1 -0
- package/dist/esm/fields/Geolocation/index.mjs +15 -28
- package/dist/esm/fields/Geolocation/index.mjs.map +1 -1
- package/dist/esm/fields/Geolocation/index.native.js +15 -28
- package/dist/esm/fields/Geolocation/index.native.js.map +1 -1
- package/dist/esm/fields/Icon/index.mjs +1 -0
- package/dist/esm/fields/Icon/index.mjs.map +1 -1
- package/dist/esm/fields/Icon/index.native.js +1 -0
- package/dist/esm/fields/Icon/index.native.js.map +1 -1
- package/dist/esm/fields/Input/index.mjs +177 -98
- package/dist/esm/fields/Input/index.mjs.map +1 -1
- package/dist/esm/fields/Input/index.native.js +85 -7
- package/dist/esm/fields/Input/index.native.js.map +1 -1
- package/dist/esm/fields/MentionInput/index.mjs +6 -1
- package/dist/esm/fields/MentionInput/index.mjs.map +1 -1
- package/dist/esm/fields/MentionInput/index.native.js +6 -1
- package/dist/esm/fields/MentionInput/index.native.js.map +1 -1
- package/dist/esm/fields/OTPInput/index.mjs +7 -3
- package/dist/esm/fields/OTPInput/index.mjs.map +1 -1
- package/dist/esm/fields/OTPInput/index.native.js +7 -3
- package/dist/esm/fields/OTPInput/index.native.js.map +1 -1
- package/dist/esm/fields/PhoneInput/index.mjs.map +1 -1
- package/dist/esm/fields/PhoneInput/index.native.js +1 -0
- package/dist/esm/fields/PhoneInput/index.native.js.map +1 -1
- package/dist/esm/fields/SearchInput/index.mjs.map +1 -1
- package/dist/esm/fields/SearchInput/index.native.js.map +1 -1
- package/dist/esm/shared/utils.mjs +4 -14
- package/dist/esm/shared/utils.mjs.map +1 -1
- package/dist/esm/shared/utils.native.js +6 -14
- package/dist/esm/shared/utils.native.js.map +1 -1
- package/dist/jsx/Button/index.mjs +6 -9
- package/dist/jsx/Button/index.mjs.map +1 -1
- package/dist/jsx/Button/index.native.js +6 -10
- package/dist/jsx/Button/index.native.js.map +1 -1
- package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.mjs +0 -1
- package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.mjs.map +1 -1
- package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.native.js +0 -1
- package/dist/jsx/ContextualSaveBar/UnsavedChangesDialog.native.js.map +1 -1
- package/dist/jsx/FieldDisplay/index.mjs +2 -1
- package/dist/jsx/FieldDisplay/index.mjs.map +1 -1
- package/dist/jsx/FieldDisplay/index.native.js +9 -2
- package/dist/jsx/FieldDisplay/index.native.js.map +1 -1
- package/dist/jsx/FloatingPanel/index.mjs +8 -1
- package/dist/jsx/FloatingPanel/index.mjs.map +1 -1
- package/dist/jsx/InputParts/index.mjs +64 -2
- package/dist/jsx/InputParts/index.mjs.map +1 -1
- package/dist/jsx/InputParts/index.native.js +70 -4
- package/dist/jsx/InputParts/index.native.js.map +1 -1
- package/dist/jsx/fields/Barcode/BarcodePreview.mjs +15 -9
- package/dist/jsx/fields/Barcode/BarcodePreview.mjs.map +1 -1
- package/dist/jsx/fields/Barcode/BarcodePreview.native.js +15 -9
- package/dist/jsx/fields/Barcode/BarcodePreview.native.js.map +1 -1
- package/dist/jsx/fields/Combobox/index.mjs +14 -11
- package/dist/jsx/fields/Combobox/index.mjs.map +1 -1
- package/dist/jsx/fields/Combobox/index.native.js +16 -11
- package/dist/jsx/fields/Combobox/index.native.js.map +1 -1
- package/dist/jsx/fields/FileUpload/index.mjs +66 -5
- package/dist/jsx/fields/FileUpload/index.mjs.map +1 -1
- package/dist/jsx/fields/FileUpload/index.native.js +85 -5
- package/dist/jsx/fields/FileUpload/index.native.js.map +1 -1
- package/dist/jsx/fields/Geolocation/geoValue.mjs +95 -0
- package/dist/jsx/fields/Geolocation/geoValue.mjs.map +1 -0
- package/dist/jsx/fields/Geolocation/geoValue.native.js +151 -0
- package/dist/jsx/fields/Geolocation/geoValue.native.js.map +1 -0
- package/dist/jsx/fields/Geolocation/index.mjs +15 -28
- package/dist/jsx/fields/Geolocation/index.mjs.map +1 -1
- package/dist/jsx/fields/Geolocation/index.native.js +16 -29
- package/dist/jsx/fields/Geolocation/index.native.js.map +1 -1
- package/dist/jsx/fields/Icon/index.mjs +1 -0
- package/dist/jsx/fields/Icon/index.mjs.map +1 -1
- package/dist/jsx/fields/Icon/index.native.js +1 -0
- package/dist/jsx/fields/Icon/index.native.js.map +1 -1
- package/dist/jsx/fields/Input/index.mjs +177 -98
- package/dist/jsx/fields/Input/index.mjs.map +1 -1
- package/dist/jsx/fields/Input/index.native.js +83 -4
- package/dist/jsx/fields/Input/index.native.js.map +1 -1
- package/dist/jsx/fields/MentionInput/index.mjs +6 -1
- package/dist/jsx/fields/MentionInput/index.mjs.map +1 -1
- package/dist/jsx/fields/MentionInput/index.native.js +6 -1
- package/dist/jsx/fields/MentionInput/index.native.js.map +1 -1
- package/dist/jsx/fields/OTPInput/index.mjs +7 -3
- package/dist/jsx/fields/OTPInput/index.mjs.map +1 -1
- package/dist/jsx/fields/OTPInput/index.native.js +7 -2
- package/dist/jsx/fields/OTPInput/index.native.js.map +1 -1
- package/dist/jsx/fields/PhoneInput/index.mjs.map +1 -1
- package/dist/jsx/fields/PhoneInput/index.native.js +1 -0
- package/dist/jsx/fields/PhoneInput/index.native.js.map +1 -1
- package/dist/jsx/fields/SearchInput/index.mjs.map +1 -1
- package/dist/jsx/fields/SearchInput/index.native.js.map +1 -1
- package/dist/jsx/shared/utils.mjs +4 -14
- package/dist/jsx/shared/utils.mjs.map +1 -1
- package/dist/jsx/shared/utils.native.js +6 -14
- package/dist/jsx/shared/utils.native.js.map +1 -1
- package/package.json +7 -7
- package/src/Button/Button.density.spec.tsx +118 -32
- package/src/Button/Button.labelInk.spec.tsx +72 -19
- package/src/Button/Button.lc71.spec.tsx +10 -3
- package/src/Button/Button.stories.tsx +97 -7
- package/src/Button/index.tsx +22 -12
- package/src/ContextualSaveBar/UnsavedChangesDialog.tsx +0 -1
- package/src/FieldDisplay/FieldDisplay.spec.tsx +11 -2
- package/src/FieldDisplay/index.tsx +5 -0
- package/src/FloatingPanel/index.tsx +22 -1
- package/src/FloatingPanel/panelWidthContract.spec.ts +1 -0
- package/src/InputParts/index.tsx +100 -3
- package/src/fields/Barcode/BarcodePreview.spec.tsx +153 -5
- package/src/fields/Barcode/BarcodePreview.stories.tsx +104 -0
- package/src/fields/Barcode/BarcodePreview.tsx +47 -15
- package/src/fields/ColorPicker/PresetSwatches.spec.tsx +71 -0
- package/src/fields/ColorPicker/PresetSwatches.stories.tsx +94 -0
- package/src/fields/Combobox/Combobox.spec.tsx +64 -1
- package/src/fields/Combobox/index.tsx +22 -11
- package/src/fields/FileUpload/FileUpload.spec.tsx +71 -0
- package/src/fields/FileUpload/index.tsx +100 -3
- package/src/fields/Geolocation/Geolocation.spec.tsx +126 -0
- package/src/fields/Geolocation/geoValue.spec.ts +169 -0
- package/src/fields/Geolocation/geoValue.ts +149 -0
- package/src/fields/Geolocation/index.tsx +15 -24
- package/src/fields/Icon/Icon.stories.tsx +1 -1
- package/src/fields/Icon/index.tsx +1 -0
- package/src/fields/Input/index.tsx +180 -81
- package/src/fields/MentionInput/index.tsx +9 -1
- package/src/fields/OTPInput/OtpInput.spec.tsx +36 -4
- package/src/fields/OTPInput/index.tsx +18 -3
- package/src/fields/OTPInput/otpCellHeightTouch.spec.ts +44 -0
- package/src/fields/PhoneInput/index.tsx +1 -0
- package/src/fields/SearchInput/index.tsx +5 -3
- package/src/shared/utils.spec.ts +46 -0
- package/src/shared/utils.ts +31 -16
- package/types/Button/index.d.ts +8 -3
- package/types/Button/index.d.ts.map +1 -1
- package/types/ContextualSaveBar/UnsavedChangesDialog.d.ts.map +1 -1
- package/types/FieldDisplay/index.d.ts.map +1 -1
- package/types/InputParts/index.d.ts +9 -5
- package/types/InputParts/index.d.ts.map +1 -1
- package/types/fields/Barcode/BarcodePreview.d.ts +20 -9
- package/types/fields/Barcode/BarcodePreview.d.ts.map +1 -1
- package/types/fields/Checkbox/index.d.ts +3 -3
- package/types/fields/Combobox/index.d.ts.map +1 -1
- package/types/fields/FileUpload/index.d.ts.map +1 -1
- package/types/fields/Geolocation/geoValue.d.ts +45 -0
- package/types/fields/Geolocation/geoValue.d.ts.map +1 -0
- package/types/fields/Geolocation/index.d.ts.map +1 -1
- package/types/fields/Icon/index.d.ts.map +1 -1
- package/types/fields/Input/index.d.ts +18 -6
- package/types/fields/Input/index.d.ts.map +1 -1
- package/types/fields/MentionInput/index.d.ts.map +1 -1
- package/types/fields/OTPInput/index.d.ts +12 -0
- package/types/fields/OTPInput/index.d.ts.map +1 -1
- package/types/fields/PhoneInput/index.d.ts.map +1 -1
- package/types/fields/RadioGroup/index.d.ts +2 -2
- package/types/fields/SearchInput/index.d.ts.map +1 -1
- package/types/fields/ToggleGroup/index.d.ts +1 -1
- package/types/shared/utils.d.ts.map +1 -1
|
@@ -119,11 +119,20 @@ describe("FieldDisplay", () => {
|
|
|
119
119
|
expect(root?.getAttribute("data-field")).toBe("datepicker");
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
/**
|
|
123
|
+
* The qr format reaches BarcodePreview (the round trip is asserted there),
|
|
124
|
+
* but a display cell's bound is the glyph size — around 20px — and a QR
|
|
125
|
+
* needs one WHOLE pixel per module, 29 minimum. So the cell draws no matrix
|
|
126
|
+
* and, because it already prints the value beside the symbol, contributes
|
|
127
|
+
* nothing rather than a second copy of the string (textFallback={false}).
|
|
128
|
+
* Growing the row to fit a scannable square is the thing LC-06 forbids.
|
|
129
|
+
*/
|
|
130
|
+
it("prints the barcode value once and draws no unreadable QR at a display bound", () => {
|
|
123
131
|
const result = renderWithProviders(
|
|
124
132
|
<FieldDisplay field="barcode" value="SKU-001234" fieldProps={{ formats: ["qr"] }} />,
|
|
125
133
|
);
|
|
126
|
-
expect(result.container.querySelector('[data-testid="barcode-preview-qr"]')).
|
|
134
|
+
expect(result.container.querySelector('[data-testid="barcode-preview-qr"]')).toBeNull();
|
|
127
135
|
expect(result.container.querySelector("canvas")).toBeNull();
|
|
136
|
+
expect(result.container.textContent?.split("SKU-001234").length).toBe(2); // exactly once
|
|
128
137
|
});
|
|
129
138
|
});
|
|
@@ -563,11 +563,16 @@ function BarcodeDisplay({ value, fieldProps }: { value: any; fieldProps?: Record
|
|
|
563
563
|
return (
|
|
564
564
|
<XStack alignItems="center" minWidth={0} flexShrink={1} {...knobProps.gap}>
|
|
565
565
|
<ValueText mono>{text}</ValueText>
|
|
566
|
+
{/* The value is already printed beside the symbol, so an undrawable one
|
|
567
|
+
contributes NOTHING rather than a second copy of the string. At this
|
|
568
|
+
bound that is every QR past a short value at a large size — a display
|
|
569
|
+
cell cannot hold 33 whole-pixel modules without growing the row. */}
|
|
566
570
|
<BarcodePreview
|
|
567
571
|
value={text}
|
|
568
572
|
height={Math.max(12, glyphSize)}
|
|
569
573
|
moduleWidth={1}
|
|
570
574
|
format={typeof format === "string" ? format : undefined}
|
|
575
|
+
textFallback={false}
|
|
571
576
|
/>
|
|
572
577
|
</XStack>
|
|
573
578
|
);
|
|
@@ -607,6 +607,18 @@ function FloatingPanelWeb({
|
|
|
607
607
|
// animation=none → no CSS tween (same contract as knobProps.transition === undefined)
|
|
608
608
|
const webTransition = panel.knobProps.transition ? panelTransition : "none";
|
|
609
609
|
|
|
610
|
+
// `elevatedSurface` carries a web-only `style: { boxShadow }` (MPO-69,
|
|
611
|
+
// d326cc0c2). Two `style` props on one element do NOT merge — the later
|
|
612
|
+
// spread replaces the earlier — so spreading the complete fragment after the
|
|
613
|
+
// positioning style silently dropped `position: fixed`, `top`, `left`,
|
|
614
|
+
// `zIndex` and `overflow`, and every web FloatingPanel rendered in normal
|
|
615
|
+
// flow, pushing the page down instead of floating over it. Measured on
|
|
616
|
+
// origin/main 048289071: DatePicker's panel sat at trigger.bottom + 5.39 (the
|
|
617
|
+
// field container's flex gap), not the contract's 0.
|
|
618
|
+
// The complete fragment still goes on the element (AGENTS.md) — its style is
|
|
619
|
+
// folded into the positioning style rather than competing with it.
|
|
620
|
+
const { style: elevatedSurfaceStyle, ...elevatedSurfaceProps } = panel.knobProps.elevatedSurface;
|
|
621
|
+
|
|
610
622
|
// Same slot problem as the sheet branch, but only half of it lands here. The
|
|
611
623
|
// POINTER path survives, because `useClick(..., { event: "mousedown" })`
|
|
612
624
|
// listens on mousedown and Tamagui only stops the click. The KEYBOARD path
|
|
@@ -650,6 +662,13 @@ function FloatingPanelWeb({
|
|
|
650
662
|
})}
|
|
651
663
|
cursor={disabled ? "not-allowed" : "pointer"}
|
|
652
664
|
opacity={disabled ? 0.5 : 1}
|
|
665
|
+
// The cover contract is stated as a relation between the panel's top
|
|
666
|
+
// edge and THIS element's bottom edge — the box floating-ui anchors
|
|
667
|
+
// to, which is the whole trigger, not whatever glyph inside it was
|
|
668
|
+
// clicked. Marking it keeps the measurement unambiguous (a ColorPicker
|
|
669
|
+
// read from its swatch measures the swatch's inset, not the gap).
|
|
670
|
+
// Same idiom TreeSelect already uses for `data-fp-panel`.
|
|
671
|
+
data-fp-reference=""
|
|
653
672
|
>
|
|
654
673
|
{trigger}
|
|
655
674
|
</View>
|
|
@@ -659,6 +678,7 @@ function FloatingPanelWeb({
|
|
|
659
678
|
{...getFloatingProps({
|
|
660
679
|
ref: panel.refs.setFloating,
|
|
661
680
|
style: {
|
|
681
|
+
...elevatedSurfaceStyle,
|
|
662
682
|
position: panel.strategy,
|
|
663
683
|
top: overlayStyleRef.current?.top ?? panel.y ?? 0,
|
|
664
684
|
left: panel.x ?? 0,
|
|
@@ -675,9 +695,10 @@ function FloatingPanelWeb({
|
|
|
675
695
|
onScroll: handleScroll,
|
|
676
696
|
})}
|
|
677
697
|
data-testid="floating-panel-viewport"
|
|
698
|
+
data-fp-panel=""
|
|
678
699
|
{...panelChromeFocus}
|
|
679
700
|
pointerEvents={panel.visible ? "auto" : "none"}
|
|
680
|
-
{...
|
|
701
|
+
{...elevatedSurfaceProps}
|
|
681
702
|
// Complete elevatedSurface fragment above is the ONLY surface source
|
|
682
703
|
// (SF-OVERLAY "including border and elevation", Round 2 docket).
|
|
683
704
|
// The one layered exception is anatomy, not surface: the dropdown
|
package/src/InputParts/index.tsx
CHANGED
|
@@ -819,6 +819,7 @@ const InputAreaImpl = InputFrame.styleable<{
|
|
|
819
819
|
{...knobProps.body}
|
|
820
820
|
type={secureTextEntry ? "password" : props.type}
|
|
821
821
|
padding={0}
|
|
822
|
+
// size-recipe-escape: zero-reset — Input.Box owns the inset (LC-71); the inner field paints no padding
|
|
822
823
|
paddingHorizontal={0}
|
|
823
824
|
paddingVertical={0}
|
|
824
825
|
keyboardType={resolvedKeyboardType}
|
|
@@ -907,24 +908,115 @@ const InputButton = styled(TButton, {
|
|
|
907
908
|
} as const,
|
|
908
909
|
});
|
|
909
910
|
|
|
910
|
-
|
|
911
|
+
/**
|
|
912
|
+
* LC-71 RING-ANATOMY / LC-49 ONE-TAB-STOP — the `glyphRing` end cap.
|
|
913
|
+
*
|
|
914
|
+
* A bare Input.Button is DECORATIVE: the Box rings for it and it takes no
|
|
915
|
+
* tab stop of its own (Stepper carets, Select/Combobox chevrons). Pass
|
|
916
|
+
* `glyphRing` to promote it to the field's SECOND focusable — the Password
|
|
917
|
+
* reveal, the CopyField copy — where it must ring its own GLYPH, not the
|
|
918
|
+
* 44px hit box. A band on the box would double with the Box's composite
|
|
919
|
+
* ring; a band on the glyph is the chip-dismiss recipe
|
|
920
|
+
* (`.mp-chip-dismiss` + `.mp-chip-dismiss-ring`, keyboardFocusRing.ts).
|
|
921
|
+
*
|
|
922
|
+
* It renders a View rather than TButton because TButton swallows a consumer
|
|
923
|
+
* className and pins its own tabIndex, so the ring class never reaches the
|
|
924
|
+
* node that actually receives focus. SearchInput hand-rolled exactly this
|
|
925
|
+
* pair for that reason (fields/SearchInput/index.tsx:348-380) and left a
|
|
926
|
+
* note saying the classes "cannot ride Input.Button until glyphRing" lands.
|
|
927
|
+
* This is that pair made reusable, so the square comes from the size recipe
|
|
928
|
+
* (LC-70) instead of every call site measuring its own.
|
|
929
|
+
*/
|
|
930
|
+
const InputGlyphButton = styled(View, {
|
|
931
|
+
name: "InputGlyphButton",
|
|
932
|
+
context: InputContext,
|
|
933
|
+
justifyContent: "center",
|
|
934
|
+
alignItems: "center",
|
|
935
|
+
backgroundColor: "transparent",
|
|
936
|
+
borderWidth: 0,
|
|
937
|
+
borderRadius: 0,
|
|
938
|
+
cursor: "pointer",
|
|
939
|
+
flexShrink: 0,
|
|
940
|
+
outlineWidth: 0,
|
|
941
|
+
focusStyle: { outlineWidth: 0 },
|
|
942
|
+
focusVisibleStyle: { outlineWidth: 0 },
|
|
943
|
+
|
|
944
|
+
variants: {
|
|
945
|
+
scaleIcon: {
|
|
946
|
+
":number": {} as any,
|
|
947
|
+
},
|
|
948
|
+
size: {
|
|
949
|
+
...sizeRecipeEndCapVariants,
|
|
950
|
+
"...size": sizeRecipeEndCapFallback,
|
|
951
|
+
},
|
|
952
|
+
} as const,
|
|
953
|
+
});
|
|
954
|
+
|
|
955
|
+
const InputButtonImpl = InputButton.styleable<{ glyphRing?: boolean }>((buttonProps, ref) => {
|
|
911
956
|
const {
|
|
912
957
|
children,
|
|
913
958
|
color: colorProp,
|
|
959
|
+
glyphRing,
|
|
960
|
+
size: sizeProp,
|
|
961
|
+
tabIndex: tabIndexProp,
|
|
962
|
+
className,
|
|
914
963
|
...props
|
|
915
964
|
} = buttonProps as typeof buttonProps & {
|
|
916
965
|
color?: string;
|
|
966
|
+
glyphRing?: boolean;
|
|
967
|
+
size?: FontSizeTokens;
|
|
968
|
+
tabIndex?: number;
|
|
969
|
+
className?: string;
|
|
917
970
|
};
|
|
918
|
-
const { size, color: contextColor } = InputContext.useStyledContext();
|
|
971
|
+
const { size: contextSize, color: contextColor } = InputContext.useStyledContext();
|
|
919
972
|
const { knobProps } = useResolvedKnobs();
|
|
920
973
|
const theme = useTheme();
|
|
921
|
-
const
|
|
974
|
+
const size = (sizeProp ?? contextSize) as FontSizeTokens;
|
|
922
975
|
const paint = resolveGlyphPaint(theme as never, colorProp ?? contextColor);
|
|
923
976
|
const getThemedIcon = useGetThemedIcon({
|
|
924
977
|
size: knobProps.controlIcon.width,
|
|
925
978
|
color: paint as never,
|
|
926
979
|
});
|
|
927
980
|
|
|
981
|
+
if (glyphRing) {
|
|
982
|
+
// A real tab stop must square off the token it was HANDED, not the
|
|
983
|
+
// knob's: a $2 field inside a $4 knob would otherwise ship a 44px stop
|
|
984
|
+
// on a 32px frame. LC-77 keeps the sub-floor case pressable via the
|
|
985
|
+
// caller's hitSlop, never by inflating the cap.
|
|
986
|
+
const square = sizeRecipeForToken(String(size ?? "$true"), { touch: isTouchSurface() }).height;
|
|
987
|
+
const disabled = (props as { disabled?: boolean }).disabled === true;
|
|
988
|
+
if (isWeb) ensureCompositeFocusRing();
|
|
989
|
+
return (
|
|
990
|
+
<InputGlyphButton
|
|
991
|
+
ref={ref as never}
|
|
992
|
+
role="button"
|
|
993
|
+
size={size}
|
|
994
|
+
height={square}
|
|
995
|
+
width={square}
|
|
996
|
+
paddingHorizontal={0} // size-recipe-escape: square end-cap; pad lives on Input.Box
|
|
997
|
+
opacity={disabled ? 0.5 : 1}
|
|
998
|
+
{...props}
|
|
999
|
+
aria-disabled={disabled || undefined}
|
|
1000
|
+
tabIndex={tabIndexProp ?? (disabled ? -1 : 0)}
|
|
1001
|
+
className={
|
|
1002
|
+
isWeb
|
|
1003
|
+
? ["mp-chip-dismiss", className].filter(Boolean).join(" ")
|
|
1004
|
+
: (className ?? undefined)
|
|
1005
|
+
}
|
|
1006
|
+
data-end-cap={square}
|
|
1007
|
+
>
|
|
1008
|
+
<View
|
|
1009
|
+
className={isWeb ? "mp-chip-dismiss-ring" : undefined}
|
|
1010
|
+
alignItems="center"
|
|
1011
|
+
justifyContent="center"
|
|
1012
|
+
>
|
|
1013
|
+
{children}
|
|
1014
|
+
</View>
|
|
1015
|
+
</InputGlyphButton>
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const square = knobProps.control.height;
|
|
928
1020
|
return (
|
|
929
1021
|
<InputButton
|
|
930
1022
|
ref={ref}
|
|
@@ -933,7 +1025,12 @@ const InputButtonImpl = InputButton.styleable((buttonProps, ref) => {
|
|
|
933
1025
|
width={square}
|
|
934
1026
|
paddingHorizontal={0} // size-recipe-escape: square end-cap; pad lives on Input.Box
|
|
935
1027
|
color={paint}
|
|
1028
|
+
// LC-49: the decorative cap is not a tab stop. tabIndex alone is not
|
|
1029
|
+
// enough — TButton re-derives focusability and stamps tabindex="0".
|
|
1030
|
+
focusable={false}
|
|
936
1031
|
tabIndex={-1}
|
|
1032
|
+
className={className}
|
|
1033
|
+
data-end-cap={square}
|
|
937
1034
|
{...props}
|
|
938
1035
|
>
|
|
939
1036
|
{getThemedIcon(children)}
|
|
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
import { renderWithProviders } from "@multiplatform.one/test-utils";
|
|
3
3
|
import { BarcodePreview, barcodePreviewSymbology } from "./BarcodePreview";
|
|
4
4
|
import { countModules, encodeCode128B } from "./code128";
|
|
5
|
-
import { encodeQr } from "./qr";
|
|
5
|
+
import { QR_QUIET_ZONE_MODULES, encodeQr } from "./qr";
|
|
6
6
|
import { resolveHtml5FormatIds, resolveNativeBarcodeTypes } from "./scannerTypes";
|
|
7
7
|
|
|
8
8
|
/** Rebuild the module string ("1" = bar) from the encoder's runs. */
|
|
@@ -101,8 +101,27 @@ describe("barcodePreviewSymbology — scanner round trip", () => {
|
|
|
101
101
|
});
|
|
102
102
|
|
|
103
103
|
describe("BarcodePreview — qr format", () => {
|
|
104
|
+
/** The field's own default bound (Barcode/index.tsx): 36 comfortable, 24 compact. */
|
|
105
|
+
const formHeight = 36;
|
|
106
|
+
|
|
107
|
+
it("QR modules paint $color on $background (themed polarity; MPO-94 is not this lane)", () => {
|
|
108
|
+
const result = renderWithProviders(<BarcodePreview value="SHC-42" format="qr" height={120} />);
|
|
109
|
+
const square = result.container.querySelector(
|
|
110
|
+
'[data-testid="barcode-preview-qr"]',
|
|
111
|
+
) as HTMLElement | null;
|
|
112
|
+
expect(square).not.toBeNull();
|
|
113
|
+
expect(square!.className).toContain("_bg-background");
|
|
114
|
+
const darkModule = [...square!.querySelectorAll("*")].find((el) =>
|
|
115
|
+
(el as HTMLElement).className?.toString().includes("_bg-color"),
|
|
116
|
+
) as HTMLElement | undefined;
|
|
117
|
+
expect(darkModule).toBeTruthy();
|
|
118
|
+
expect(darkModule!.className).not.toMatch(/_bg-(?:white|black|color12)\b/);
|
|
119
|
+
});
|
|
120
|
+
|
|
104
121
|
it("draws the QR matrix canvas-free (labeled image, real module rows)", () => {
|
|
105
|
-
const result = renderWithProviders(
|
|
122
|
+
const result = renderWithProviders(
|
|
123
|
+
<BarcodePreview value="SKU-001234" format="qr" height={formHeight} />,
|
|
124
|
+
);
|
|
106
125
|
const preview = result.container.querySelector('[data-testid="barcode-preview"]');
|
|
107
126
|
expect(preview).not.toBeNull();
|
|
108
127
|
expect(preview?.getAttribute("aria-label")).toContain("SKU-001234");
|
|
@@ -114,12 +133,16 @@ describe("BarcodePreview — qr format", () => {
|
|
|
114
133
|
});
|
|
115
134
|
|
|
116
135
|
it("draws QR for the Frappe fieldtype's qrcode token via the shared alias table", () => {
|
|
117
|
-
const result = renderWithProviders(
|
|
136
|
+
const result = renderWithProviders(
|
|
137
|
+
<BarcodePreview value="SKU-001234" format="qrcode" height={formHeight} />,
|
|
138
|
+
);
|
|
118
139
|
expect(result.container.querySelector('[data-testid="barcode-preview-qr"]')).not.toBeNull();
|
|
119
140
|
});
|
|
120
141
|
|
|
121
142
|
it("draws values the 1-D strip cannot encode (the closed asymmetry)", () => {
|
|
122
|
-
const result = renderWithProviders(
|
|
143
|
+
const result = renderWithProviders(
|
|
144
|
+
<BarcodePreview value="héllo" format="qr" height={formHeight} />,
|
|
145
|
+
);
|
|
123
146
|
expect(result.container.querySelector('[data-testid="barcode-preview-qr"]')).not.toBeNull();
|
|
124
147
|
});
|
|
125
148
|
|
|
@@ -130,7 +153,9 @@ describe("BarcodePreview — qr format", () => {
|
|
|
130
153
|
|
|
131
154
|
it("falls back to honest mono text past version 10 capacity", () => {
|
|
132
155
|
const long = "x".repeat(214);
|
|
133
|
-
const result = renderWithProviders(
|
|
156
|
+
const result = renderWithProviders(
|
|
157
|
+
<BarcodePreview value={long} format="qr" height={formHeight} />,
|
|
158
|
+
);
|
|
134
159
|
expect(result.container.querySelector('[data-testid="barcode-preview-qr"]')).toBeNull();
|
|
135
160
|
expect(result.container.textContent).toContain(long);
|
|
136
161
|
});
|
|
@@ -141,3 +166,126 @@ describe("BarcodePreview — qr format", () => {
|
|
|
141
166
|
expect(result.container.querySelector('[data-testid="barcode-preview"]')).not.toBeNull();
|
|
142
167
|
});
|
|
143
168
|
});
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Whole-pixel modules. Measured on the media-11 board at DPR 2 BEFORE this
|
|
172
|
+
* rule existed: bounds 33, 72 and 96 decoded and bounds 18, 24, 44 and 56 did
|
|
173
|
+
* not — not a size floor (33 read, 44 did not) but a resampling one. 33/33
|
|
174
|
+
* modules is exactly 1.000 px per module; 44/33 is 1.333, and a fractional
|
|
175
|
+
* module rounds to different widths across the grid. It reached the field's
|
|
176
|
+
* own default path: at height 36 / moduleWidth 1 a payload over 26 bytes
|
|
177
|
+
* clamps under one pixel per module and stops reading, which is precisely the
|
|
178
|
+
* scan-to-draw round trip MPO-53 AC-9 exists to close.
|
|
179
|
+
*/
|
|
180
|
+
describe("BarcodePreview — qr geometry is whole-pixel or nothing", () => {
|
|
181
|
+
const qrSquare = (ui: React.ReactElement) =>
|
|
182
|
+
renderWithProviders(ui).container.querySelector(
|
|
183
|
+
'[data-testid="barcode-preview-qr"]',
|
|
184
|
+
) as HTMLElement | null;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Tamagui compiles geometry to atomic classes (`_w-87px`, `_pt-12px`), not
|
|
188
|
+
* inline style, so the measurement reads the class list. Same numbers, and
|
|
189
|
+
* it is the class the browser actually applies.
|
|
190
|
+
*/
|
|
191
|
+
const px = (el: HTMLElement, prefix: string) => {
|
|
192
|
+
const hit = (el.getAttribute("class") ?? "")
|
|
193
|
+
.split(/\s+/)
|
|
194
|
+
.find((c) => c.startsWith(`_${prefix}-`));
|
|
195
|
+
return hit ? Number.parseFloat(hit.slice(prefix.length + 2)) : Number.NaN;
|
|
196
|
+
};
|
|
197
|
+
const edgeOf = (el: HTMLElement) => px(el, "w");
|
|
198
|
+
const quietOf = (el: HTMLElement) => px(el, "pt");
|
|
199
|
+
|
|
200
|
+
/** Modules per side including the 4-module quiet zone on each side. */
|
|
201
|
+
const totalModules = (value: string) => encodeQr(value)!.size + 2 * QR_QUIET_ZONE_MODULES;
|
|
202
|
+
|
|
203
|
+
it("draws at the natural size when moduleWidth fits under the bound", () => {
|
|
204
|
+
// 6 bytes → version 1 → 21 + 8 = 29 modules. moduleWidth 3 asks for 3px
|
|
205
|
+
// modules and the 120px bound allows 4, so 3 wins: 29 × 3 = 87.
|
|
206
|
+
const value = "SHC-42";
|
|
207
|
+
expect(totalModules(value)).toBe(29);
|
|
208
|
+
const square = qrSquare(
|
|
209
|
+
<BarcodePreview value={value} format="qr" height={120} moduleWidth={3} />,
|
|
210
|
+
);
|
|
211
|
+
expect(square).not.toBeNull();
|
|
212
|
+
expect(edgeOf(square!)).toBe(87);
|
|
213
|
+
expect(px(square!, "h")).toBe(87);
|
|
214
|
+
expect(quietOf(square!)).toBe(12); // 4 modules × 3px
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("treats the height bound as a CEILING, never a target", () => {
|
|
218
|
+
// 18 bytes → version 2 → 25 + 8 = 33 modules at moduleWidth 1 = 33px. A
|
|
219
|
+
// 44px bound does not stretch it to 44 and a fractional 1.333px module.
|
|
220
|
+
const value = "SHC-COMPOSE01-0417";
|
|
221
|
+
expect(totalModules(value)).toBe(33);
|
|
222
|
+
for (const height of [36, 44, 56]) {
|
|
223
|
+
const square = qrSquare(<BarcodePreview value={value} format="qr" height={height} />);
|
|
224
|
+
expect(edgeOf(square!)).toBe(33);
|
|
225
|
+
expect(quietOf(square!)).toBe(4);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("snaps the module DOWN to a whole pixel rather than draw a fractional one", () => {
|
|
230
|
+
// 33 modules under a 96px bound would be 2.909px per module. Snap to 2.
|
|
231
|
+
const square = qrSquare(
|
|
232
|
+
<BarcodePreview value="SHC-COMPOSE01-0417" format="qr" height={96} moduleWidth={9} />,
|
|
233
|
+
);
|
|
234
|
+
expect(edgeOf(square!)).toBe(66);
|
|
235
|
+
expect(quietOf(square!)).toBe(8);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("never draws a module narrower than one whole pixel — every bound, every payload", () => {
|
|
239
|
+
for (const value of ["SHC-42", "SHC-COMPOSE01-0417", "SHC-COMPOSE01-BACKUP-2026-08-28"]) {
|
|
240
|
+
for (const height of [18, 24, 29, 33, 36, 37, 44, 66, 96, 120]) {
|
|
241
|
+
for (const moduleWidth of [1, 2, 3]) {
|
|
242
|
+
const square = qrSquare(
|
|
243
|
+
<BarcodePreview value={value} format="qr" height={height} moduleWidth={moduleWidth} />,
|
|
244
|
+
);
|
|
245
|
+
if (!square) continue; // honest text fallback — asserted below
|
|
246
|
+
const edge = edgeOf(square);
|
|
247
|
+
const pad = quietOf(square);
|
|
248
|
+
const cell = edge / totalModules(value);
|
|
249
|
+
expect(Number.isInteger(cell)).toBe(true);
|
|
250
|
+
expect(cell).toBeGreaterThanOrEqual(1);
|
|
251
|
+
expect(cell).toBeLessThanOrEqual(moduleWidth);
|
|
252
|
+
expect(edge).toBeLessThanOrEqual(height); // LC-10 containment
|
|
253
|
+
expect(pad).toBe(cell * QR_QUIET_ZONE_MODULES);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("falls back to honest text under one whole pixel per module", () => {
|
|
260
|
+
// compact (24) cannot hold 33 modules at a whole pixel each.
|
|
261
|
+
const value = "SHC-COMPOSE01-0417";
|
|
262
|
+
const result = renderWithProviders(<BarcodePreview value={value} format="qr" height={24} />);
|
|
263
|
+
expect(result.container.querySelector('[data-testid="barcode-preview-qr"]')).toBeNull();
|
|
264
|
+
expect(result.container.textContent).toContain(value);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("falls back to honest text when the payload outgrows the form bound", () => {
|
|
268
|
+
// 31 bytes → version 3 → 29 + 8 = 37 modules, which does not fit 36px at
|
|
269
|
+
// one whole pixel each. This is the case that silently shipped unreadable.
|
|
270
|
+
const value = "SHC-COMPOSE01-BACKUP-2026-08-28";
|
|
271
|
+
expect(totalModules(value)).toBe(37);
|
|
272
|
+
const result = renderWithProviders(<BarcodePreview value={value} format="qr" height={36} />);
|
|
273
|
+
expect(result.container.querySelector('[data-testid="barcode-preview-qr"]')).toBeNull();
|
|
274
|
+
expect(result.container.textContent).toContain(value);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it("draws nothing at all when the host already shows the value (textFallback false)", () => {
|
|
278
|
+
const value = "SHC-COMPOSE01-0417";
|
|
279
|
+
const result = renderWithProviders(
|
|
280
|
+
<BarcodePreview value={value} format="qr" height={24} textFallback={false} />,
|
|
281
|
+
);
|
|
282
|
+
expect(result.container.querySelector('[data-testid="barcode-preview-qr"]')).toBeNull();
|
|
283
|
+
expect(result.container.textContent).not.toContain(value);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("leaves the 1-D strip's geometry untouched", () => {
|
|
287
|
+
const result = renderWithProviders(<BarcodePreview value="SKU-001234" height={18} />);
|
|
288
|
+
expect(result.container.querySelector('[data-testid="barcode-preview"]')).not.toBeNull();
|
|
289
|
+
expect(result.container.querySelector('[data-testid="barcode-preview-qr"]')).toBeNull();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BarcodePreview — the drawn face of the barcode field, not the input.
|
|
3
|
+
* Barcode.stories.tsx mounts the field; this file mounts the preview alone
|
|
4
|
+
* so Kitchen Sink can show QR and Code 128 without a scanner.
|
|
5
|
+
*
|
|
6
|
+
* QR polarity is themed ($color on $background). MPO-94 is Clay's call;
|
|
7
|
+
* these stories do not pin dark-on-light.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Meta, StoryObj } from "@storybook/react-native-web-vite";
|
|
11
|
+
import { Text, XStack, YStack } from "tamagui";
|
|
12
|
+
import { BarcodePreview } from "./BarcodePreview";
|
|
13
|
+
|
|
14
|
+
const meta: Meta<typeof BarcodePreview> = {
|
|
15
|
+
title: "Forms/BarcodePreview",
|
|
16
|
+
component: BarcodePreview,
|
|
17
|
+
parameters: {
|
|
18
|
+
status: { type: "stable" },
|
|
19
|
+
docs: {
|
|
20
|
+
description: {
|
|
21
|
+
component:
|
|
22
|
+
"Canvas-free barcode/QR preview. Theme ink on theme surface in both schemes. MPO-94 (pin vs themed QR polarity) is not taken here.",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
argTypes: {
|
|
27
|
+
value: { control: "text" },
|
|
28
|
+
format: { control: "select", options: ["code128", "qr", "qrcode"] },
|
|
29
|
+
height: { control: "number" },
|
|
30
|
+
moduleWidth: { control: "number" },
|
|
31
|
+
showText: { control: "boolean" },
|
|
32
|
+
textFallback: { control: "boolean" },
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export default meta;
|
|
37
|
+
type Story = StoryObj<typeof BarcodePreview>;
|
|
38
|
+
|
|
39
|
+
export const Main: Story = {
|
|
40
|
+
name: "Main",
|
|
41
|
+
args: {
|
|
42
|
+
value: "SKU-001234",
|
|
43
|
+
height: 36,
|
|
44
|
+
showText: true,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export const Code128: Story = {
|
|
49
|
+
args: {
|
|
50
|
+
value: "SKU-001234",
|
|
51
|
+
format: "code128",
|
|
52
|
+
height: 36,
|
|
53
|
+
showText: true,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* QR face (MPO-53 AC-9). Height 120 so a short payload draws at a whole
|
|
59
|
+
* pixel per module. Polarity stays themed — MPO-94 is not this lane.
|
|
60
|
+
*/
|
|
61
|
+
export const Qr: Story = {
|
|
62
|
+
args: {
|
|
63
|
+
value: "SHC-42",
|
|
64
|
+
format: "qr",
|
|
65
|
+
height: 120,
|
|
66
|
+
showText: true,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** Host owns the empty marker; the preview paints nothing. */
|
|
71
|
+
export const Empty: Story = {
|
|
72
|
+
render: () => (
|
|
73
|
+
<YStack gap="$2">
|
|
74
|
+
<Text fontSize="$2" color="$color10">
|
|
75
|
+
empty value — preview is null
|
|
76
|
+
</Text>
|
|
77
|
+
<BarcodePreview value="" format="qr" height={72} />
|
|
78
|
+
</YStack>
|
|
79
|
+
),
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Themed polarity specimen. Both faces paint $color on $background, so a
|
|
84
|
+
* scheme flip restyles them with the rest of the screen. Do not treat this
|
|
85
|
+
* as a pin-to-dark-on-light preview; that decision lives on MPO-94.
|
|
86
|
+
*/
|
|
87
|
+
export const ThemedPolarity: Story = {
|
|
88
|
+
render: () => (
|
|
89
|
+
<XStack gap="$6" flexWrap="wrap" padding="$4" alignItems="flex-end">
|
|
90
|
+
<YStack gap="$2" alignItems="center">
|
|
91
|
+
<Text fontSize="$2" color="$color10">
|
|
92
|
+
QR (themed)
|
|
93
|
+
</Text>
|
|
94
|
+
<BarcodePreview value="SHC-42" format="qr" height={120} showText />
|
|
95
|
+
</YStack>
|
|
96
|
+
<YStack gap="$2" alignItems="center">
|
|
97
|
+
<Text fontSize="$2" color="$color10">
|
|
98
|
+
Code 128 (themed)
|
|
99
|
+
</Text>
|
|
100
|
+
<BarcodePreview value="SKU-001234" format="code128" height={36} showText />
|
|
101
|
+
</YStack>
|
|
102
|
+
</XStack>
|
|
103
|
+
),
|
|
104
|
+
};
|
|
@@ -11,15 +11,16 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Containment contract (LC-10): the symbol never exceeds its host. The strip
|
|
13
13
|
* caps width at 100% and bars shrink proportionally (preserving relative
|
|
14
|
-
* module widths) instead of painting past the frame; the QR square
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* height via the `height` prop (table cells keep it under the row height).
|
|
14
|
+
* module widths) instead of painting past the frame; the QR square is drawn at
|
|
15
|
+
* a WHOLE number of pixels per module, capped by `moduleWidth` and by the
|
|
16
|
+
* `height` bound — always square, never letterboxed or clipped. Hosts bound
|
|
17
|
+
* the height via the `height` prop (table cells keep it under the row height).
|
|
18
18
|
*
|
|
19
19
|
* Honest states: an empty value renders nothing (hosts show their own empty
|
|
20
20
|
* marker); a value the symbology cannot encode (non-ASCII for Code 128 B,
|
|
21
|
-
* over version 10 capacity for QR)
|
|
22
|
-
*
|
|
21
|
+
* over version 10 capacity for QR), or a QR whose modules would be under one
|
|
22
|
+
* whole pixel at the host's bound, falls back to the raw value as mono text —
|
|
23
|
+
* never a fake, truncated or unreadable symbol.
|
|
23
24
|
*/
|
|
24
25
|
|
|
25
26
|
import { Text, View, XStack, YStack } from "tamagui";
|
|
@@ -44,10 +45,11 @@ export interface BarcodePreviewProps {
|
|
|
44
45
|
value?: string;
|
|
45
46
|
/**
|
|
46
47
|
* Symbol height bound in px — content geometry, bounded by the host. The
|
|
47
|
-
* 1-D strip always fills it; the QR square uses it as a
|
|
48
|
+
* 1-D strip always fills it; the QR square uses it as a CEILING and draws at
|
|
49
|
+
* the largest whole-pixel module that fits under it.
|
|
48
50
|
*/
|
|
49
51
|
height?: number;
|
|
50
|
-
/** Width of one barcode module in px. */
|
|
52
|
+
/** Width of one barcode module in px. Fractional values floor for QR. */
|
|
51
53
|
moduleWidth?: number;
|
|
52
54
|
/** Render the human-readable value under the symbol. */
|
|
53
55
|
showText?: boolean;
|
|
@@ -56,6 +58,15 @@ export interface BarcodePreviewProps {
|
|
|
56
58
|
* alias of `qr` draws the QR matrix; others draw the Code 128 strip.
|
|
57
59
|
*/
|
|
58
60
|
format?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Render the raw value as mono text when the symbology cannot draw it —
|
|
63
|
+
* non-ASCII for Code 128 B, over capacity or under one whole pixel per
|
|
64
|
+
* module for QR. Default true, which is right for a standalone preview:
|
|
65
|
+
* disappearing silently would be worse than an honest string. A host that
|
|
66
|
+
* ALREADY shows the value beside the symbol (FieldDisplay) passes false, so
|
|
67
|
+
* an undrawable symbol contributes nothing instead of printing it twice.
|
|
68
|
+
*/
|
|
69
|
+
textFallback?: boolean;
|
|
59
70
|
}
|
|
60
71
|
|
|
61
72
|
/** Mono value text (LC-07 CODE-KNOB-IMMUNE) — both the honest fallback for
|
|
@@ -74,15 +85,22 @@ export function BarcodePreview({
|
|
|
74
85
|
moduleWidth = 1,
|
|
75
86
|
showText = false,
|
|
76
87
|
format,
|
|
88
|
+
textFallback = true,
|
|
77
89
|
}: BarcodePreviewProps) {
|
|
78
90
|
if (!value) return null;
|
|
79
91
|
if (barcodePreviewSymbology(format) === "qr") {
|
|
80
92
|
return (
|
|
81
|
-
<QrPreview
|
|
93
|
+
<QrPreview
|
|
94
|
+
value={value}
|
|
95
|
+
height={height}
|
|
96
|
+
moduleWidth={moduleWidth}
|
|
97
|
+
showText={showText}
|
|
98
|
+
textFallback={textFallback}
|
|
99
|
+
/>
|
|
82
100
|
);
|
|
83
101
|
}
|
|
84
102
|
const runs = encodeCode128B(value);
|
|
85
|
-
if (!runs) return <MonoValue value={value}
|
|
103
|
+
if (!runs) return textFallback ? <MonoValue value={value} /> : null;
|
|
86
104
|
const quietZone = moduleWidth * 10;
|
|
87
105
|
return (
|
|
88
106
|
<View
|
|
@@ -126,19 +144,33 @@ function QrPreview({
|
|
|
126
144
|
height,
|
|
127
145
|
moduleWidth,
|
|
128
146
|
showText,
|
|
147
|
+
textFallback,
|
|
129
148
|
}: {
|
|
130
149
|
value: string;
|
|
131
150
|
height: number;
|
|
132
151
|
moduleWidth: number;
|
|
133
152
|
showText: boolean;
|
|
153
|
+
textFallback: boolean;
|
|
134
154
|
}) {
|
|
135
155
|
const matrix = encodeQr(value);
|
|
136
|
-
if (!matrix) return <MonoValue value={value}
|
|
137
|
-
//
|
|
138
|
-
//
|
|
156
|
+
if (!matrix) return textFallback ? <MonoValue value={value} /> : null;
|
|
157
|
+
// A module must land on a WHOLE pixel. A fractional module width rounds to
|
|
158
|
+
// different widths across the grid, so a decoder cannot resample the symbol —
|
|
159
|
+
// measured on the media-11 board at DPR 2: the 33px square (1.000 px/module)
|
|
160
|
+
// decoded and the 44px one (1.333 px/module) did not, in either scheme. So
|
|
161
|
+
// the module is the largest whole pixel that fits, capped by `moduleWidth`
|
|
162
|
+
// and by the host's `height` bound (LC-10 — the square never exceeds its
|
|
163
|
+
// host, and the bound is a ceiling, never a target).
|
|
139
164
|
const totalModules = matrix.size + 2 * QR_QUIET_ZONE_MODULES;
|
|
140
|
-
const
|
|
141
|
-
|
|
165
|
+
const cell = Math.floor(Math.min(moduleWidth, height / totalModules));
|
|
166
|
+
// Under one whole pixel there is no readable symbol to draw at all. Fall
|
|
167
|
+
// back to the value as text, the same honest contract the over-capacity and
|
|
168
|
+
// non-ASCII cases have: a symbol no reader can resample is decoration, and
|
|
169
|
+
// decoration that looks like data is the one thing this component refuses to
|
|
170
|
+
// paint. This is what a `compact` bound (24) or a table-row bound does to
|
|
171
|
+
// anything past a short SKU.
|
|
172
|
+
if (cell < 1) return textFallback ? <MonoValue value={value} /> : null;
|
|
173
|
+
const edge = cell * totalModules;
|
|
142
174
|
const quietZone = cell * QR_QUIET_ZONE_MODULES;
|
|
143
175
|
const rows = qrRowRuns(matrix);
|
|
144
176
|
return (
|