@lockin90/ui-native 1.0.1 → 2.1.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/lib/commonjs/components/AuroraBackground.js +63 -35
- package/lib/commonjs/components/AuroraBackground.js.map +1 -1
- package/lib/commonjs/components/Avatar.js +2 -1
- package/lib/commonjs/components/Avatar.js.map +1 -1
- package/lib/commonjs/components/BrandMark.js +7 -2
- package/lib/commonjs/components/BrandMark.js.map +1 -1
- package/lib/commonjs/components/FocalPointPicker.js +2 -1
- package/lib/commonjs/components/FocalPointPicker.js.map +1 -1
- package/lib/commonjs/components/Gauge.js +2 -1
- package/lib/commonjs/components/Gauge.js.map +1 -1
- package/lib/commonjs/components/Icon.js +1 -0
- package/lib/commonjs/components/Icon.js.map +1 -1
- package/lib/commonjs/components/Menu.js +2 -1
- package/lib/commonjs/components/Menu.js.map +1 -1
- package/lib/commonjs/components/Modal.js +2 -1
- package/lib/commonjs/components/Modal.js.map +1 -1
- package/lib/commonjs/components/PasswordInput.js +3 -3
- package/lib/commonjs/components/PasswordInput.js.map +1 -1
- package/lib/commonjs/components/PhaseWheel.js +2 -1
- package/lib/commonjs/components/PhaseWheel.js.map +1 -1
- package/lib/commonjs/components/PkChart.js +1 -1
- package/lib/commonjs/components/PkChart.js.map +1 -1
- package/lib/commonjs/components/Popover.js +2 -1
- package/lib/commonjs/components/Popover.js.map +1 -1
- package/lib/commonjs/components/ProGate.js +2 -1
- package/lib/commonjs/components/ProGate.js.map +1 -1
- package/lib/commonjs/components/RestTimer.js +2 -1
- package/lib/commonjs/components/RestTimer.js.map +1 -1
- package/lib/commonjs/components/Ring.js +2 -1
- package/lib/commonjs/components/Ring.js.map +1 -1
- package/lib/commonjs/components/SegmentedTabs.js +2 -1
- package/lib/commonjs/components/SegmentedTabs.js.map +1 -1
- package/lib/commonjs/components/Select.js +7 -1
- package/lib/commonjs/components/Select.js.map +1 -1
- package/lib/commonjs/components/ShareCard.js +2 -1
- package/lib/commonjs/components/ShareCard.js.map +1 -1
- package/lib/commonjs/components/Sheet.js +2 -1
- package/lib/commonjs/components/Sheet.js.map +1 -1
- package/lib/commonjs/components/TabBar.js +2 -1
- package/lib/commonjs/components/TabBar.js.map +1 -1
- package/lib/commonjs/components/TagInput.js +86 -42
- package/lib/commonjs/components/TagInput.js.map +1 -1
- package/lib/commonjs/components/Toggle.js +2 -1
- package/lib/commonjs/components/Toggle.js.map +1 -1
- package/lib/commonjs/components/Tooltip.js +2 -1
- package/lib/commonjs/components/Tooltip.js.map +1 -1
- package/lib/commonjs/internal/absoluteFill.js +18 -0
- package/lib/commonjs/internal/absoluteFill.js.map +1 -0
- package/lib/module/components/AuroraBackground.js +64 -36
- package/lib/module/components/AuroraBackground.js.map +1 -1
- package/lib/module/components/Avatar.js +2 -1
- package/lib/module/components/Avatar.js.map +1 -1
- package/lib/module/components/BrandMark.js +7 -2
- package/lib/module/components/BrandMark.js.map +1 -1
- package/lib/module/components/FocalPointPicker.js +2 -1
- package/lib/module/components/FocalPointPicker.js.map +1 -1
- package/lib/module/components/Gauge.js +2 -1
- package/lib/module/components/Gauge.js.map +1 -1
- package/lib/module/components/Icon.js +2 -1
- package/lib/module/components/Icon.js.map +1 -1
- package/lib/module/components/Menu.js +2 -1
- package/lib/module/components/Menu.js.map +1 -1
- package/lib/module/components/Modal.js +2 -1
- package/lib/module/components/Modal.js.map +1 -1
- package/lib/module/components/PasswordInput.js +3 -3
- package/lib/module/components/PasswordInput.js.map +1 -1
- package/lib/module/components/PhaseWheel.js +2 -1
- package/lib/module/components/PhaseWheel.js.map +1 -1
- package/lib/module/components/PkChart.js +1 -1
- package/lib/module/components/PkChart.js.map +1 -1
- package/lib/module/components/Popover.js +2 -1
- package/lib/module/components/Popover.js.map +1 -1
- package/lib/module/components/ProGate.js +2 -1
- package/lib/module/components/ProGate.js.map +1 -1
- package/lib/module/components/RestTimer.js +2 -1
- package/lib/module/components/RestTimer.js.map +1 -1
- package/lib/module/components/Ring.js +2 -1
- package/lib/module/components/Ring.js.map +1 -1
- package/lib/module/components/SegmentedTabs.js +2 -1
- package/lib/module/components/SegmentedTabs.js.map +1 -1
- package/lib/module/components/Select.js +7 -1
- package/lib/module/components/Select.js.map +1 -1
- package/lib/module/components/ShareCard.js +2 -1
- package/lib/module/components/ShareCard.js.map +1 -1
- package/lib/module/components/Sheet.js +2 -1
- package/lib/module/components/Sheet.js.map +1 -1
- package/lib/module/components/TabBar.js +2 -1
- package/lib/module/components/TabBar.js.map +1 -1
- package/lib/module/components/TagInput.js +82 -41
- package/lib/module/components/TagInput.js.map +1 -1
- package/lib/module/components/Toggle.js +2 -1
- package/lib/module/components/Toggle.js.map +1 -1
- package/lib/module/components/Tooltip.js +2 -1
- package/lib/module/components/Tooltip.js.map +1 -1
- package/lib/module/internal/absoluteFill.js +14 -0
- package/lib/module/internal/absoluteFill.js.map +1 -0
- package/lib/typescript/components/AuroraBackground.d.ts.map +1 -1
- package/lib/typescript/components/Avatar.d.ts.map +1 -1
- package/lib/typescript/components/BrandMark.d.ts +4 -1
- package/lib/typescript/components/BrandMark.d.ts.map +1 -1
- package/lib/typescript/components/FocalPointPicker.d.ts.map +1 -1
- package/lib/typescript/components/Gauge.d.ts.map +1 -1
- package/lib/typescript/components/Icon.d.ts +1 -1
- package/lib/typescript/components/Icon.d.ts.map +1 -1
- package/lib/typescript/components/Menu.d.ts.map +1 -1
- package/lib/typescript/components/Modal.d.ts.map +1 -1
- package/lib/typescript/components/PasswordInput.d.ts +7 -4
- package/lib/typescript/components/PasswordInput.d.ts.map +1 -1
- package/lib/typescript/components/PhaseWheel.d.ts.map +1 -1
- package/lib/typescript/components/PkChart.d.ts.map +1 -1
- package/lib/typescript/components/Popover.d.ts.map +1 -1
- package/lib/typescript/components/ProGate.d.ts.map +1 -1
- package/lib/typescript/components/RestTimer.d.ts.map +1 -1
- package/lib/typescript/components/Ring.d.ts.map +1 -1
- package/lib/typescript/components/SegmentedTabs.d.ts.map +1 -1
- package/lib/typescript/components/Select.d.ts +6 -1
- package/lib/typescript/components/Select.d.ts.map +1 -1
- package/lib/typescript/components/ShareCard.d.ts.map +1 -1
- package/lib/typescript/components/Sheet.d.ts.map +1 -1
- package/lib/typescript/components/TabBar.d.ts.map +1 -1
- package/lib/typescript/components/TagInput.d.ts +21 -8
- package/lib/typescript/components/TagInput.d.ts.map +1 -1
- package/lib/typescript/components/Toggle.d.ts.map +1 -1
- package/lib/typescript/components/Tooltip.d.ts.map +1 -1
- package/lib/typescript/internal/absoluteFill.d.ts +8 -0
- package/lib/typescript/internal/absoluteFill.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/components/AuroraBackground.tsx +39 -18
- package/src/components/Avatar.tsx +2 -1
- package/src/components/BrandMark.tsx +10 -4
- package/src/components/FocalPointPicker.tsx +2 -1
- package/src/components/Gauge.tsx +2 -1
- package/src/components/Icon.tsx +3 -0
- package/src/components/Menu.tsx +2 -1
- package/src/components/Modal.tsx +2 -1
- package/src/components/PasswordInput.tsx +9 -6
- package/src/components/PhaseWheel.tsx +2 -1
- package/src/components/PkChart.tsx +2 -1
- package/src/components/Popover.tsx +2 -1
- package/src/components/ProGate.tsx +2 -1
- package/src/components/RestTimer.tsx +2 -1
- package/src/components/Ring.tsx +2 -1
- package/src/components/SegmentedTabs.tsx +2 -1
- package/src/components/Select.tsx +10 -1
- package/src/components/ShareCard.tsx +2 -1
- package/src/components/Sheet.tsx +2 -1
- package/src/components/TabBar.tsx +2 -1
- package/src/components/TagInput.tsx +84 -44
- package/src/components/Toggle.tsx +2 -1
- package/src/components/Tooltip.tsx +2 -1
- package/src/components/__tests__/BrandMark.test.tsx +17 -0
- package/src/components/__tests__/CodeInput.test.tsx +81 -25
- package/src/components/__tests__/PasswordInput.test.tsx +19 -0
- package/src/components/__tests__/Select.test.tsx +22 -0
- package/src/internal/absoluteFill.ts +11 -0
package/src/components/Icon.tsx
CHANGED
|
@@ -71,6 +71,7 @@ import {
|
|
|
71
71
|
Pill,
|
|
72
72
|
Play,
|
|
73
73
|
Plus,
|
|
74
|
+
QrCode,
|
|
74
75
|
Quote,
|
|
75
76
|
RefreshCcw,
|
|
76
77
|
Ruler,
|
|
@@ -231,6 +232,7 @@ export type IconName =
|
|
|
231
232
|
| 'drop'
|
|
232
233
|
| 'utensil'
|
|
233
234
|
| 'barcode-scan'
|
|
235
|
+
| 'qr-code'
|
|
234
236
|
| 'pill'
|
|
235
237
|
| 'flask'
|
|
236
238
|
| 'syringe'
|
|
@@ -422,6 +424,7 @@ const LUCIDE: Record<Exclude<IconName, CustomIconName>, LucideIcon> = {
|
|
|
422
424
|
drop: Droplet,
|
|
423
425
|
utensil: Utensils,
|
|
424
426
|
'barcode-scan': ScanBarcode,
|
|
427
|
+
'qr-code': QrCode,
|
|
425
428
|
pill: Pill,
|
|
426
429
|
flask: FlaskConical,
|
|
427
430
|
syringe: Syringe,
|
package/src/components/Menu.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { useRef, useState, type ReactNode } from 'react';
|
|
|
2
2
|
import { View, Text, Pressable, StyleSheet, Modal, Dimensions } from 'react-native';
|
|
3
3
|
import { colors, radii, fonts, nativeShadows } from '@lockin90/tokens';
|
|
4
4
|
import { Icon } from './Icon';
|
|
5
|
+
import { absoluteFill } from '../internal/absoluteFill';
|
|
5
6
|
|
|
6
7
|
/** A single row in a {@link Menu}. A `divider` row renders a hairline
|
|
7
8
|
* separator instead of an action (its `label` is ignored). */
|
|
@@ -129,7 +130,7 @@ const styles = StyleSheet.create({
|
|
|
129
130
|
alignItems: 'center',
|
|
130
131
|
justifyContent: 'center',
|
|
131
132
|
},
|
|
132
|
-
backdrop: { ...
|
|
133
|
+
backdrop: { ...absoluteFill },
|
|
133
134
|
panel: {
|
|
134
135
|
position: 'absolute',
|
|
135
136
|
minWidth: 160,
|
package/src/components/Modal.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { type ReactNode } from 'react';
|
|
|
2
2
|
import { Modal as RNModal, View, Text, Pressable, StyleSheet } from 'react-native';
|
|
3
3
|
import Svg, { Path } from 'react-native-svg';
|
|
4
4
|
import { colors, radii, fonts } from '@lockin90/tokens';
|
|
5
|
+
import { absoluteFill } from '../internal/absoluteFill';
|
|
5
6
|
|
|
6
7
|
export interface ModalProps {
|
|
7
8
|
/** Show the modal. When false, nothing renders. */
|
|
@@ -61,7 +62,7 @@ export function Modal({ open = true, onClose, title, children, footer, size = 'm
|
|
|
61
62
|
|
|
62
63
|
const styles = StyleSheet.create({
|
|
63
64
|
scrim: {
|
|
64
|
-
...
|
|
65
|
+
...absoluteFill,
|
|
65
66
|
flex: 1,
|
|
66
67
|
alignItems: 'center',
|
|
67
68
|
justifyContent: 'center',
|
|
@@ -14,9 +14,12 @@ export interface PasswordInputProps extends Omit<TextInputProps, 'secureTextEntr
|
|
|
14
14
|
leading?: ReactNode;
|
|
15
15
|
/** start with the password revealed @default false */
|
|
16
16
|
defaultVisible?: boolean;
|
|
17
|
-
/** accessibility label
|
|
18
|
-
*
|
|
19
|
-
|
|
17
|
+
/** accessibility label of the toggle while masked (pass a localized string)
|
|
18
|
+
* @default 'Show password' */
|
|
19
|
+
showLabel?: string;
|
|
20
|
+
/** accessibility label of the toggle while revealed (pass a localized string)
|
|
21
|
+
* @default 'Hide password' */
|
|
22
|
+
hideLabel?: string;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
/**
|
|
@@ -30,7 +33,8 @@ export function PasswordInput({
|
|
|
30
33
|
inputSize = 'md',
|
|
31
34
|
leading,
|
|
32
35
|
defaultVisible = false,
|
|
33
|
-
|
|
36
|
+
showLabel = 'Show password',
|
|
37
|
+
hideLabel = 'Hide password',
|
|
34
38
|
style,
|
|
35
39
|
onFocus,
|
|
36
40
|
onBlur,
|
|
@@ -40,7 +44,6 @@ export function PasswordInput({
|
|
|
40
44
|
const [visible, setVisible] = useState(defaultVisible);
|
|
41
45
|
const sm = inputSize === 'sm';
|
|
42
46
|
const borderColor = invalid ? colors.zoneLow : focused ? colors.accent : colors.border2;
|
|
43
|
-
const aria = toggleLabel ?? (visible ? 'Hide password' : 'Show password');
|
|
44
47
|
|
|
45
48
|
return (
|
|
46
49
|
<View style={styles.wrap}>
|
|
@@ -63,7 +66,7 @@ export function PasswordInput({
|
|
|
63
66
|
/>
|
|
64
67
|
<Pressable
|
|
65
68
|
accessibilityRole="button"
|
|
66
|
-
accessibilityLabel={
|
|
69
|
+
accessibilityLabel={visible ? hideLabel : showLabel}
|
|
67
70
|
hitSlop={8}
|
|
68
71
|
onPress={() => setVisible((v) => !v)}
|
|
69
72
|
style={styles.trail}
|
|
@@ -2,6 +2,7 @@ import { useId, type ReactNode } from 'react';
|
|
|
2
2
|
import { View, Text, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
3
|
import Svg, { Circle, Defs, FeGaussianBlur, Filter, G, Line, Path, Rect, Text as SvgText } from 'react-native-svg';
|
|
4
4
|
import { colors, fonts, radii } from '@lockin90/tokens';
|
|
5
|
+
import { absoluteFill } from '../internal/absoluteFill';
|
|
5
6
|
|
|
6
7
|
/** A colored phase band on the 24h ring, drawn CLOCKWISE from `from` to `to`
|
|
7
8
|
* (hours 0..24). When `from > to` the band wraps through midnight (e.g. 23→7). */
|
|
@@ -694,7 +695,7 @@ export function PhaseWheel({
|
|
|
694
695
|
const styles = StyleSheet.create({
|
|
695
696
|
root: { position: 'relative', alignItems: 'center', justifyContent: 'center' },
|
|
696
697
|
center: {
|
|
697
|
-
...
|
|
698
|
+
...absoluteFill,
|
|
698
699
|
flexDirection: 'column',
|
|
699
700
|
alignItems: 'center',
|
|
700
701
|
justifyContent: 'center',
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
} from 'react-native';
|
|
13
13
|
import Svg, { Path, Line, Rect, Defs, LinearGradient, Stop, ClipPath } from 'react-native-svg';
|
|
14
14
|
import { colors, fonts, radii } from '@lockin90/tokens';
|
|
15
|
+
import { absoluteFill } from '../internal/absoluteFill';
|
|
15
16
|
|
|
16
17
|
/** A single dose taken at `at` hours from t0, of `mg` of substance. */
|
|
17
18
|
export interface PkDose {
|
|
@@ -326,7 +327,7 @@ export function PkChart({
|
|
|
326
327
|
{beatAnim && line && canvasW > 0 ? (
|
|
327
328
|
<Animated.View
|
|
328
329
|
pointerEvents="none"
|
|
329
|
-
style={[StyleSheet.
|
|
330
|
+
style={[StyleSheet.absoluteFill, { opacity: beatAnim.glowOpacity }]}
|
|
330
331
|
>
|
|
331
332
|
<Svg width={canvasW} height={height} viewBox={`0 0 ${VB_W} ${height}`} preserveAspectRatio="none">
|
|
332
333
|
<Path d={line} fill="none" stroke={color} strokeWidth={7} strokeLinecap="round" strokeLinejoin="round" />
|
|
@@ -3,6 +3,7 @@ import { View, Text, Pressable, StyleSheet, Modal, Dimensions } from 'react-nati
|
|
|
3
3
|
import Svg, { Rect, Path } from 'react-native-svg';
|
|
4
4
|
import { colors, radii, fonts, nativeShadows } from '@lockin90/tokens';
|
|
5
5
|
import { Icon } from './Icon';
|
|
6
|
+
import { absoluteFill } from '../internal/absoluteFill';
|
|
6
7
|
|
|
7
8
|
/* expo-clipboard is an optional dependency — not a hard dep of this kit.
|
|
8
9
|
* Resolve it lazily and guard usage; if it's absent we keep the visual
|
|
@@ -112,7 +113,7 @@ const styles = StyleSheet.create({
|
|
|
112
113
|
alignItems: 'center',
|
|
113
114
|
justifyContent: 'center',
|
|
114
115
|
},
|
|
115
|
-
backdrop: { ...
|
|
116
|
+
backdrop: { ...absoluteFill },
|
|
116
117
|
panel: {
|
|
117
118
|
position: 'absolute',
|
|
118
119
|
maxWidth: 260,
|
|
@@ -3,6 +3,7 @@ import { View, Text, Pressable, StyleSheet, type ViewProps } from 'react-native'
|
|
|
3
3
|
import Svg, { Path, Rect } from 'react-native-svg';
|
|
4
4
|
import { colors, radii, fonts } from '@lockin90/tokens';
|
|
5
5
|
import { OptionalBlurView } from '../internal/optionalBlur';
|
|
6
|
+
import { absoluteFill } from '../internal/absoluteFill';
|
|
6
7
|
|
|
7
8
|
export interface ProGateProps extends Omit<ViewProps, 'children'> {
|
|
8
9
|
/** Lock-card heading. */
|
|
@@ -100,7 +101,7 @@ const styles = StyleSheet.create({
|
|
|
100
101
|
opacity: 0.35,
|
|
101
102
|
},
|
|
102
103
|
frost: {
|
|
103
|
-
...
|
|
104
|
+
...absoluteFill,
|
|
104
105
|
},
|
|
105
106
|
contentEmpty: {
|
|
106
107
|
minHeight: 150,
|
|
@@ -3,6 +3,7 @@ import { View, Text, StyleSheet, type StyleProp, type ViewStyle } from 'react-na
|
|
|
3
3
|
import Svg, { Circle, Defs, FeGaussianBlur, Filter, G } from 'react-native-svg';
|
|
4
4
|
import { colors, fonts } from '@lockin90/tokens';
|
|
5
5
|
import { Button } from './Button';
|
|
6
|
+
import { absoluteFill } from '../internal/absoluteFill';
|
|
6
7
|
|
|
7
8
|
export interface RestTimerProps {
|
|
8
9
|
/** Seconds left (drives the arc + the centered readout). */
|
|
@@ -159,7 +160,7 @@ const styles = StyleSheet.create({
|
|
|
159
160
|
ring: { width: SIZE, height: SIZE },
|
|
160
161
|
ringCompact: { width: SIZE_COMPACT, height: SIZE_COMPACT },
|
|
161
162
|
center: {
|
|
162
|
-
...
|
|
163
|
+
...absoluteFill,
|
|
163
164
|
alignItems: 'center',
|
|
164
165
|
justifyContent: 'center',
|
|
165
166
|
gap: 2,
|
package/src/components/Ring.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import { useId, type ReactNode } from 'react';
|
|
|
2
2
|
import { View, Text, StyleSheet, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
3
|
import Svg, { Circle, Defs, FeGaussianBlur, Filter, G, LinearGradient, Stop } from 'react-native-svg';
|
|
4
4
|
import { colors, fonts, radii } from '@lockin90/tokens';
|
|
5
|
+
import { absoluteFill } from '../internal/absoluteFill';
|
|
5
6
|
|
|
6
7
|
/** Zone verdict color for a 0–100 value (same thresholds as the chart catalog). */
|
|
7
8
|
function zoneColor(v: number): string {
|
|
@@ -238,7 +239,7 @@ export function Ring({
|
|
|
238
239
|
const styles = StyleSheet.create({
|
|
239
240
|
root: { position: 'relative', alignItems: 'center', justifyContent: 'center' },
|
|
240
241
|
center: {
|
|
241
|
-
...
|
|
242
|
+
...absoluteFill,
|
|
242
243
|
alignItems: 'center',
|
|
243
244
|
justifyContent: 'center',
|
|
244
245
|
},
|
|
@@ -3,6 +3,7 @@ import { LinearGradient } from 'expo-linear-gradient';
|
|
|
3
3
|
import { colors, radii, fonts, gradients, nativeShadows } from '@lockin90/tokens';
|
|
4
4
|
import { hapticSelection } from "../internal/optionalHaptics";
|
|
5
5
|
import { Icon, type IconName } from './Icon';
|
|
6
|
+
import { absoluteFill } from '../internal/absoluteFill';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Per-segment tone — mirrors the web `SegmentedTone`. Two families:
|
|
@@ -347,7 +348,7 @@ const styles = StyleSheet.create({
|
|
|
347
348
|
// absolutely positioned behind the label) + glow, mirroring web
|
|
348
349
|
// `[data-tone='mint'|'gradient'] .is-active`. The label paints dark-on-bright.
|
|
349
350
|
tabActiveFill: {
|
|
350
|
-
...
|
|
351
|
+
...absoluteFill,
|
|
351
352
|
borderRadius: radii.pill,
|
|
352
353
|
},
|
|
353
354
|
glowMint: { ...nativeShadows.glowMint },
|
|
@@ -44,6 +44,11 @@ export interface SelectProps {
|
|
|
44
44
|
searchPlaceholder?: string;
|
|
45
45
|
/** title shown at the top of the option sheet (with a close button). */
|
|
46
46
|
title?: string;
|
|
47
|
+
/** accessibility label of the sheet's close button (pass a localized string). @default 'Close' */
|
|
48
|
+
closeLabel?: string;
|
|
49
|
+
/** the field's name for screen readers when there is no visible `label`
|
|
50
|
+
* (falls back to `label`). Mirrors web `aria-label`. */
|
|
51
|
+
accessibilityLabel?: string;
|
|
47
52
|
/**
|
|
48
53
|
* Field scale. `'md'` (default) is the standard form field. `'sm'` is the
|
|
49
54
|
* design settings compact glass pill (`.pref-select`): mono 11px value,
|
|
@@ -114,6 +119,8 @@ export function Select({
|
|
|
114
119
|
searchable = false,
|
|
115
120
|
searchPlaceholder = 'Search…',
|
|
116
121
|
title,
|
|
122
|
+
closeLabel = 'Close',
|
|
123
|
+
accessibilityLabel,
|
|
117
124
|
size = 'md',
|
|
118
125
|
leading,
|
|
119
126
|
}: SelectProps) {
|
|
@@ -147,6 +154,8 @@ export function Select({
|
|
|
147
154
|
disabled && styles.fieldDisabled,
|
|
148
155
|
]}
|
|
149
156
|
accessibilityRole="button"
|
|
157
|
+
accessibilityLabel={accessibilityLabel ?? label}
|
|
158
|
+
accessibilityValue={selected ? { text: selected.label } : undefined}
|
|
150
159
|
accessibilityState={{ disabled, expanded: open }}
|
|
151
160
|
disabled={disabled}
|
|
152
161
|
onPress={() => { hapticSelection(); setOpen(true); }}
|
|
@@ -174,7 +183,7 @@ export function Select({
|
|
|
174
183
|
<Text style={styles.title}>{title.toUpperCase()}</Text>
|
|
175
184
|
<Pressable
|
|
176
185
|
accessibilityRole="button"
|
|
177
|
-
accessibilityLabel=
|
|
186
|
+
accessibilityLabel={closeLabel}
|
|
178
187
|
hitSlop={8}
|
|
179
188
|
onPress={close}
|
|
180
189
|
style={styles.closeBtn}
|
|
@@ -5,6 +5,7 @@ import { BrandMark } from './BrandMark';
|
|
|
5
5
|
import { GradientText } from './GradientText';
|
|
6
6
|
import { Ring } from './Ring';
|
|
7
7
|
import { Stat } from './Stat';
|
|
8
|
+
import { absoluteFill } from '../internal/absoluteFill';
|
|
8
9
|
|
|
9
10
|
export type ShareCardGradient = 'flux' | 'cta' | 'mint' | 'violet';
|
|
10
11
|
export type ShareCardSize = 'sm' | 'md' | 'lg';
|
|
@@ -131,7 +132,7 @@ const styles = StyleSheet.create({
|
|
|
131
132
|
},
|
|
132
133
|
// aurora wash — a subtle token-gradient over the void surface (kept low so the
|
|
133
134
|
// void dominates the center and light --text stays legible, ~matching the web mesh)
|
|
134
|
-
wash: { ...
|
|
135
|
+
wash: { ...absoluteFill, opacity: 0.32 },
|
|
135
136
|
top: { position: 'relative' },
|
|
136
137
|
body: { flex: 1, position: 'relative', alignItems: 'center', justifyContent: 'center', gap: 10 },
|
|
137
138
|
eyebrow: { fontFamily: fonts.mono, letterSpacing: 2, textTransform: 'uppercase', color: colors.textDim },
|
package/src/components/Sheet.tsx
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
Platform,
|
|
12
12
|
} from 'react-native';
|
|
13
13
|
import { colors, radii, fonts } from '@lockin90/tokens';
|
|
14
|
+
import { absoluteFill } from '../internal/absoluteFill';
|
|
14
15
|
|
|
15
16
|
export interface SheetProps {
|
|
16
17
|
/** Show the sheet. When false, nothing renders. */
|
|
@@ -129,7 +130,7 @@ export function Sheet({ open = true, onClose, side = 'bottom', title, children }
|
|
|
129
130
|
|
|
130
131
|
const styles = StyleSheet.create({
|
|
131
132
|
scrim: {
|
|
132
|
-
...
|
|
133
|
+
...absoluteFill,
|
|
133
134
|
flex: 1,
|
|
134
135
|
backgroundColor: 'rgba(0,0,0,0.55)',
|
|
135
136
|
},
|
|
@@ -5,6 +5,7 @@ import { colors, radii, fonts, gradients, nativeShadows } from '@lockin90/tokens
|
|
|
5
5
|
import { Icon, type IconName } from './Icon';
|
|
6
6
|
import { OptionalBlurView } from '../internal/optionalBlur';
|
|
7
7
|
import { hapticSelection } from '../internal/optionalHaptics';
|
|
8
|
+
import { absoluteFill } from '../internal/absoluteFill';
|
|
8
9
|
|
|
9
10
|
/* ============================================================================
|
|
10
11
|
Mobile atoms — TabBar (bottom navigation) + FAB (floating action button).
|
|
@@ -223,7 +224,7 @@ const styles = StyleSheet.create({
|
|
|
223
224
|
// the frosted/translucent background, clipped to the capsule independently
|
|
224
225
|
// of the content so the FAB overhang survives
|
|
225
226
|
capsuleFill: {
|
|
226
|
-
...
|
|
227
|
+
...absoluteFill,
|
|
227
228
|
borderRadius: radii.pill,
|
|
228
229
|
overflow: 'hidden',
|
|
229
230
|
backgroundColor: 'rgba(12,15,26,0.82)',
|
|
@@ -129,38 +129,84 @@ export interface CodeInputProps extends ViewProps {
|
|
|
129
129
|
/** Web-parity alias of `error` — the web `CodeInput` names this `invalid`, so
|
|
130
130
|
* the same prop works on both platforms. Either prop turns the boxes red. */
|
|
131
131
|
invalid?: boolean;
|
|
132
|
+
/** each box's a11y label, `position` from 1 — pass a translated one (the default is English, for stories).
|
|
133
|
+
* Box 0 also takes a pasted / autofilled whole code but keeps `digitLabel(1, length)`. */
|
|
134
|
+
digitLabel?: (position: number, length: number) => string;
|
|
132
135
|
}
|
|
133
136
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
const defaultDigitLabel = (position: number, length: number) => `Digit ${position} of ${length}`;
|
|
138
|
+
|
|
139
|
+
/** The boxes for a code string: one digit each, non-digits dropped, padded with '' to `length`. */
|
|
140
|
+
export function codeToBoxes(value: string, length: number): string[] {
|
|
141
|
+
const digits = value.replace(/\D/g, '');
|
|
142
|
+
return Array.from({ length }, (_, i) => digits[i] ?? '');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Spread a multi-char entry (paste / iOS SMS autofill delivered into one box) across the boxes from
|
|
146
|
+
* `index` on: digits only, the ones past the last box dropped, earlier boxes kept. Focus lands after
|
|
147
|
+
* the last digit written (clamped). (Pure, so the paste math is unit-testable; mirrors the web.) */
|
|
148
|
+
export function spreadDigits(
|
|
149
|
+
boxes: readonly string[],
|
|
140
150
|
index: number,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const next =
|
|
145
|
-
for (let i = 0; i < digits.length && index + i < length; i++)
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
151
|
+
text: string,
|
|
152
|
+
): { boxes: string[]; focusIndex: number } {
|
|
153
|
+
const digits = text.replace(/\D/g, '');
|
|
154
|
+
const next = boxes.slice();
|
|
155
|
+
for (let i = 0; i < digits.length && index + i < next.length; i++) next[index + i] = digits[i];
|
|
156
|
+
return { boxes: next, focusIndex: Math.min(index + digits.length, next.length - 1) };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** What a box's new text does (its digit is always selected, so typing replaces it): one digit fills
|
|
160
|
+
* the box; more digits (paste, autofill) spread; no digit at all (a letter) is ignored (`null`);
|
|
161
|
+
* empty clears the box. Focus moves on after a written digit. */
|
|
162
|
+
export function typeIntoBox(
|
|
163
|
+
boxes: readonly string[],
|
|
164
|
+
index: number,
|
|
165
|
+
raw: string,
|
|
166
|
+
): { boxes: string[]; focusIndex: number } | null {
|
|
167
|
+
const digits = raw.replace(/\D/g, '');
|
|
168
|
+
if (raw && !digits) return null;
|
|
169
|
+
if (digits.length > 1) return spreadDigits(boxes, index, digits);
|
|
170
|
+
const next = boxes.slice();
|
|
171
|
+
next[index] = digits;
|
|
172
|
+
return { boxes: next, focusIndex: digits ? Math.min(index + 1, next.length - 1) : index };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** The boxes at another `length`: each keeps its place (a gap stays a gap), the extra ones dropped. */
|
|
176
|
+
export function resizeBoxes(boxes: readonly string[], length: number): string[] {
|
|
177
|
+
return Array.from({ length }, (_, i) => boxes[i] ?? '');
|
|
149
178
|
}
|
|
150
179
|
|
|
151
|
-
/** Segmented N-digit verification code — auto-advance + backspace. */
|
|
152
|
-
export function CodeInput({
|
|
180
|
+
/** Segmented N-digit verification code — auto-advance + backspace + paste / autofill into box 0. */
|
|
181
|
+
export function CodeInput({
|
|
182
|
+
length = 6,
|
|
183
|
+
value = '',
|
|
184
|
+
onChange,
|
|
185
|
+
error = false,
|
|
186
|
+
invalid = false,
|
|
187
|
+
digitLabel = defaultDigitLabel,
|
|
188
|
+
style,
|
|
189
|
+
...rest
|
|
190
|
+
}: CodeInputProps) {
|
|
153
191
|
const isInvalid = invalid || error;
|
|
154
|
-
|
|
192
|
+
// One entry per box, so clearing a middle box leaves a gap instead of shifting later digits.
|
|
193
|
+
const [boxes, setBoxes] = useState<string[]>(() => codeToBoxes(value, length));
|
|
194
|
+
// A new `value` from the app (e.g. cleared after a wrong code) replaces what's shown; the app echoing
|
|
195
|
+
// back what `onChange` gave it doesn't (it would close up a gap the user left).
|
|
196
|
+
// Another `length` (another kind of code) re-draws the boxes, each digit in its place; an app that
|
|
197
|
+
// changes it passes the matching `value` too, which wins.
|
|
198
|
+
const [prev, setPrev] = useState({ value, length });
|
|
199
|
+
if (value !== prev.value || length !== prev.length) {
|
|
200
|
+
setPrev({ value, length });
|
|
201
|
+
if (value !== prev.value && value !== boxes.join('')) setBoxes(codeToBoxes(value, length));
|
|
202
|
+
else if (length !== prev.length) setBoxes(resizeBoxes(boxes, length));
|
|
203
|
+
}
|
|
155
204
|
const [focused, setFocused] = useState<number | null>(null);
|
|
156
205
|
const refs = useRef<Array<TextInput | null>>([]);
|
|
157
206
|
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const clamped = next.slice(0, length);
|
|
162
|
-
setCode(clamped);
|
|
163
|
-
onChange?.(clamped);
|
|
207
|
+
const commit = (next: string[]) => {
|
|
208
|
+
setBoxes(next);
|
|
209
|
+
onChange?.(next.join(''));
|
|
164
210
|
};
|
|
165
211
|
|
|
166
212
|
const focusBox = (index: number) => {
|
|
@@ -168,39 +214,30 @@ export function CodeInput({ length = 6, value = '', onChange, error = false, inv
|
|
|
168
214
|
};
|
|
169
215
|
|
|
170
216
|
const onBoxChange = (index: number, raw: string) => {
|
|
171
|
-
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
commit(spread.code);
|
|
177
|
-
focusBox(spread.focusIndex);
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
const ch = digits.slice(-1);
|
|
181
|
-
const next = chars.slice();
|
|
182
|
-
next[index] = ch;
|
|
183
|
-
commit(next.join(''));
|
|
184
|
-
if (ch) focusBox(index + 1);
|
|
217
|
+
const typed = typeIntoBox(boxes, index, raw);
|
|
218
|
+
// A letter: re-render, so the native field drops it and shows the box's digit again.
|
|
219
|
+
if (!typed) return setBoxes((current) => current.slice());
|
|
220
|
+
commit(typed.boxes);
|
|
221
|
+
if (typed.focusIndex !== index) focusBox(typed.focusIndex);
|
|
185
222
|
};
|
|
186
223
|
|
|
187
224
|
const onKeyPress = (index: number, e: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
|
|
188
225
|
if (e.nativeEvent.key !== 'Backspace') return;
|
|
189
|
-
if (
|
|
190
|
-
const next =
|
|
226
|
+
if (boxes[index]) {
|
|
227
|
+
const next = boxes.slice();
|
|
191
228
|
next[index] = '';
|
|
192
|
-
commit(next
|
|
229
|
+
commit(next);
|
|
193
230
|
} else if (index > 0) {
|
|
194
|
-
const next =
|
|
231
|
+
const next = boxes.slice();
|
|
195
232
|
next[index - 1] = '';
|
|
196
|
-
commit(next
|
|
233
|
+
commit(next);
|
|
197
234
|
focusBox(index - 1);
|
|
198
235
|
}
|
|
199
236
|
};
|
|
200
237
|
|
|
201
238
|
return (
|
|
202
239
|
<View style={[styles.code, style]} {...rest}>
|
|
203
|
-
{
|
|
240
|
+
{boxes.map((ch, i) => {
|
|
204
241
|
const filled = !!ch;
|
|
205
242
|
const isFocused = focused === i;
|
|
206
243
|
const borderColor = isFocused
|
|
@@ -230,7 +267,10 @@ export function CodeInput({ length = 6, value = '', onChange, error = false, inv
|
|
|
230
267
|
// spreads across boxes; the rest stay single-char for clean typing.
|
|
231
268
|
maxLength={i === 0 ? length : 1}
|
|
232
269
|
value={ch}
|
|
233
|
-
|
|
270
|
+
// The digit stays selected, so typing replaces it (also after a tap on a focused box).
|
|
271
|
+
selectTextOnFocus
|
|
272
|
+
selection={{ start: 0, end: ch.length }}
|
|
273
|
+
accessibilityLabel={digitLabel(i + 1, length)}
|
|
234
274
|
selectionColor={colors.accent}
|
|
235
275
|
onChangeText={(t) => onBoxChange(i, t)}
|
|
236
276
|
onKeyPress={(e) => onKeyPress(i, e)}
|
|
@@ -2,6 +2,7 @@ import { View, Text, Pressable, StyleSheet, type StyleProp, type ViewStyle } fro
|
|
|
2
2
|
import { hapticSelection } from '../internal/optionalHaptics';
|
|
3
3
|
import { LinearGradient } from 'expo-linear-gradient';
|
|
4
4
|
import { colors, radii, fonts, gradients } from '@lockin90/tokens';
|
|
5
|
+
import { absoluteFill } from '../internal/absoluteFill';
|
|
5
6
|
|
|
6
7
|
export type ToggleSize = 'sm' | 'md';
|
|
7
8
|
|
|
@@ -142,7 +143,7 @@ const styles = StyleSheet.create({
|
|
|
142
143
|
boxShadow: '0 0 14px rgba(124,92,255,0.45)',
|
|
143
144
|
},
|
|
144
145
|
trackFill: {
|
|
145
|
-
...
|
|
146
|
+
...absoluteFill,
|
|
146
147
|
borderRadius: radii.pill,
|
|
147
148
|
},
|
|
148
149
|
knob: {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useRef, useState, type ReactNode } from 'react';
|
|
2
2
|
import { View, Text, Pressable, StyleSheet, Modal, Dimensions, type ViewStyle } from 'react-native';
|
|
3
3
|
import { colors, radii, fonts, nativeShadows } from '@lockin90/tokens';
|
|
4
|
+
import { absoluteFill } from '../internal/absoluteFill';
|
|
4
5
|
|
|
5
6
|
export type TooltipPlacement = 'top' | 'bottom' | 'left' | 'right';
|
|
6
7
|
|
|
@@ -166,7 +167,7 @@ const styles = StyleSheet.create({
|
|
|
166
167
|
justifyContent: 'center',
|
|
167
168
|
},
|
|
168
169
|
badgeText: { fontFamily: fonts.ui, fontSize: 11, fontWeight: '600', color: colors.textDim },
|
|
169
|
-
backdrop: { ...
|
|
170
|
+
backdrop: { ...absoluteFill },
|
|
170
171
|
bubble: {
|
|
171
172
|
position: 'absolute',
|
|
172
173
|
maxWidth: 220,
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest';
|
|
2
|
+
import { BrandMark } from '../BrandMark';
|
|
3
|
+
import { render } from './markup';
|
|
4
|
+
|
|
5
|
+
/* The web logo is a 30px mark with 19px text: the lockup's wordmark can be
|
|
6
|
+
* sized apart from the mark, as on web. */
|
|
7
|
+
describe('BrandMark lockup wordmark', () => {
|
|
8
|
+
test('scales with the mark by default', () => {
|
|
9
|
+
expect(render(BrandMark, { variant: 'lockup', size: 30 })).toMatch(/font-size:32px/);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('takes its own size from wordmarkSize', () => {
|
|
13
|
+
const html = render(BrandMark, { variant: 'lockup', size: 30, wordmarkSize: 19 });
|
|
14
|
+
expect(html).toMatch(/font-size:19px/);
|
|
15
|
+
expect(html).not.toMatch(/font-size:32px/);
|
|
16
|
+
});
|
|
17
|
+
});
|