mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-03 23:23:21 +00:00
* feat(plugins): unify source installation and simplify plugin settings * feat(plugins): install npm sources and review CLI updates Treat install selectors as one-time choices. Keep source identity separate from the installed revision, and resolve ordinary updates from the current upstream default. npm only acquires directories; plugin loading remains independent of npm. * fix(plugins): preserve CLI diagnostics and exercise reviewed updates Move new source capability coverage to real browser journeys. Update the requirements regression to exercise preview and apply, and preserve the existing duplicate-selector diagnostic across positional option scopes. * test(plugins): remove unused pending helper * test(plugins): pack registry fixtures with the Windows command runner * test(plugins): assert failed client contribution setup * docs(plugins): consolidate guides and document npm publishing * docs(plugins): introduce publishing choices before npm instructions * test(plugins): own browser journey cleanup in a fixture * feat(plugins): prepare scaffolded packages for publishing
300 lines
7.4 KiB
TypeScript
300 lines
7.4 KiB
TypeScript
import {
|
|
forwardRef,
|
|
useCallback,
|
|
useMemo,
|
|
useState,
|
|
type ForwardedRef,
|
|
type ReactNode,
|
|
} from "react";
|
|
import {
|
|
Pressable,
|
|
StyleSheet as RNStyleSheet,
|
|
Text,
|
|
View,
|
|
type PressableStateCallbackType,
|
|
type TextStyle,
|
|
type ViewStyle,
|
|
} from "react-native";
|
|
import { StyleSheet } from "react-native-unistyles";
|
|
import { AdaptiveTextInput, type AdaptiveTextInputProps } from "@/components/adaptive-modal-sheet";
|
|
import type { EditingTextInputHandle } from "@/components/ui/text-input";
|
|
import {
|
|
createControlGeometry,
|
|
resolveControlInteractionStyles,
|
|
type FieldControlSize,
|
|
} from "@/components/ui/control-geometry";
|
|
|
|
interface FieldProps {
|
|
label: string;
|
|
children: ReactNode;
|
|
trailing?: ReactNode;
|
|
hint?: string;
|
|
error?: string | null;
|
|
testID?: string;
|
|
}
|
|
|
|
export function Field({ label, children, trailing, hint, error, testID }: FieldProps) {
|
|
const hintTestID = useMemo(() => (testID ? `${testID}-hint` : undefined), [testID]);
|
|
const errorTestID = useMemo(() => (testID ? `${testID}-error` : undefined), [testID]);
|
|
const subtext = useMemo(() => {
|
|
if (error) {
|
|
return (
|
|
<Text numberOfLines={1} style={styles.errorText} testID={errorTestID}>
|
|
{error}
|
|
</Text>
|
|
);
|
|
}
|
|
if (hint) {
|
|
return (
|
|
<Text style={styles.hintText} testID={hintTestID}>
|
|
{hint}
|
|
</Text>
|
|
);
|
|
}
|
|
return null;
|
|
}, [error, hint, errorTestID, hintTestID]);
|
|
|
|
return (
|
|
<View style={styles.container} testID={testID}>
|
|
<View style={styles.header}>
|
|
<Text style={styles.label}>{label}</Text>
|
|
{trailing}
|
|
</View>
|
|
{children}
|
|
{subtext}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
type FormTextInputProps = AdaptiveTextInputProps & {
|
|
size?: FieldControlSize;
|
|
};
|
|
|
|
type FlatFormTextInputStyle = ViewStyle & TextStyle;
|
|
|
|
interface SplitFormTextInputStyle {
|
|
chromeStyle?: ViewStyle;
|
|
inputStyle?: TextStyle;
|
|
}
|
|
|
|
function splitFormTextInputStyle(style: AdaptiveTextInputProps["style"]): SplitFormTextInputStyle {
|
|
const flattened = RNStyleSheet.flatten(style) as FlatFormTextInputStyle | undefined;
|
|
if (!flattened) {
|
|
return {};
|
|
}
|
|
|
|
const {
|
|
color,
|
|
fontFamily,
|
|
fontSize,
|
|
fontStyle,
|
|
fontVariant,
|
|
fontWeight,
|
|
includeFontPadding,
|
|
letterSpacing,
|
|
lineHeight,
|
|
textAlign,
|
|
textAlignVertical,
|
|
textDecorationColor,
|
|
textDecorationLine,
|
|
textDecorationStyle,
|
|
textShadowColor,
|
|
textShadowOffset,
|
|
textShadowRadius,
|
|
textTransform,
|
|
writingDirection,
|
|
...chromeStyle
|
|
} = flattened;
|
|
|
|
const inputStyle: TextStyle = {
|
|
color,
|
|
fontFamily,
|
|
fontSize,
|
|
fontStyle,
|
|
fontVariant,
|
|
fontWeight,
|
|
includeFontPadding,
|
|
letterSpacing,
|
|
lineHeight,
|
|
textAlign,
|
|
textAlignVertical,
|
|
textDecorationColor,
|
|
textDecorationLine,
|
|
textDecorationStyle,
|
|
textShadowColor,
|
|
textShadowOffset,
|
|
textShadowRadius,
|
|
textTransform,
|
|
writingDirection,
|
|
};
|
|
|
|
return {
|
|
chromeStyle: stripUnistylesMetadata(chromeStyle),
|
|
inputStyle: stripUnistylesMetadata(inputStyle),
|
|
};
|
|
}
|
|
|
|
function stripUnistylesMetadata<TStyle extends object>(style: TStyle): TStyle {
|
|
const cleanStyle: Record<string, unknown> = {};
|
|
for (const [key, value] of Object.entries(style as Record<string, unknown>)) {
|
|
if (key.startsWith("unistyles_") || value === undefined) {
|
|
continue;
|
|
}
|
|
cleanStyle[key] = value;
|
|
}
|
|
return cleanStyle as TStyle;
|
|
}
|
|
|
|
function assignTextInputRef(
|
|
forwardedRef: ForwardedRef<EditingTextInputHandle>,
|
|
node: EditingTextInputHandle | null,
|
|
): void {
|
|
if (typeof forwardedRef === "function") {
|
|
forwardedRef(node);
|
|
return;
|
|
}
|
|
if (forwardedRef) {
|
|
forwardedRef.current = node;
|
|
}
|
|
}
|
|
|
|
export const FormTextInput = forwardRef<EditingTextInputHandle, FormTextInputProps>(
|
|
function FormTextInput({ size = "md", style, onFocus, onBlur, editable, ...props }, ref) {
|
|
const [focused, setFocused] = useState(false);
|
|
const isDisabled = editable === false;
|
|
const chromeSizeStyle = size === "sm" ? formInputStyles.chromeSm : formInputStyles.chromeMd;
|
|
const inputSizeStyle = size === "sm" ? formInputStyles.inputSm : formInputStyles.inputMd;
|
|
const splitStyle = useMemo(() => splitFormTextInputStyle(style), [style]);
|
|
const setInputRef = useCallback(
|
|
(node: EditingTextInputHandle | null) => {
|
|
assignTextInputRef(ref, node);
|
|
},
|
|
[ref],
|
|
);
|
|
const handleFocus = useCallback<NonNullable<AdaptiveTextInputProps["onFocus"]>>(
|
|
(event) => {
|
|
setFocused(true);
|
|
onFocus?.(event);
|
|
},
|
|
[onFocus],
|
|
);
|
|
const handleBlur = useCallback<NonNullable<AdaptiveTextInputProps["onBlur"]>>(
|
|
(event) => {
|
|
setFocused(false);
|
|
onBlur?.(event);
|
|
},
|
|
[onBlur],
|
|
);
|
|
const inputStyle = useMemo(
|
|
() => [formInputStyles.input, inputSizeStyle, splitStyle.inputStyle],
|
|
[inputSizeStyle, splitStyle.inputStyle],
|
|
) as AdaptiveTextInputProps["style"];
|
|
const chromeStyle = useCallback(
|
|
({ hovered = false }: PressableStateCallbackType & { hovered?: boolean }) => [
|
|
formInputStyles.chrome,
|
|
chromeSizeStyle,
|
|
resolveControlInteractionStyles(
|
|
{
|
|
controlRest: formInputStyles.controlRest,
|
|
controlHover: formInputStyles.controlHover,
|
|
controlActive: formInputStyles.controlActive,
|
|
controlDisabled: formInputStyles.controlDisabled,
|
|
},
|
|
{
|
|
hovered,
|
|
focused,
|
|
disabled: isDisabled,
|
|
},
|
|
),
|
|
splitStyle.chromeStyle,
|
|
],
|
|
[chromeSizeStyle, focused, isDisabled, splitStyle.chromeStyle],
|
|
);
|
|
|
|
return (
|
|
<Pressable disabled={isDisabled} style={chromeStyle}>
|
|
<AdaptiveTextInput
|
|
ref={setInputRef}
|
|
editable={editable}
|
|
{...props}
|
|
onBlur={handleBlur}
|
|
onFocus={handleFocus}
|
|
style={inputStyle}
|
|
/>
|
|
</Pressable>
|
|
);
|
|
},
|
|
);
|
|
|
|
const styles = StyleSheet.create((theme) => ({
|
|
container: {
|
|
gap: theme.spacing[2],
|
|
},
|
|
header: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
gap: theme.spacing[2],
|
|
},
|
|
label: {
|
|
flexShrink: 1,
|
|
minWidth: 0,
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.base,
|
|
fontWeight: theme.fontWeight.normal,
|
|
},
|
|
hintText: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.sm,
|
|
lineHeight: Math.round(theme.fontSize.sm * 1.4),
|
|
},
|
|
errorText: {
|
|
color: theme.colors.palette.red[300],
|
|
fontSize: theme.fontSize.sm,
|
|
lineHeight: Math.round(theme.fontSize.sm * 1.4),
|
|
},
|
|
}));
|
|
|
|
const formInputStyles = StyleSheet.create((theme) => {
|
|
const geometry = createControlGeometry(theme);
|
|
|
|
return {
|
|
chrome: {
|
|
backgroundColor: theme.colors.surface2,
|
|
},
|
|
chromeSm: {
|
|
...geometry.fieldControlSm,
|
|
},
|
|
chromeMd: {
|
|
...geometry.fieldControlMd,
|
|
},
|
|
controlRest: {
|
|
...geometry.controlRest,
|
|
},
|
|
controlHover: {
|
|
...geometry.controlHover,
|
|
},
|
|
controlActive: {
|
|
...geometry.controlActive,
|
|
},
|
|
controlDisabled: {
|
|
...geometry.controlDisabled,
|
|
},
|
|
input: {
|
|
flex: 1,
|
|
minWidth: 0,
|
|
color: theme.colors.foreground,
|
|
paddingHorizontal: 0,
|
|
paddingVertical: 0,
|
|
outlineColor: "transparent",
|
|
outlineWidth: 0,
|
|
},
|
|
inputSm: {
|
|
...geometry.fieldTextSm,
|
|
},
|
|
inputMd: {
|
|
...geometry.fieldTextMd,
|
|
},
|
|
};
|
|
});
|