@dloizides/ui-forms 1.1.0 → 1.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.3.0
4
+
5
+ - `ChipSelector` gains a `variant` prop (`'solid' | 'outline'`, default `'solid'`). `solid` is the
6
+ original filled-pill look (selected = solid brand fill, white ink) — existing consumers
7
+ (erevna / katalogos / kefi) render **identically**, no change. `outline` is the AML v1 console
8
+ look: an outlined pill on a muted fill where the **selected** state is a subtle tinted-outline
9
+ (brand border + brand ink + a low-alpha brand wash), NOT a solid fill; on web, hover lifts the
10
+ border + ink to the brand colour. Fully theme-driven (brand colour + alpha, border/text from the
11
+ theme), so it re-themes per tenant. Additive + backward-compatible.
12
+
13
+ ## 1.2.0
14
+
15
+ - Add `ThemedTextInput` + `useThemedInput` — the shared "readable + lively" text-input treatment,
16
+ driven entirely by the active theme (re-themes per tenant, no hard-coded hex). At rest: dark
17
+ readable text on a subtle off-white surface with a soft border. On hover (web): the border
18
+ strengthens a touch. On focus: the border becomes the brand primary, the background brightens to
19
+ the plain surface (white), and a soft 3px brand-tinted focus ring fades in. The animation is a
20
+ pure GPU-cheap CSS transition on `border-color`/`background-color`/`box-shadow` (the ring is a
21
+ `box-shadow`, so no layout/reflow) — NO `Animated`/`LayoutAnimation`/JS timers. Web-only style
22
+ keys and the mouse-hover handlers are guarded behind `Platform.OS === 'web'`, so it stays
23
+ native-safe (native applies the state-driven colours instantly).
24
+ - `FormField` now renders `ThemedTextInput`, so every field gets the treatment for free; its label
25
+ uses `textSecondary` at 13px/600 for readability. Props/behaviour (label, hint, error, testID,
26
+ a11y, `onFocus`/`onBlur` pass-through) are backward-compatible.
27
+
3
28
  ## 1.1.0
4
29
 
5
30
  - Add `FormCheckbox` — a native-safe (`Pressable`-based, RN-web + native) themed checkbox: a rounded
package/dist/index.d.mts CHANGED
@@ -1,8 +1,12 @@
1
1
  import React from 'react';
2
- import { TextInputProps, ViewStyle } from 'react-native';
2
+ import { TextInputProps, ViewStyle, TextInput, TextStyle } from 'react-native';
3
3
 
4
4
  /**
5
5
  * Reusable form field — labelled text input with optional required mark + error.
6
+ *
7
+ * The input itself is the shared `ThemedTextInput` primitive, so every FormField gets the
8
+ * readable + lively treatment (dark text, subtle off-white rest background, brand focus
9
+ * animation with a soft ring) for free, driven by the active theme.
6
10
  */
7
11
 
8
12
  interface FormFieldProps extends Omit<TextInputProps, 'style'> {
@@ -13,10 +17,66 @@ interface FormFieldProps extends Omit<TextInputProps, 'style'> {
13
17
  }
14
18
  declare const FormField: ({ label, required, error, containerStyle, ...textInputProps }: FormFieldProps) => React.ReactElement;
15
19
 
20
+ /**
21
+ * `ThemedTextInput` — the shared, readable + lively text-input primitive. A thin `<TextInput>`
22
+ * wrapper that applies the `useThemedInput` treatment (dark readable text, subtle off-white rest
23
+ * background, and the brand focus animation: indigo border + white background + soft 3px ring,
24
+ * eased over .14s via a GPU-cheap CSS transition on web; instant, native-safe off web).
25
+ *
26
+ * It is the ONE source of truth for the input look: `FormField` renders it, and raw filter inputs
27
+ * (search / country / year / date fallbacks) use it directly instead of forking the styles.
28
+ *
29
+ * Every existing `TextInputProps` (value, placeholder, onChangeText, testID, accessibility…) is
30
+ * forwarded untouched; `onFocus`/`onBlur` you pass are composed with the internal focus state.
31
+ */
32
+
33
+ interface ThemedTextInputProps extends TextInputProps {
34
+ /** Render with the error border colour (matches `FormField`'s error treatment). */
35
+ hasError?: boolean;
36
+ }
37
+ declare const ThemedTextInput: React.ForwardRefExoticComponent<ThemedTextInputProps & React.RefAttributes<TextInput>>;
38
+
39
+ /** The exact focus/blur event types of the installed `react-native` `TextInput` (version-safe). */
40
+ type FocusEvent = Parameters<NonNullable<TextInputProps['onFocus']>>[0];
41
+ type BlurEvent = Parameters<NonNullable<TextInputProps['onBlur']>>[0];
42
+ interface UseThemedInputOptions {
43
+ /** Render with the error border colour (the brand focus border is suppressed while errored). */
44
+ hasError?: boolean;
45
+ /** Consumer focus handler — composed with the internal focus-state toggle. */
46
+ onFocus?: (e: FocusEvent) => void;
47
+ /** Consumer blur handler — composed with the internal focus-state toggle. */
48
+ onBlur?: (e: BlurEvent) => void;
49
+ }
50
+ interface ThemedInput {
51
+ /** The themed input style for the current focus/hover state (memoised). Merge AFTER your box style. */
52
+ style: TextStyle;
53
+ /** A readable mid-grey placeholder colour from the theme. */
54
+ placeholderTextColor: string;
55
+ isFocused: boolean;
56
+ isHovered: boolean;
57
+ /** Spread/attach onto the `<TextInput>` to drive the focus treatment. */
58
+ focusBind: {
59
+ onFocus: (e: FocusEvent) => void;
60
+ onBlur: (e: BlurEvent) => void;
61
+ };
62
+ /** Web-only mouse-hover handlers, pre-typed for `<TextInput>` (an empty object on native). */
63
+ hoverBind: TextInputProps;
64
+ }
65
+ declare function useThemedInput(options?: UseThemedInputOptions): ThemedInput;
66
+
16
67
  /**
17
68
  * Chip/pill selector for selecting one or multiple options.
69
+ *
70
+ * Two visual variants (both fully theme-driven):
71
+ * - `solid` (default) — the original filled-pill look: selected = solid primary fill with
72
+ * white ink. Unchanged, so existing consumers (erevna / katalogos / kefi) render identically.
73
+ * - `outline` — the AML v1 console look: an outlined pill on a muted fill; selected = a subtle
74
+ * tinted-outline (primary border + primary ink + a low-alpha primary wash), NOT a solid fill.
75
+ * Hover (web) lifts the border/ink to the primary colour.
18
76
  */
19
77
 
78
+ /** Visual variant. `solid` = filled selected pill (default); `outline` = v1 tinted-outline. */
79
+ type ChipVariant = 'solid' | 'outline';
20
80
  interface ChipOption<T> {
21
81
  value: T;
22
82
  label: string;
@@ -28,9 +88,11 @@ interface ChipSelectorProps<T> {
28
88
  onChange: (value: T) => void;
29
89
  multiple?: boolean;
30
90
  disabled?: boolean;
91
+ /** Visual variant. `solid` (default) = filled selected pill; `outline` = v1 tinted-outline. */
92
+ variant?: ChipVariant;
31
93
  containerStyle?: ViewStyle;
32
94
  }
33
- declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, containerStyle, }: ChipSelectorProps<T>) => React.ReactElement;
95
+ declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, variant, containerStyle, }: ChipSelectorProps<T>) => React.ReactElement;
34
96
 
35
97
  /**
36
98
  * Reusable form switch/toggle with an optional description.
@@ -73,4 +135,4 @@ interface FormCheckboxProps {
73
135
  }
74
136
  declare const FormCheckbox: ({ label, value, onValueChange, onChange, hint, description, disabled, containerStyle, testID, accessibilityLabel, accessibilityHint, }: FormCheckboxProps) => React.ReactElement;
75
137
 
76
- export { type ChipOption, ChipSelector, type ChipSelectorProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormSwitch, type FormSwitchProps };
138
+ export { type ChipOption, ChipSelector, type ChipSelectorProps, type ChipVariant, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormSwitch, type FormSwitchProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, type UseThemedInputOptions, useThemedInput };
package/dist/index.d.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  import React from 'react';
2
- import { TextInputProps, ViewStyle } from 'react-native';
2
+ import { TextInputProps, ViewStyle, TextInput, TextStyle } from 'react-native';
3
3
 
4
4
  /**
5
5
  * Reusable form field — labelled text input with optional required mark + error.
6
+ *
7
+ * The input itself is the shared `ThemedTextInput` primitive, so every FormField gets the
8
+ * readable + lively treatment (dark text, subtle off-white rest background, brand focus
9
+ * animation with a soft ring) for free, driven by the active theme.
6
10
  */
7
11
 
8
12
  interface FormFieldProps extends Omit<TextInputProps, 'style'> {
@@ -13,10 +17,66 @@ interface FormFieldProps extends Omit<TextInputProps, 'style'> {
13
17
  }
14
18
  declare const FormField: ({ label, required, error, containerStyle, ...textInputProps }: FormFieldProps) => React.ReactElement;
15
19
 
20
+ /**
21
+ * `ThemedTextInput` — the shared, readable + lively text-input primitive. A thin `<TextInput>`
22
+ * wrapper that applies the `useThemedInput` treatment (dark readable text, subtle off-white rest
23
+ * background, and the brand focus animation: indigo border + white background + soft 3px ring,
24
+ * eased over .14s via a GPU-cheap CSS transition on web; instant, native-safe off web).
25
+ *
26
+ * It is the ONE source of truth for the input look: `FormField` renders it, and raw filter inputs
27
+ * (search / country / year / date fallbacks) use it directly instead of forking the styles.
28
+ *
29
+ * Every existing `TextInputProps` (value, placeholder, onChangeText, testID, accessibility…) is
30
+ * forwarded untouched; `onFocus`/`onBlur` you pass are composed with the internal focus state.
31
+ */
32
+
33
+ interface ThemedTextInputProps extends TextInputProps {
34
+ /** Render with the error border colour (matches `FormField`'s error treatment). */
35
+ hasError?: boolean;
36
+ }
37
+ declare const ThemedTextInput: React.ForwardRefExoticComponent<ThemedTextInputProps & React.RefAttributes<TextInput>>;
38
+
39
+ /** The exact focus/blur event types of the installed `react-native` `TextInput` (version-safe). */
40
+ type FocusEvent = Parameters<NonNullable<TextInputProps['onFocus']>>[0];
41
+ type BlurEvent = Parameters<NonNullable<TextInputProps['onBlur']>>[0];
42
+ interface UseThemedInputOptions {
43
+ /** Render with the error border colour (the brand focus border is suppressed while errored). */
44
+ hasError?: boolean;
45
+ /** Consumer focus handler — composed with the internal focus-state toggle. */
46
+ onFocus?: (e: FocusEvent) => void;
47
+ /** Consumer blur handler — composed with the internal focus-state toggle. */
48
+ onBlur?: (e: BlurEvent) => void;
49
+ }
50
+ interface ThemedInput {
51
+ /** The themed input style for the current focus/hover state (memoised). Merge AFTER your box style. */
52
+ style: TextStyle;
53
+ /** A readable mid-grey placeholder colour from the theme. */
54
+ placeholderTextColor: string;
55
+ isFocused: boolean;
56
+ isHovered: boolean;
57
+ /** Spread/attach onto the `<TextInput>` to drive the focus treatment. */
58
+ focusBind: {
59
+ onFocus: (e: FocusEvent) => void;
60
+ onBlur: (e: BlurEvent) => void;
61
+ };
62
+ /** Web-only mouse-hover handlers, pre-typed for `<TextInput>` (an empty object on native). */
63
+ hoverBind: TextInputProps;
64
+ }
65
+ declare function useThemedInput(options?: UseThemedInputOptions): ThemedInput;
66
+
16
67
  /**
17
68
  * Chip/pill selector for selecting one or multiple options.
69
+ *
70
+ * Two visual variants (both fully theme-driven):
71
+ * - `solid` (default) — the original filled-pill look: selected = solid primary fill with
72
+ * white ink. Unchanged, so existing consumers (erevna / katalogos / kefi) render identically.
73
+ * - `outline` — the AML v1 console look: an outlined pill on a muted fill; selected = a subtle
74
+ * tinted-outline (primary border + primary ink + a low-alpha primary wash), NOT a solid fill.
75
+ * Hover (web) lifts the border/ink to the primary colour.
18
76
  */
19
77
 
78
+ /** Visual variant. `solid` = filled selected pill (default); `outline` = v1 tinted-outline. */
79
+ type ChipVariant = 'solid' | 'outline';
20
80
  interface ChipOption<T> {
21
81
  value: T;
22
82
  label: string;
@@ -28,9 +88,11 @@ interface ChipSelectorProps<T> {
28
88
  onChange: (value: T) => void;
29
89
  multiple?: boolean;
30
90
  disabled?: boolean;
91
+ /** Visual variant. `solid` (default) = filled selected pill; `outline` = v1 tinted-outline. */
92
+ variant?: ChipVariant;
31
93
  containerStyle?: ViewStyle;
32
94
  }
33
- declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, containerStyle, }: ChipSelectorProps<T>) => React.ReactElement;
95
+ declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, variant, containerStyle, }: ChipSelectorProps<T>) => React.ReactElement;
34
96
 
35
97
  /**
36
98
  * Reusable form switch/toggle with an optional description.
@@ -73,4 +135,4 @@ interface FormCheckboxProps {
73
135
  }
74
136
  declare const FormCheckbox: ({ label, value, onValueChange, onChange, hint, description, disabled, containerStyle, testID, accessibilityLabel, accessibilityHint, }: FormCheckboxProps) => React.ReactElement;
75
137
 
76
- export { type ChipOption, ChipSelector, type ChipSelectorProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormSwitch, type FormSwitchProps };
138
+ export { type ChipOption, ChipSelector, type ChipSelectorProps, type ChipVariant, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormSwitch, type FormSwitchProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, type UseThemedInputOptions, useThemedInput };
package/dist/index.js CHANGED
@@ -1,29 +1,142 @@
1
1
  'use strict';
2
2
 
3
- var React = require('react');
3
+ var React3 = require('react');
4
4
  var reactNative = require('react-native');
5
5
  var uiFeedback = require('@dloizides/ui-feedback');
6
6
  var jsxRuntime = require('react/jsx-runtime');
7
7
 
8
8
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
9
 
10
- var React__default = /*#__PURE__*/_interopDefault(React);
10
+ var React3__default = /*#__PURE__*/_interopDefault(React3);
11
11
 
12
12
  // src/FormField/FormField.tsx
13
+ var IS_WEB = reactNative.Platform.OS === "web";
14
+ var TRANSITION_PROPERTY = "border-color, background-color, box-shadow";
15
+ var TRANSITION_DURATION = "0.14s";
16
+ var TRANSITION_TIMING = "ease";
17
+ var FOCUS_RING_WIDTH = 3;
18
+ var FOCUS_RING_ALPHA = 0.16;
19
+ var HOVER_BORDER_ALPHA = 0.22;
20
+ var HEX_RADIX = 16;
21
+ var HEX_SHORT_LENGTH = 4;
22
+ var HEX_FULL_LENGTH = 7;
23
+ var RED_START = 1;
24
+ var GREEN_START = 3;
25
+ var BLUE_START = 5;
26
+ var CHANNEL_END_OFFSET = 2;
27
+ function withAlpha(color, alpha) {
28
+ const hex = color.trim();
29
+ const isHex = hex.charAt(0) === "#" && (hex.length === HEX_FULL_LENGTH || hex.length === HEX_SHORT_LENGTH);
30
+ if (!isHex) return color;
31
+ const full = hex.length === HEX_SHORT_LENGTH ? `#${hex.charAt(1)}${hex.charAt(1)}${hex.charAt(2)}${hex.charAt(2)}${hex.charAt(3)}${hex.charAt(3)}` : hex;
32
+ const r = parseInt(full.slice(RED_START, RED_START + CHANNEL_END_OFFSET), HEX_RADIX);
33
+ const g = parseInt(full.slice(GREEN_START, GREEN_START + CHANNEL_END_OFFSET), HEX_RADIX);
34
+ const b = parseInt(full.slice(BLUE_START, BLUE_START + CHANNEL_END_OFFSET), HEX_RADIX);
35
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
36
+ }
37
+ function useThemedInput(options = {}) {
38
+ const { hasError = false, onFocus, onBlur } = options;
39
+ const { theme } = uiFeedback.useUi();
40
+ const { colors, palette, semantic } = theme;
41
+ const [isFocused, setIsFocused] = React3.useState(false);
42
+ const [isHovered, setIsHovered] = React3.useState(false);
43
+ const handleFocus = React3.useCallback(
44
+ (e) => {
45
+ setIsFocused(true);
46
+ onFocus?.(e);
47
+ },
48
+ [onFocus]
49
+ );
50
+ const handleBlur = React3.useCallback(
51
+ (e) => {
52
+ setIsFocused(false);
53
+ onBlur?.(e);
54
+ },
55
+ [onBlur]
56
+ );
57
+ const handleHoverIn = React3.useCallback(() => setIsHovered(true), []);
58
+ const handleHoverOut = React3.useCallback(() => setIsHovered(false), []);
59
+ const primary = palette.primary["500"];
60
+ const errorColor = semantic.error["500"];
61
+ const style = React3.useMemo(() => {
62
+ const restBorder = hasError ? errorColor : colors.border;
63
+ const focusBorder = hasError ? errorColor : primary;
64
+ const borderColor = isFocused ? focusBorder : isHovered ? withAlpha(colors.text, HOVER_BORDER_ALPHA) : restBorder;
65
+ const backgroundColor = isFocused ? colors.surface : colors.surfaceElevated;
66
+ const base = { color: colors.text, borderColor, backgroundColor };
67
+ if (!IS_WEB) return base;
68
+ const ring = isFocused && !hasError ? `0 0 0 ${FOCUS_RING_WIDTH}px ${withAlpha(primary, FOCUS_RING_ALPHA)}` : "none";
69
+ const webStyle = {
70
+ transitionProperty: TRANSITION_PROPERTY,
71
+ transitionDuration: TRANSITION_DURATION,
72
+ transitionTimingFunction: TRANSITION_TIMING,
73
+ boxShadow: ring,
74
+ outlineStyle: "none"
75
+ };
76
+ return { ...base, ...webStyle };
77
+ }, [
78
+ isFocused,
79
+ isHovered,
80
+ hasError,
81
+ colors.text,
82
+ colors.border,
83
+ colors.surface,
84
+ colors.surfaceElevated,
85
+ primary,
86
+ errorColor
87
+ ]);
88
+ const focusBind = React3.useMemo(() => ({ onFocus: handleFocus, onBlur: handleBlur }), [handleFocus, handleBlur]);
89
+ const hoverBind = React3.useMemo(
90
+ () => IS_WEB ? { onMouseEnter: handleHoverIn, onMouseLeave: handleHoverOut } : {},
91
+ [handleHoverIn, handleHoverOut]
92
+ );
93
+ return {
94
+ style,
95
+ placeholderTextColor: colors.textSecondary,
96
+ isFocused,
97
+ isHovered,
98
+ focusBind,
99
+ hoverBind
100
+ };
101
+ }
102
+ var ThemedTextInput = React3__default.default.forwardRef(function ThemedTextInput2({ hasError = false, onFocus, onBlur, style, placeholderTextColor, ...rest }, ref) {
103
+ const {
104
+ style: themedStyle,
105
+ placeholderTextColor: themedPlaceholder,
106
+ focusBind,
107
+ hoverBind
108
+ } = useThemedInput({ hasError, onFocus, onBlur });
109
+ return /* @__PURE__ */ jsxRuntime.jsx(
110
+ reactNative.TextInput,
111
+ {
112
+ ref,
113
+ placeholderTextColor: placeholderTextColor ?? themedPlaceholder,
114
+ ...rest,
115
+ ...hoverBind,
116
+ onFocus: focusBind.onFocus,
117
+ onBlur: focusBind.onBlur,
118
+ style: [style, themedStyle]
119
+ }
120
+ );
121
+ });
13
122
  var CONTAINER_MARGIN_BOTTOM = 16;
14
- var INPUT_PADDING = 12;
123
+ var INPUT_PADDING_V = 10;
124
+ var INPUT_PADDING_H = 12;
15
125
  var INPUT_BORDER_RADIUS = 8;
16
126
  var ERROR_FONT_SIZE = 12;
127
+ var LABEL_FONT_SIZE = 13;
17
128
  var styles = reactNative.StyleSheet.create({
18
129
  container: {
19
130
  marginBottom: CONTAINER_MARGIN_BOTTOM
20
131
  },
21
132
  label: {
22
133
  marginBottom: 4,
134
+ fontSize: LABEL_FONT_SIZE,
23
135
  fontWeight: "600"
24
136
  },
25
137
  input: {
26
- padding: INPUT_PADDING,
138
+ paddingVertical: INPUT_PADDING_V,
139
+ paddingHorizontal: INPUT_PADDING_H,
27
140
  borderRadius: INPUT_BORDER_RADIUS,
28
141
  borderWidth: 1
29
142
  },
@@ -43,22 +156,14 @@ var FormField = ({
43
156
  const { colors, semantic } = theme;
44
157
  const errorColor = semantic.error["500"];
45
158
  const hasError = typeof error === "string" && error !== "";
46
- const themeStyles = React__default.default.useMemo(() => {
47
- const labelStyle = { color: colors.text };
48
- const requiredMarkStyle = { color: errorColor };
49
- const inputStyle = {
50
- backgroundColor: colors.surface,
51
- color: colors.text,
52
- borderColor: hasError ? errorColor : colors.border
53
- };
54
- const errorTextStyle = { color: errorColor };
55
- return {
56
- label: labelStyle,
57
- requiredMark: requiredMarkStyle,
58
- input: inputStyle,
59
- errorText: errorTextStyle
60
- };
61
- }, [colors.border, colors.surface, colors.text, errorColor, hasError]);
159
+ const themeStyles = React3__default.default.useMemo(
160
+ () => ({
161
+ label: { color: colors.textSecondary },
162
+ requiredMark: { color: errorColor },
163
+ errorText: { color: errorColor }
164
+ }),
165
+ [colors.textSecondary, errorColor]
166
+ );
62
167
  return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: [styles.container, containerStyle], children: [
63
168
  /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: [styles.label, themeStyles.label], children: [
64
169
  label,
@@ -66,12 +171,12 @@ var FormField = ({
66
171
  required ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: themeStyles.requiredMark, children: "*" }) : null
67
172
  ] }),
68
173
  /* @__PURE__ */ jsxRuntime.jsx(
69
- reactNative.TextInput,
174
+ ThemedTextInput,
70
175
  {
71
176
  accessibilityHint: `Enter ${label}`,
72
177
  accessibilityLabel: label,
73
- placeholderTextColor: colors.textSecondary,
74
- style: [styles.input, themeStyles.input],
178
+ hasError,
179
+ style: styles.input,
75
180
  testID: "form-field-input",
76
181
  ...textInputProps
77
182
  }
@@ -81,6 +186,28 @@ var FormField = ({
81
186
  };
82
187
  var TRANSPARENT_COLOR = "transparent";
83
188
  var WHITE_COLOR = "#fff";
189
+ var SOLID_RADIUS = 20;
190
+ var OUTLINE_RADIUS = 999;
191
+ var OUTLINE_PAD_H = 12;
192
+ var OUTLINE_PAD_V = 5;
193
+ var OUTLINE_FONT_SIZE = 13;
194
+ var OUTLINE_TINT_ALPHA = 0.1;
195
+ var HEX_SHORT_LENGTH2 = 3;
196
+ var HEX_RADIX2 = 16;
197
+ var RGB_R_START = 0;
198
+ var RGB_R_END = 2;
199
+ var RGB_G_END = 4;
200
+ var RGB_B_END = 6;
201
+ function withAlpha2(hex, alpha) {
202
+ const normalized = hex.replace("#", "");
203
+ const isShort = normalized.length === HEX_SHORT_LENGTH2;
204
+ const full = isShort ? normalized.split("").map((c) => c + c).join("") : normalized;
205
+ const r = parseInt(full.slice(RGB_R_START, RGB_R_END), HEX_RADIX2);
206
+ const g = parseInt(full.slice(RGB_R_END, RGB_G_END), HEX_RADIX2);
207
+ const b = parseInt(full.slice(RGB_G_END, RGB_B_END), HEX_RADIX2);
208
+ const isValid = !Number.isNaN(r) && !Number.isNaN(g) && !Number.isNaN(b);
209
+ return isValid ? `rgba(${r}, ${g}, ${b}, ${alpha})` : hex;
210
+ }
84
211
  var styles2 = reactNative.StyleSheet.create({
85
212
  label: {
86
213
  marginBottom: 8,
@@ -97,12 +224,72 @@ var styles2 = reactNative.StyleSheet.create({
97
224
  chip: {
98
225
  paddingHorizontal: 12,
99
226
  paddingVertical: 6,
100
- borderRadius: 20,
227
+ borderRadius: SOLID_RADIUS,
101
228
  backgroundColor: TRANSPARENT_COLOR,
102
229
  borderWidth: 1
103
230
  },
104
- chipText: {}
231
+ chipOutline: {
232
+ paddingHorizontal: OUTLINE_PAD_H,
233
+ paddingVertical: OUTLINE_PAD_V,
234
+ borderRadius: OUTLINE_RADIUS,
235
+ borderWidth: 1
236
+ },
237
+ chipText: {},
238
+ chipTextOutline: { fontSize: OUTLINE_FONT_SIZE }
105
239
  });
240
+ function OutlineChip({
241
+ option,
242
+ selected,
243
+ disabled,
244
+ colors,
245
+ onPress
246
+ }) {
247
+ const [hovered, setHovered] = React3__default.default.useState(false);
248
+ const active = selected || hovered;
249
+ const borderColor = active ? colors.primary : colors.border;
250
+ const backgroundColor = selected ? colors.tint : colors.muted;
251
+ const textColor = active ? colors.primary : colors.textMuted;
252
+ return /* @__PURE__ */ jsxRuntime.jsx(
253
+ reactNative.Pressable,
254
+ {
255
+ accessibilityHint: `Selects ${option.label}`,
256
+ accessibilityLabel: option.label,
257
+ accessibilityRole: "button",
258
+ accessibilityState: { selected },
259
+ disabled,
260
+ style: styles2.chipWrapper,
261
+ testID: `chip-selector-chip-${String(option.value)}`,
262
+ onHoverIn: () => setHovered(true),
263
+ onHoverOut: () => setHovered(false),
264
+ onPress,
265
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles2.chipOutline, { borderColor, backgroundColor }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.chipText, styles2.chipTextOutline, { color: textColor }], children: option.label }) })
266
+ }
267
+ );
268
+ }
269
+ function SolidChip({
270
+ option,
271
+ selected,
272
+ disabled,
273
+ colors,
274
+ onPress
275
+ }) {
276
+ const backgroundColor = selected ? colors.primary : TRANSPARENT_COLOR;
277
+ const textColor = selected ? WHITE_COLOR : colors.textStrong;
278
+ return /* @__PURE__ */ jsxRuntime.jsx(
279
+ reactNative.TouchableOpacity,
280
+ {
281
+ accessibilityHint: `Selects ${option.label}`,
282
+ accessibilityLabel: option.label,
283
+ accessibilityRole: "button",
284
+ accessibilityState: { selected },
285
+ disabled,
286
+ style: styles2.chipWrapper,
287
+ testID: `chip-selector-chip-${String(option.value)}`,
288
+ onPress,
289
+ children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles2.chip, { borderColor: colors.border, backgroundColor }], children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.chipText, { color: textColor }], children: option.label }) })
290
+ }
291
+ );
292
+ }
106
293
  var ChipSelector = ({
107
294
  label,
108
295
  options,
@@ -110,57 +297,52 @@ var ChipSelector = ({
110
297
  onChange,
111
298
  multiple = false,
112
299
  disabled = false,
300
+ variant = "solid",
113
301
  containerStyle
114
302
  }) => {
115
303
  const { theme } = uiFeedback.useUi();
116
304
  const { colors, palette } = theme;
117
- const primaryColor = palette.primary["500"];
118
305
  const hasLabel = typeof label === "string" && label !== "";
119
- const themeStyles = React__default.default.useMemo(() => {
120
- const labelStyle = { color: colors.text };
121
- const chipStyle = { borderColor: colors.border };
122
- const chipSelectedStyle = { backgroundColor: primaryColor };
123
- const chipTextStyle = { color: colors.text };
124
- const chipTextSelectedStyle = { color: WHITE_COLOR };
306
+ const chipColors = React3__default.default.useMemo(() => {
307
+ const primary = palette.primary["500"];
125
308
  return {
126
- label: labelStyle,
127
- chip: chipStyle,
128
- chipSelected: chipSelectedStyle,
129
- chipText: chipTextStyle,
130
- chipTextSelected: chipTextSelectedStyle
309
+ border: colors.border,
310
+ textStrong: colors.text,
311
+ textMuted: colors.textSecondary,
312
+ primary,
313
+ muted: colors.background,
314
+ tint: withAlpha2(primary, OUTLINE_TINT_ALPHA)
131
315
  };
132
- }, [colors.border, colors.text, primaryColor]);
316
+ }, [colors.border, colors.text, colors.textSecondary, colors.background, palette.primary]);
317
+ const labelStyle = React3__default.default.useMemo(() => ({ color: colors.text }), [colors.text]);
133
318
  function isSelected(optionValue) {
134
319
  if (multiple && Array.isArray(value))
135
320
  return value.includes(optionValue);
136
321
  return value === optionValue;
137
322
  }
138
323
  return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: containerStyle, children: [
139
- hasLabel ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.label, themeStyles.label], children: label }) : null,
324
+ hasLabel ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.label, labelStyle], children: label }) : null,
140
325
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.chipContainer, children: options.map((option) => {
141
326
  const selected = isSelected(option.value);
142
- return /* @__PURE__ */ jsxRuntime.jsx(
143
- reactNative.TouchableOpacity,
327
+ const onPress = () => onChange(option.value);
328
+ return variant === "outline" ? /* @__PURE__ */ jsxRuntime.jsx(
329
+ OutlineChip,
330
+ {
331
+ colors: chipColors,
332
+ disabled,
333
+ option,
334
+ selected,
335
+ onPress
336
+ },
337
+ String(option.value)
338
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
339
+ SolidChip,
144
340
  {
145
- accessibilityHint: `Selects ${option.label}`,
146
- accessibilityLabel: option.label,
147
- accessibilityRole: "button",
148
- accessibilityState: { selected },
341
+ colors: chipColors,
149
342
  disabled,
150
- style: styles2.chipWrapper,
151
- testID: `chip-selector-chip-${String(option.value)}`,
152
- onPress: () => onChange(option.value),
153
- children: /* @__PURE__ */ jsxRuntime.jsx(
154
- reactNative.View,
155
- {
156
- style: [
157
- styles2.chip,
158
- themeStyles.chip,
159
- selected ? themeStyles.chipSelected : null
160
- ],
161
- children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.chipText, themeStyles.chipText, selected ? themeStyles.chipTextSelected : null], children: option.label })
162
- }
163
- )
343
+ option,
344
+ selected,
345
+ onPress
164
346
  },
165
347
  String(option.value)
166
348
  );
@@ -199,7 +381,7 @@ var FormSwitch = ({
199
381
  const { theme } = uiFeedback.useUi();
200
382
  const { colors } = theme;
201
383
  const hasDescription = typeof description === "string" && description !== "";
202
- const themeStyles = React__default.default.useMemo(() => {
384
+ const themeStyles = React3__default.default.useMemo(() => {
203
385
  const labelStyle = { color: colors.text };
204
386
  const descriptionStyle = { color: colors.textSecondary };
205
387
  return { label: labelStyle, description: descriptionStyle };
@@ -289,7 +471,7 @@ var FormCheckbox = ({
289
471
  const brand = palette.primary["500"];
290
472
  const hintText = typeof hint === "string" && hint !== "" ? hint : description;
291
473
  const hasHint = typeof hintText === "string" && hintText !== "";
292
- const themeStyles = React__default.default.useMemo(() => {
474
+ const themeStyles = React3__default.default.useMemo(() => {
293
475
  const boxOn = { backgroundColor: brand, borderColor: brand };
294
476
  const boxOff = { backgroundColor: "transparent", borderColor: colors.border };
295
477
  const labelStyle = { color: colors.text };
@@ -329,5 +511,7 @@ exports.ChipSelector = ChipSelector;
329
511
  exports.FormCheckbox = FormCheckbox;
330
512
  exports.FormField = FormField;
331
513
  exports.FormSwitch = FormSwitch;
514
+ exports.ThemedTextInput = ThemedTextInput;
515
+ exports.useThemedInput = useThemedInput;
332
516
  //# sourceMappingURL=index.js.map
333
517
  //# sourceMappingURL=index.js.map