@dloizides/ui-forms 1.2.0 → 1.4.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,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.4.0
4
+
5
+ - **Accessibility (WCAG 2.1 AA) hardening — additive + backward-compatible.**
6
+ - `FormField`: the error message is now programmatically tied to the input — the input gets
7
+ `aria-invalid` when errored and `aria-describedby` pointing at the error line (which is a
8
+ `role="alert"` live region), and `aria-required` when `required`. The required `*` is marked
9
+ decorative (`aria-hidden` / `accessibilityElementsHidden`) so a screen reader hears "required"
10
+ (via `aria-required`) instead of "star".
11
+ - `ThemedTextInput`: new optional `describedById` + `requiredField` props forward
12
+ `aria-describedby` / `aria-required`, and it emits `aria-invalid` from `hasError` (web only;
13
+ no-ops on native). Existing callers are unaffected.
14
+ - `FormCheckbox`: now keyboard-operable — the `role="checkbox"` Pressable renders a `<div>`
15
+ (no free Enter/Space), so an explicit web-only Enter/Space handler toggles it (WCAG 2.1.1).
16
+ - `ChipSelector`: chips gain a `hitSlop` so the touch target reaches ~44px WITHOUT changing the
17
+ rendered pill size, and expose `accessibilityState.disabled` when the selector is disabled.
18
+ - `useThemedInput`: the focus/hover CSS transition now collapses to instant when the user
19
+ requests `prefers-reduced-motion: reduce` (WCAG 2.3.3, web only).
20
+
21
+ ## 1.3.0
22
+
23
+ - `ChipSelector` gains a `variant` prop (`'solid' | 'outline'`, default `'solid'`). `solid` is the
24
+ original filled-pill look (selected = solid brand fill, white ink) — existing consumers
25
+ (erevna / katalogos / kefi) render **identically**, no change. `outline` is the AML v1 console
26
+ look: an outlined pill on a muted fill where the **selected** state is a subtle tinted-outline
27
+ (brand border + brand ink + a low-alpha brand wash), NOT a solid fill; on web, hover lifts the
28
+ border + ink to the brand colour. Fully theme-driven (brand colour + alpha, border/text from the
29
+ theme), so it re-themes per tenant. Additive + backward-compatible.
30
+
3
31
  ## 1.2.0
4
32
 
5
33
  - Add `ThemedTextInput` + `useThemedInput` — the shared "readable + lively" text-input treatment,
package/dist/index.d.mts CHANGED
@@ -33,6 +33,13 @@ declare const FormField: ({ label, required, error, containerStyle, ...textInput
33
33
  interface ThemedTextInputProps extends TextInputProps {
34
34
  /** Render with the error border colour (matches `FormField`'s error treatment). */
35
35
  hasError?: boolean;
36
+ /**
37
+ * id of an element describing this input (e.g. an error/hint line). Linked via
38
+ * `aria-describedby` on web so a screen reader reads it after the field's name. No-op native.
39
+ */
40
+ describedById?: string;
41
+ /** Marks the field required for assistive tech (`aria-required` on web). No-op on native. */
42
+ requiredField?: boolean;
36
43
  }
37
44
  declare const ThemedTextInput: React.ForwardRefExoticComponent<ThemedTextInputProps & React.RefAttributes<TextInput>>;
38
45
 
@@ -66,8 +73,17 @@ declare function useThemedInput(options?: UseThemedInputOptions): ThemedInput;
66
73
 
67
74
  /**
68
75
  * Chip/pill selector for selecting one or multiple options.
76
+ *
77
+ * Two visual variants (both fully theme-driven):
78
+ * - `solid` (default) — the original filled-pill look: selected = solid primary fill with
79
+ * white ink. Unchanged, so existing consumers (erevna / katalogos / kefi) render identically.
80
+ * - `outline` — the AML v1 console look: an outlined pill on a muted fill; selected = a subtle
81
+ * tinted-outline (primary border + primary ink + a low-alpha primary wash), NOT a solid fill.
82
+ * Hover (web) lifts the border/ink to the primary colour.
69
83
  */
70
84
 
85
+ /** Visual variant. `solid` = filled selected pill (default); `outline` = v1 tinted-outline. */
86
+ type ChipVariant = 'solid' | 'outline';
71
87
  interface ChipOption<T> {
72
88
  value: T;
73
89
  label: string;
@@ -79,9 +95,11 @@ interface ChipSelectorProps<T> {
79
95
  onChange: (value: T) => void;
80
96
  multiple?: boolean;
81
97
  disabled?: boolean;
98
+ /** Visual variant. `solid` (default) = filled selected pill; `outline` = v1 tinted-outline. */
99
+ variant?: ChipVariant;
82
100
  containerStyle?: ViewStyle;
83
101
  }
84
- declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, containerStyle, }: ChipSelectorProps<T>) => React.ReactElement;
102
+ declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, variant, containerStyle, }: ChipSelectorProps<T>) => React.ReactElement;
85
103
 
86
104
  /**
87
105
  * Reusable form switch/toggle with an optional description.
@@ -124,4 +142,4 @@ interface FormCheckboxProps {
124
142
  }
125
143
  declare const FormCheckbox: ({ label, value, onValueChange, onChange, hint, description, disabled, containerStyle, testID, accessibilityLabel, accessibilityHint, }: FormCheckboxProps) => React.ReactElement;
126
144
 
127
- export { type ChipOption, ChipSelector, type ChipSelectorProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormSwitch, type FormSwitchProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, type UseThemedInputOptions, useThemedInput };
145
+ 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
@@ -33,6 +33,13 @@ declare const FormField: ({ label, required, error, containerStyle, ...textInput
33
33
  interface ThemedTextInputProps extends TextInputProps {
34
34
  /** Render with the error border colour (matches `FormField`'s error treatment). */
35
35
  hasError?: boolean;
36
+ /**
37
+ * id of an element describing this input (e.g. an error/hint line). Linked via
38
+ * `aria-describedby` on web so a screen reader reads it after the field's name. No-op native.
39
+ */
40
+ describedById?: string;
41
+ /** Marks the field required for assistive tech (`aria-required` on web). No-op on native. */
42
+ requiredField?: boolean;
36
43
  }
37
44
  declare const ThemedTextInput: React.ForwardRefExoticComponent<ThemedTextInputProps & React.RefAttributes<TextInput>>;
38
45
 
@@ -66,8 +73,17 @@ declare function useThemedInput(options?: UseThemedInputOptions): ThemedInput;
66
73
 
67
74
  /**
68
75
  * Chip/pill selector for selecting one or multiple options.
76
+ *
77
+ * Two visual variants (both fully theme-driven):
78
+ * - `solid` (default) — the original filled-pill look: selected = solid primary fill with
79
+ * white ink. Unchanged, so existing consumers (erevna / katalogos / kefi) render identically.
80
+ * - `outline` — the AML v1 console look: an outlined pill on a muted fill; selected = a subtle
81
+ * tinted-outline (primary border + primary ink + a low-alpha primary wash), NOT a solid fill.
82
+ * Hover (web) lifts the border/ink to the primary colour.
69
83
  */
70
84
 
85
+ /** Visual variant. `solid` = filled selected pill (default); `outline` = v1 tinted-outline. */
86
+ type ChipVariant = 'solid' | 'outline';
71
87
  interface ChipOption<T> {
72
88
  value: T;
73
89
  label: string;
@@ -79,9 +95,11 @@ interface ChipSelectorProps<T> {
79
95
  onChange: (value: T) => void;
80
96
  multiple?: boolean;
81
97
  disabled?: boolean;
98
+ /** Visual variant. `solid` (default) = filled selected pill; `outline` = v1 tinted-outline. */
99
+ variant?: ChipVariant;
82
100
  containerStyle?: ViewStyle;
83
101
  }
84
- declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, containerStyle, }: ChipSelectorProps<T>) => React.ReactElement;
102
+ declare const ChipSelector: <T extends string | number>({ label, options, value, onChange, multiple, disabled, variant, containerStyle, }: ChipSelectorProps<T>) => React.ReactElement;
85
103
 
86
104
  /**
87
105
  * Reusable form switch/toggle with an optional description.
@@ -124,4 +142,4 @@ interface FormCheckboxProps {
124
142
  }
125
143
  declare const FormCheckbox: ({ label, value, onValueChange, onChange, hint, description, disabled, containerStyle, testID, accessibilityLabel, accessibilityHint, }: FormCheckboxProps) => React.ReactElement;
126
144
 
127
- export { type ChipOption, ChipSelector, type ChipSelectorProps, FormCheckbox, type FormCheckboxProps, FormField, type FormFieldProps, FormSwitch, type FormSwitchProps, type ThemedInput, ThemedTextInput, type ThemedTextInputProps, type UseThemedInputOptions, useThemedInput };
145
+ 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,13 +1,13 @@
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
13
  var IS_WEB = reactNative.Platform.OS === "web";
@@ -24,6 +24,10 @@ var RED_START = 1;
24
24
  var GREEN_START = 3;
25
25
  var BLUE_START = 5;
26
26
  var CHANNEL_END_OFFSET = 2;
27
+ function prefersReducedMotion() {
28
+ if (!IS_WEB || typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
29
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
30
+ }
27
31
  function withAlpha(color, alpha) {
28
32
  const hex = color.trim();
29
33
  const isHex = hex.charAt(0) === "#" && (hex.length === HEX_FULL_LENGTH || hex.length === HEX_SHORT_LENGTH);
@@ -38,27 +42,27 @@ function useThemedInput(options = {}) {
38
42
  const { hasError = false, onFocus, onBlur } = options;
39
43
  const { theme } = uiFeedback.useUi();
40
44
  const { colors, palette, semantic } = theme;
41
- const [isFocused, setIsFocused] = React.useState(false);
42
- const [isHovered, setIsHovered] = React.useState(false);
43
- const handleFocus = React.useCallback(
45
+ const [isFocused, setIsFocused] = React3.useState(false);
46
+ const [isHovered, setIsHovered] = React3.useState(false);
47
+ const handleFocus = React3.useCallback(
44
48
  (e) => {
45
49
  setIsFocused(true);
46
50
  onFocus?.(e);
47
51
  },
48
52
  [onFocus]
49
53
  );
50
- const handleBlur = React.useCallback(
54
+ const handleBlur = React3.useCallback(
51
55
  (e) => {
52
56
  setIsFocused(false);
53
57
  onBlur?.(e);
54
58
  },
55
59
  [onBlur]
56
60
  );
57
- const handleHoverIn = React.useCallback(() => setIsHovered(true), []);
58
- const handleHoverOut = React.useCallback(() => setIsHovered(false), []);
61
+ const handleHoverIn = React3.useCallback(() => setIsHovered(true), []);
62
+ const handleHoverOut = React3.useCallback(() => setIsHovered(false), []);
59
63
  const primary = palette.primary["500"];
60
64
  const errorColor = semantic.error["500"];
61
- const style = React.useMemo(() => {
65
+ const style = React3.useMemo(() => {
62
66
  const restBorder = hasError ? errorColor : colors.border;
63
67
  const focusBorder = hasError ? errorColor : primary;
64
68
  const borderColor = isFocused ? focusBorder : isHovered ? withAlpha(colors.text, HOVER_BORDER_ALPHA) : restBorder;
@@ -68,7 +72,8 @@ function useThemedInput(options = {}) {
68
72
  const ring = isFocused && !hasError ? `0 0 0 ${FOCUS_RING_WIDTH}px ${withAlpha(primary, FOCUS_RING_ALPHA)}` : "none";
69
73
  const webStyle = {
70
74
  transitionProperty: TRANSITION_PROPERTY,
71
- transitionDuration: TRANSITION_DURATION,
75
+ // Collapse the eased transition to instant when the user prefers reduced motion.
76
+ transitionDuration: prefersReducedMotion() ? "0s" : TRANSITION_DURATION,
72
77
  transitionTimingFunction: TRANSITION_TIMING,
73
78
  boxShadow: ring,
74
79
  outlineStyle: "none"
@@ -85,8 +90,8 @@ function useThemedInput(options = {}) {
85
90
  primary,
86
91
  errorColor
87
92
  ]);
88
- const focusBind = React.useMemo(() => ({ onFocus: handleFocus, onBlur: handleBlur }), [handleFocus, handleBlur]);
89
- const hoverBind = React.useMemo(
93
+ const focusBind = React3.useMemo(() => ({ onFocus: handleFocus, onBlur: handleBlur }), [handleFocus, handleBlur]);
94
+ const hoverBind = React3.useMemo(
90
95
  () => IS_WEB ? { onMouseEnter: handleHoverIn, onMouseLeave: handleHoverOut } : {},
91
96
  [handleHoverIn, handleHoverOut]
92
97
  );
@@ -99,19 +104,26 @@ function useThemedInput(options = {}) {
99
104
  hoverBind
100
105
  };
101
106
  }
102
- var ThemedTextInput = React__default.default.forwardRef(function ThemedTextInput2({ hasError = false, onFocus, onBlur, style, placeholderTextColor, ...rest }, ref) {
107
+ var IS_WEB2 = reactNative.Platform.OS === "web";
108
+ var ThemedTextInput = React3__default.default.forwardRef(function ThemedTextInput2({ hasError = false, describedById, requiredField = false, onFocus, onBlur, style, placeholderTextColor, ...rest }, ref) {
103
109
  const {
104
110
  style: themedStyle,
105
111
  placeholderTextColor: themedPlaceholder,
106
112
  focusBind,
107
113
  hoverBind
108
114
  } = useThemedInput({ hasError, onFocus, onBlur });
115
+ const webA11y = IS_WEB2 ? {
116
+ "aria-invalid": hasError ? true : void 0,
117
+ "aria-describedby": describedById,
118
+ "aria-required": requiredField ? true : void 0
119
+ } : {};
109
120
  return /* @__PURE__ */ jsxRuntime.jsx(
110
121
  reactNative.TextInput,
111
122
  {
112
123
  ref,
113
124
  placeholderTextColor: placeholderTextColor ?? themedPlaceholder,
114
125
  ...rest,
126
+ ...webA11y,
115
127
  ...hoverBind,
116
128
  onFocus: focusBind.onFocus,
117
129
  onBlur: focusBind.onBlur,
@@ -145,6 +157,14 @@ var styles = reactNative.StyleSheet.create({
145
157
  marginTop: 4
146
158
  }
147
159
  });
160
+ var formFieldSeq = 0;
161
+ function useErrorId() {
162
+ const [id] = React3__default.default.useState(() => {
163
+ formFieldSeq += 1;
164
+ return `form-field-error-${String(formFieldSeq)}`;
165
+ });
166
+ return id;
167
+ }
148
168
  var FormField = ({
149
169
  label,
150
170
  required = false,
@@ -156,7 +176,8 @@ var FormField = ({
156
176
  const { colors, semantic } = theme;
157
177
  const errorColor = semantic.error["500"];
158
178
  const hasError = typeof error === "string" && error !== "";
159
- const themeStyles = React__default.default.useMemo(
179
+ const errorId = useErrorId();
180
+ const themeStyles = React3__default.default.useMemo(
160
181
  () => ({
161
182
  label: { color: colors.textSecondary },
162
183
  requiredMark: { color: errorColor },
@@ -168,24 +189,52 @@ var FormField = ({
168
189
  /* @__PURE__ */ jsxRuntime.jsxs(reactNative.Text, { style: [styles.label, themeStyles.label], children: [
169
190
  label,
170
191
  " ",
171
- required ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: themeStyles.requiredMark, children: "*" }) : null
192
+ required ? (
193
+ // The asterisk is decorative — `requiredField` conveys "required" to assistive tech.
194
+ /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { "aria-hidden": true, accessibilityElementsHidden: true, importantForAccessibility: "no", style: themeStyles.requiredMark, children: "*" })
195
+ ) : null
172
196
  ] }),
173
197
  /* @__PURE__ */ jsxRuntime.jsx(
174
198
  ThemedTextInput,
175
199
  {
176
200
  accessibilityHint: `Enter ${label}`,
177
201
  accessibilityLabel: label,
202
+ describedById: hasError ? errorId : void 0,
178
203
  hasError,
204
+ requiredField: required,
179
205
  style: styles.input,
180
206
  testID: "form-field-input",
181
207
  ...textInputProps
182
208
  }
183
209
  ),
184
- hasError ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles.errorText, themeStyles.errorText], children: error }) : null
210
+ hasError ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { nativeID: errorId, role: "alert", style: [styles.errorText, themeStyles.errorText], children: error }) : null
185
211
  ] });
186
212
  };
187
213
  var TRANSPARENT_COLOR = "transparent";
188
214
  var WHITE_COLOR = "#fff";
215
+ var SOLID_RADIUS = 20;
216
+ var OUTLINE_RADIUS = 999;
217
+ var OUTLINE_PAD_H = 12;
218
+ var OUTLINE_PAD_V = 5;
219
+ var OUTLINE_FONT_SIZE = 13;
220
+ var OUTLINE_TINT_ALPHA = 0.1;
221
+ var HEX_SHORT_LENGTH2 = 3;
222
+ var HEX_RADIX2 = 16;
223
+ var RGB_R_START = 0;
224
+ var RGB_R_END = 2;
225
+ var RGB_G_END = 4;
226
+ var RGB_B_END = 6;
227
+ var CHIP_HIT_SLOP = { top: 10, bottom: 10, left: 4, right: 4 };
228
+ function withAlpha2(hex, alpha) {
229
+ const normalized = hex.replace("#", "");
230
+ const isShort = normalized.length === HEX_SHORT_LENGTH2;
231
+ const full = isShort ? normalized.split("").map((c) => c + c).join("") : normalized;
232
+ const r = parseInt(full.slice(RGB_R_START, RGB_R_END), HEX_RADIX2);
233
+ const g = parseInt(full.slice(RGB_R_END, RGB_G_END), HEX_RADIX2);
234
+ const b = parseInt(full.slice(RGB_G_END, RGB_B_END), HEX_RADIX2);
235
+ const isValid = !Number.isNaN(r) && !Number.isNaN(g) && !Number.isNaN(b);
236
+ return isValid ? `rgba(${r}, ${g}, ${b}, ${alpha})` : hex;
237
+ }
189
238
  var styles2 = reactNative.StyleSheet.create({
190
239
  label: {
191
240
  marginBottom: 8,
@@ -202,12 +251,74 @@ var styles2 = reactNative.StyleSheet.create({
202
251
  chip: {
203
252
  paddingHorizontal: 12,
204
253
  paddingVertical: 6,
205
- borderRadius: 20,
254
+ borderRadius: SOLID_RADIUS,
206
255
  backgroundColor: TRANSPARENT_COLOR,
207
256
  borderWidth: 1
208
257
  },
209
- chipText: {}
258
+ chipOutline: {
259
+ paddingHorizontal: OUTLINE_PAD_H,
260
+ paddingVertical: OUTLINE_PAD_V,
261
+ borderRadius: OUTLINE_RADIUS,
262
+ borderWidth: 1
263
+ },
264
+ chipText: {},
265
+ chipTextOutline: { fontSize: OUTLINE_FONT_SIZE }
210
266
  });
267
+ function OutlineChip({
268
+ option,
269
+ selected,
270
+ disabled,
271
+ colors,
272
+ onPress
273
+ }) {
274
+ const [hovered, setHovered] = React3__default.default.useState(false);
275
+ const active = selected || hovered;
276
+ const borderColor = active ? colors.primary : colors.border;
277
+ const backgroundColor = selected ? colors.tint : colors.muted;
278
+ const textColor = active ? colors.primary : colors.textMuted;
279
+ return /* @__PURE__ */ jsxRuntime.jsx(
280
+ reactNative.Pressable,
281
+ {
282
+ accessibilityHint: `Selects ${option.label}`,
283
+ accessibilityLabel: option.label,
284
+ accessibilityRole: "button",
285
+ accessibilityState: { selected, disabled },
286
+ disabled,
287
+ hitSlop: CHIP_HIT_SLOP,
288
+ style: styles2.chipWrapper,
289
+ testID: `chip-selector-chip-${String(option.value)}`,
290
+ onHoverIn: () => setHovered(true),
291
+ onHoverOut: () => setHovered(false),
292
+ onPress,
293
+ 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 }) })
294
+ }
295
+ );
296
+ }
297
+ function SolidChip({
298
+ option,
299
+ selected,
300
+ disabled,
301
+ colors,
302
+ onPress
303
+ }) {
304
+ const backgroundColor = selected ? colors.primary : TRANSPARENT_COLOR;
305
+ const textColor = selected ? WHITE_COLOR : colors.textStrong;
306
+ return /* @__PURE__ */ jsxRuntime.jsx(
307
+ reactNative.TouchableOpacity,
308
+ {
309
+ accessibilityHint: `Selects ${option.label}`,
310
+ accessibilityLabel: option.label,
311
+ accessibilityRole: "button",
312
+ accessibilityState: { selected, disabled },
313
+ disabled,
314
+ hitSlop: CHIP_HIT_SLOP,
315
+ style: styles2.chipWrapper,
316
+ testID: `chip-selector-chip-${String(option.value)}`,
317
+ onPress,
318
+ 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 }) })
319
+ }
320
+ );
321
+ }
211
322
  var ChipSelector = ({
212
323
  label,
213
324
  options,
@@ -215,57 +326,52 @@ var ChipSelector = ({
215
326
  onChange,
216
327
  multiple = false,
217
328
  disabled = false,
329
+ variant = "solid",
218
330
  containerStyle
219
331
  }) => {
220
332
  const { theme } = uiFeedback.useUi();
221
333
  const { colors, palette } = theme;
222
- const primaryColor = palette.primary["500"];
223
334
  const hasLabel = typeof label === "string" && label !== "";
224
- const themeStyles = React__default.default.useMemo(() => {
225
- const labelStyle = { color: colors.text };
226
- const chipStyle = { borderColor: colors.border };
227
- const chipSelectedStyle = { backgroundColor: primaryColor };
228
- const chipTextStyle = { color: colors.text };
229
- const chipTextSelectedStyle = { color: WHITE_COLOR };
335
+ const chipColors = React3__default.default.useMemo(() => {
336
+ const primary = palette.primary["500"];
230
337
  return {
231
- label: labelStyle,
232
- chip: chipStyle,
233
- chipSelected: chipSelectedStyle,
234
- chipText: chipTextStyle,
235
- chipTextSelected: chipTextSelectedStyle
338
+ border: colors.border,
339
+ textStrong: colors.text,
340
+ textMuted: colors.textSecondary,
341
+ primary,
342
+ muted: colors.background,
343
+ tint: withAlpha2(primary, OUTLINE_TINT_ALPHA)
236
344
  };
237
- }, [colors.border, colors.text, primaryColor]);
345
+ }, [colors.border, colors.text, colors.textSecondary, colors.background, palette.primary]);
346
+ const labelStyle = React3__default.default.useMemo(() => ({ color: colors.text }), [colors.text]);
238
347
  function isSelected(optionValue) {
239
348
  if (multiple && Array.isArray(value))
240
349
  return value.includes(optionValue);
241
350
  return value === optionValue;
242
351
  }
243
352
  return /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: containerStyle, children: [
244
- hasLabel ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.label, themeStyles.label], children: label }) : null,
353
+ hasLabel ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.label, labelStyle], children: label }) : null,
245
354
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: styles2.chipContainer, children: options.map((option) => {
246
355
  const selected = isSelected(option.value);
247
- return /* @__PURE__ */ jsxRuntime.jsx(
248
- reactNative.TouchableOpacity,
356
+ const onPress = () => onChange(option.value);
357
+ return variant === "outline" ? /* @__PURE__ */ jsxRuntime.jsx(
358
+ OutlineChip,
359
+ {
360
+ colors: chipColors,
361
+ disabled,
362
+ option,
363
+ selected,
364
+ onPress
365
+ },
366
+ String(option.value)
367
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
368
+ SolidChip,
249
369
  {
250
- accessibilityHint: `Selects ${option.label}`,
251
- accessibilityLabel: option.label,
252
- accessibilityRole: "button",
253
- accessibilityState: { selected },
370
+ colors: chipColors,
254
371
  disabled,
255
- style: styles2.chipWrapper,
256
- testID: `chip-selector-chip-${String(option.value)}`,
257
- onPress: () => onChange(option.value),
258
- children: /* @__PURE__ */ jsxRuntime.jsx(
259
- reactNative.View,
260
- {
261
- style: [
262
- styles2.chip,
263
- themeStyles.chip,
264
- selected ? themeStyles.chipSelected : null
265
- ],
266
- children: /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles2.chipText, themeStyles.chipText, selected ? themeStyles.chipTextSelected : null], children: option.label })
267
- }
268
- )
372
+ option,
373
+ selected,
374
+ onPress
269
375
  },
270
376
  String(option.value)
271
377
  );
@@ -304,7 +410,7 @@ var FormSwitch = ({
304
410
  const { theme } = uiFeedback.useUi();
305
411
  const { colors } = theme;
306
412
  const hasDescription = typeof description === "string" && description !== "";
307
- const themeStyles = React__default.default.useMemo(() => {
413
+ const themeStyles = React3__default.default.useMemo(() => {
308
414
  const labelStyle = { color: colors.text };
309
415
  const descriptionStyle = { color: colors.textSecondary };
310
416
  return { label: labelStyle, description: descriptionStyle };
@@ -338,6 +444,7 @@ var FormSwitch = ({
338
444
  }
339
445
  );
340
446
  };
447
+ var ACTIVATION_KEYS = ["Enter", " ", "Spacebar"];
341
448
  var HINT_FONT_SIZE = 12;
342
449
  var BOX_SIZE = 20;
343
450
  var BOX_RADIUS = 5;
@@ -394,7 +501,7 @@ var FormCheckbox = ({
394
501
  const brand = palette.primary["500"];
395
502
  const hintText = typeof hint === "string" && hint !== "" ? hint : description;
396
503
  const hasHint = typeof hintText === "string" && hintText !== "";
397
- const themeStyles = React__default.default.useMemo(() => {
504
+ const themeStyles = React3__default.default.useMemo(() => {
398
505
  const boxOn = { backgroundColor: brand, borderColor: brand };
399
506
  const boxOff = { backgroundColor: "transparent", borderColor: colors.border };
400
507
  const labelStyle = { color: colors.text };
@@ -406,6 +513,14 @@ var FormCheckbox = ({
406
513
  if (onValueChange) onValueChange(next);
407
514
  if (onChange) onChange(next);
408
515
  };
516
+ const handleKeyDown = (event) => {
517
+ if (disabled) return;
518
+ const key = event.key ?? "";
519
+ if (!ACTIVATION_KEYS.includes(key)) return;
520
+ if (typeof event.preventDefault === "function") event.preventDefault();
521
+ toggle();
522
+ };
523
+ const keyboardProps = reactNative.Platform.OS === "web" ? { onKeyDown: handleKeyDown } : {};
409
524
  return /* @__PURE__ */ jsxRuntime.jsxs(
410
525
  reactNative.Pressable,
411
526
  {
@@ -419,6 +534,7 @@ var FormCheckbox = ({
419
534
  role: "checkbox",
420
535
  style: [styles4.container, containerStyle, disabled ? { opacity: DISABLED_OPACITY } : null],
421
536
  testID,
537
+ ...keyboardProps,
422
538
  children: [
423
539
  /* @__PURE__ */ jsxRuntime.jsx(reactNative.View, { style: [styles4.box, value ? themeStyles.boxOn : themeStyles.boxOff], children: value ? /* @__PURE__ */ jsxRuntime.jsx(reactNative.Text, { style: [styles4.check, { color: colors.surface }], accessibilityElementsHidden: true, children: "\u2713" }) : null }),
424
540
  /* @__PURE__ */ jsxRuntime.jsxs(reactNative.View, { style: styles4.textColumn, children: [