@joinskips/brand 5.0.0-develop.118 → 5.0.0-develop.120
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.
|
@@ -62,6 +62,9 @@ export declare function CodeStep({ words, length, noMatch, ranOut, resendAfterMs
|
|
|
62
62
|
onResend: () => void;
|
|
63
63
|
onChangeEmail: () => void;
|
|
64
64
|
}): import("react").JSX.Element;
|
|
65
|
+
export declare const DOT_REST_OPACITY = 0.35;
|
|
66
|
+
export declare const DOT_RISE = 3;
|
|
67
|
+
export declare const DOT_STILL_OPACITY = 0.6;
|
|
65
68
|
/** Skip writing a reply: three dots rising in turn in Skip's bubble beside Skip's mark, announced as a status. */
|
|
66
69
|
export declare function SkipTyping({ label, skipMark }: {
|
|
67
70
|
label: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useRef, useState } from 'react';
|
|
3
|
-
import { ActivityIndicator, Animated, Image, Pressable, Text, TextInput, View } from 'react-native';
|
|
2
|
+
import { useEffect, useId, useRef, useState } from 'react';
|
|
3
|
+
import { AccessibilityInfo, ActivityIndicator, Animated, Image, Pressable, Text, TextInput, View } from 'react-native';
|
|
4
4
|
import { DOT_CYCLE_MS } from "../skipTyping.js";
|
|
5
5
|
import { codeDigits, completesCode, EMAIL_MAX_LENGTH, givenEmail, givenName, NAME_MAX_LENGTH, RESEND_AFTER_MS, resendClock, } from "../welcome.js";
|
|
6
6
|
import { useColor } from "./parts.js";
|
|
@@ -23,10 +23,16 @@ export function Choices({ items, question, sending, holding = false, failed, wor
|
|
|
23
23
|
onChoose(item);
|
|
24
24
|
} }, item.value))) }), failed ? (_jsx(Text, { role: "alert", className: "font-ui text-sm text-error-ink", children: words.choiceNotSent })) : null] }));
|
|
25
25
|
}
|
|
26
|
+
// The error under a field, as the field's description. React Native's types leave aria-describedby out, and
|
|
27
|
+
// react-native-web reads it.
|
|
28
|
+
function describedBy(errorId) {
|
|
29
|
+
return errorId === undefined ? {} : { 'aria-describedby': errorId };
|
|
30
|
+
}
|
|
26
31
|
// One thing asked of a new visitor: a field in focus and its button. What is typed is checked here and handed on.
|
|
27
32
|
function StepField({ label, error, submitLabel, input, usable, onSubmit, initial = '', aside }) {
|
|
28
33
|
const [typed, setTyped] = useState(initial);
|
|
29
34
|
const [refused, setRefused] = useState(false);
|
|
35
|
+
const errorId = useId();
|
|
30
36
|
const placeholder = useColor('ink-placeholder');
|
|
31
37
|
const submit = () => {
|
|
32
38
|
const value = usable(typed);
|
|
@@ -34,7 +40,7 @@ function StepField({ label, error, submitLabel, input, usable, onSubmit, initial
|
|
|
34
40
|
if (value !== null)
|
|
35
41
|
onSubmit(value);
|
|
36
42
|
};
|
|
37
|
-
return (_jsxs(View, { className: "w-full max-w-[360px] gap-2 self-center", children: [aside ? (_jsx(View, { className: "self-center", children: _jsx(Pill, { label: aside.label, look: "flat", onPress: aside.onPress }) })) : null, _jsx(Text, { className: "font-ui text-sm font-medium text-ink", children: label }), _jsx(TextInput, { autoFocus: true, value: typed, onChangeText: setTyped, onSubmitEditing: submit, accessibilityLabel: label, "aria-invalid": refused, placeholderTextColor: placeholder, className: `h-control rounded-control border bg-surface px-3 font-ui text-md text-ink ${refused ? 'border-error-ink' : 'border-line-control'}`, ...input }), refused ? (_jsx(Text, { role: "alert", className: "font-ui text-sm text-error-ink", children: error })) : null, _jsx(View, { className: "self-center", children: _jsx(Pill, { label: submitLabel, onPress: submit }) })] }));
|
|
43
|
+
return (_jsxs(View, { className: "w-full max-w-[360px] gap-2 self-center", children: [aside ? (_jsx(View, { className: "self-center", children: _jsx(Pill, { label: aside.label, look: "flat", onPress: aside.onPress }) })) : null, _jsx(Text, { className: "font-ui text-sm font-medium text-ink", children: label }), _jsx(TextInput, { autoFocus: true, value: typed, onChangeText: setTyped, onSubmitEditing: submit, accessibilityLabel: label, "aria-invalid": refused, ...describedBy(refused ? errorId : undefined), placeholderTextColor: placeholder, className: `h-control rounded-control border bg-surface px-3 font-ui text-md text-ink ${refused ? 'border-error-ink' : 'border-line-control'}`, ...input }), refused ? (_jsx(Text, { nativeID: errorId, role: "alert", className: "font-ui text-sm text-error-ink", children: error })) : null, _jsx(View, { className: "self-center", children: _jsx(Pill, { label: submitLabel, onPress: submit }) })] }));
|
|
38
44
|
}
|
|
39
45
|
/** The email asked for after Continue here, and again for a visitor changing it. */
|
|
40
46
|
export function EmailStep({ words, onSubmit, initial, }) {
|
|
@@ -68,23 +74,55 @@ function NewCode({ waitMs, words, onResend }) {
|
|
|
68
74
|
*/
|
|
69
75
|
export function CodeStep({ words, length, noMatch, ranOut, resendAfterMs = RESEND_AFTER_MS, onSubmit, onResend, onChangeEmail, }) {
|
|
70
76
|
const [code, setCode] = useState('');
|
|
77
|
+
const [focused, setFocused] = useState(false);
|
|
78
|
+
const errorId = useId();
|
|
71
79
|
const error = ranOut ? words.codeRanOut : noMatch ? words.codeNoMatch : null;
|
|
72
|
-
|
|
80
|
+
// The box the next digit goes in, or the last once the code is whole.
|
|
81
|
+
const nextBox = Math.min(code.length, length - 1);
|
|
82
|
+
return (_jsxs(View, { className: "w-full max-w-[360px] items-center gap-3 self-center", children: [_jsx(Text, { className: "font-ui text-sm font-medium text-ink", children: words.codeLabel }), _jsxs(View, { className: "relative", children: [_jsx(View, { "aria-hidden": true, className: "flex-row gap-2", children: Array.from({ length }, (_, box) => (_jsx(View, { className: `h-control w-11 items-center justify-center rounded-control border ${focused && box === nextBox ? 'border-focus' : error ? 'border-error-ink' : 'border-line-control'}`, children: _jsx(Text, { className: "font-ui text-xl font-medium text-ink", children: code[box] ?? '' }) }, box))) }), _jsx(TextInput, { autoFocus: true, value: code, onChangeText: (typed) => {
|
|
73
83
|
const digits = codeDigits(typed, length);
|
|
74
84
|
setCode(digits);
|
|
75
85
|
if (completesCode(digits, code, length))
|
|
76
86
|
onSubmit(digits);
|
|
77
|
-
},
|
|
87
|
+
}, onFocus: () => {
|
|
88
|
+
setFocused(true);
|
|
89
|
+
}, onBlur: () => {
|
|
90
|
+
setFocused(false);
|
|
91
|
+
}, accessibilityLabel: words.codeLabel, "aria-invalid": error !== null, ...describedBy(error === null ? undefined : errorId), inputMode: "numeric", autoComplete: "one-time-code", textContentType: "oneTimeCode", caretHidden: true,
|
|
92
|
+
// Unseen, not just clear: the boxes draw the digits and the focus, and a checker would read clear text as
|
|
93
|
+
// text with no contrast.
|
|
94
|
+
className: "absolute inset-0 opacity-0" })] }), error ? (_jsx(Text, { nativeID: errorId, role: "alert", className: "text-center font-ui text-sm text-error-ink", children: error })) : null, _jsxs(View, { className: "flex-row flex-wrap items-center justify-center gap-2", children: [_jsx(Pill, { label: words.changeEmail, look: "flat", onPress: onChangeEmail }), _jsx(NewCode, { waitMs: resendAfterMs, words: words, onResend: onResend }), _jsx(Pill, { label: words.codeSubmit, disabled: code.length < length, onPress: () => {
|
|
78
95
|
onSubmit(code);
|
|
79
96
|
} })] })] }));
|
|
80
97
|
}
|
|
81
98
|
const DOTS = [0, 1, 2];
|
|
82
99
|
const DOT_SIZE = 6;
|
|
100
|
+
// A dot at rest is faint, rises this far at the top of its turn, and holds at the still strength for a visitor who
|
|
101
|
+
// asks for less motion.
|
|
102
|
+
export const DOT_REST_OPACITY = 0.35;
|
|
103
|
+
export const DOT_RISE = 3;
|
|
104
|
+
export const DOT_STILL_OPACITY = 0.6;
|
|
105
|
+
// Null until the device has answered, so the dots never start before a visitor who wants them still is known.
|
|
106
|
+
function useReducedMotion() {
|
|
107
|
+
const [reduced, setReduced] = useState(null);
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
void AccessibilityInfo.isReduceMotionEnabled().then(setReduced);
|
|
110
|
+
const change = AccessibilityInfo.addEventListener('reduceMotionChanged', setReduced);
|
|
111
|
+
return () => {
|
|
112
|
+
change.remove();
|
|
113
|
+
};
|
|
114
|
+
}, []);
|
|
115
|
+
return reduced;
|
|
116
|
+
}
|
|
83
117
|
/** Skip writing a reply: three dots rising in turn in Skip's bubble beside Skip's mark, announced as a status. */
|
|
84
118
|
export function SkipTyping({ label, skipMark }) {
|
|
85
119
|
const dotColor = useColor('ink-secondary');
|
|
120
|
+
const reduced = useReducedMotion();
|
|
121
|
+
const still = reduced === true;
|
|
86
122
|
const rise = useRef(DOTS.map(() => new Animated.Value(0))).current;
|
|
87
123
|
useEffect(() => {
|
|
124
|
+
if (reduced !== false)
|
|
125
|
+
return undefined;
|
|
88
126
|
const step = DOT_CYCLE_MS / (2 * DOTS.length);
|
|
89
127
|
const loop = Animated.loop(Animated.stagger(step, rise.map((value) => Animated.sequence([
|
|
90
128
|
Animated.timing(value, { toValue: 1, duration: step, useNativeDriver: true }),
|
|
@@ -94,12 +132,17 @@ export function SkipTyping({ label, skipMark }) {
|
|
|
94
132
|
return () => {
|
|
95
133
|
loop.stop();
|
|
96
134
|
};
|
|
97
|
-
}, [rise]);
|
|
135
|
+
}, [rise, reduced]);
|
|
98
136
|
return (_jsxs(View, { className: "mt-3 flex-row items-end gap-2", children: [_jsx(Image, { source: skipMark, "aria-hidden": true, style: { width: 32, height: 32 }, className: "rounded-control" }), _jsx(View, { role: "status", accessibilityLabel: label, className: "flex-row items-center gap-1 rounded-[20px] rounded-bl-[7px] border border-line-subtle bg-surface px-4 py-4", children: rise.map((value, dot) => (_jsx(Animated.View, { style: {
|
|
99
137
|
width: DOT_SIZE,
|
|
100
138
|
height: DOT_SIZE,
|
|
101
139
|
borderRadius: DOT_SIZE / 2,
|
|
102
140
|
backgroundColor: dotColor,
|
|
103
|
-
|
|
141
|
+
...(still
|
|
142
|
+
? { opacity: DOT_STILL_OPACITY }
|
|
143
|
+
: {
|
|
144
|
+
opacity: value.interpolate({ inputRange: [0, 1], outputRange: [DOT_REST_OPACITY, 1] }),
|
|
145
|
+
transform: [{ translateY: value.interpolate({ inputRange: [0, 1], outputRange: [0, -DOT_RISE] }) }],
|
|
146
|
+
}),
|
|
104
147
|
} }, DOTS[dot]))) })] }));
|
|
105
148
|
}
|
|
Binary file
|
package/manifest.json
CHANGED
|
@@ -409,6 +409,10 @@
|
|
|
409
409
|
"sha256": "6606d135bbbd06103bacef4cb89bf4019e53e092dcd19b51278edcbab1786b9d",
|
|
410
410
|
"bytes": 130031
|
|
411
411
|
},
|
|
412
|
+
"logos/png-2048/skips-wordmark-dark-2048.png": {
|
|
413
|
+
"sha256": "286ed3c2b3d37b8559ab773a692eca07d04d843ae093820c94ce5bdadff78713",
|
|
414
|
+
"bytes": 46330
|
|
415
|
+
},
|
|
412
416
|
"logos/png-2048/skips-wordmark-light-2048.png": {
|
|
413
417
|
"sha256": "61da312bae3aec7dddef398494fa98d13de940c81b3ecdb038c6904d26d85506",
|
|
414
418
|
"bytes": 59707
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joinskips/brand",
|
|
3
|
-
"version": "5.0.0-develop.
|
|
3
|
+
"version": "5.0.0-develop.120",
|
|
4
4
|
"description": "Skips design tokens, Onest, logos, favicons, interface icons, member pictures and app icons.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"homepage": "https://brand.joinskips.com",
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
"validator": "^13.15.35"
|
|
21
21
|
},
|
|
22
22
|
"peerDependencies": {
|
|
23
|
-
"react": "^19.
|
|
23
|
+
"react": "^19.0.0",
|
|
24
24
|
"react-native": ">=0.81.0",
|
|
25
|
-
"react-native-web": "~0.21.
|
|
26
|
-
"tailwindcss": "^4.
|
|
25
|
+
"react-native-web": "~0.21.0",
|
|
26
|
+
"tailwindcss": "^4.0.0",
|
|
27
27
|
"uniwind": "1.13.2"
|
|
28
28
|
},
|
|
29
29
|
"peerDependenciesMeta": {
|