@colixsystems/widget-sdk 0.210.0 → 0.212.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.
@@ -0,0 +1,98 @@
1
+ // What every form control shares on screen: the label above it and the helper
2
+ // or error below it. One frame, so `<TextField>`, `<Checkbox>`, `<Switch>`,
3
+ // `<Select>` and `<SearchField>` cannot drift apart. Its rules, which need no
4
+ // renderer, live in ./form-control-logic.js.
5
+
6
+ import React from "react";
7
+ import { errorMessage, isShown, isText } from "./form-control-logic.js";
8
+
9
+ const h = React.createElement;
10
+
11
+ /**
12
+ * Bind the shared frame to one platform's primitives.
13
+ *
14
+ * @param {{View: any, Text: any, Pressable: any}} rn
15
+ */
16
+ export function makeFieldFrame(rn) {
17
+ const { View, Text, Pressable } = rn;
18
+
19
+ /**
20
+ * @param {object} props
21
+ * @param {object} props.t — resolved form-control tokens.
22
+ * @param {any} [props.label] — text, or an element rendered as given.
23
+ * @param {any} [props.helper]
24
+ * @param {any} [props.error] — a message replaces the helper; `true` shows none.
25
+ * @param {string} [props.noteId] — the id the control's `aria-describedby` names.
26
+ * @param {() => void} [props.onLabelPress] — a text label then acts for the control.
27
+ * @param {boolean} [props.noteDisabled] — dim the note alone, when the
28
+ * control dims itself and `disabled` would dim it twice.
29
+ */
30
+ function FieldFrame({
31
+ t,
32
+ label,
33
+ helper,
34
+ error,
35
+ disabled,
36
+ noteId,
37
+ onLabelPress,
38
+ noteDisabled,
39
+ style,
40
+ children,
41
+ }) {
42
+ const message = errorMessage(error);
43
+ const note = message || (isShown(helper) ? helper : null);
44
+ const labelText = isText(label)
45
+ ? h(
46
+ Text,
47
+ {
48
+ style: {
49
+ color: t.labelColor,
50
+ fontFamily: t.fontFamily,
51
+ fontSize: t.labelSize,
52
+ fontWeight: "500",
53
+ },
54
+ },
55
+ label,
56
+ )
57
+ : null;
58
+ return h(
59
+ View,
60
+ { style: [{ gap: t.gap }, disabled ? { opacity: t.disabledOpacity } : null, style] },
61
+ labelText && typeof onLabelPress === "function"
62
+ ? h(
63
+ Pressable,
64
+ {
65
+ onPress: onLabelPress,
66
+ disabled,
67
+ // The control is the one stop for a keyboard and a reader.
68
+ tabIndex: -1,
69
+ accessible: false,
70
+ style: { alignSelf: "flex-start" },
71
+ },
72
+ labelText,
73
+ )
74
+ : labelText || (isShown(label) ? label : null),
75
+ children,
76
+ isText(note)
77
+ ? h(
78
+ Text,
79
+ {
80
+ nativeID: noteId,
81
+ // Announced where the host can: the web and Android.
82
+ ...(message ? { accessibilityLiveRegion: "polite" } : null),
83
+ style: {
84
+ color: message ? t.errorColor : t.helperColor,
85
+ fontFamily: t.fontFamily,
86
+ fontSize: t.helperSize,
87
+ ...(noteDisabled ? { opacity: t.disabledOpacity } : null),
88
+ },
89
+ },
90
+ note,
91
+ )
92
+ : note,
93
+ );
94
+ }
95
+
96
+ FieldFrame.displayName = "FieldFrame";
97
+ return FieldFrame;
98
+ }
package/dist/index.d.ts CHANGED
@@ -3167,7 +3167,6 @@ export const TextInput: any;
3167
3167
  export const FlatList: any;
3168
3168
  export const SectionList: any;
3169
3169
  export const ActivityIndicator: any;
3170
- export const Switch: any;
3171
3170
  export const StyleSheet: any;
3172
3171
 
3173
3172
  /**
@@ -3620,19 +3619,150 @@ export const Button: (props: {
3620
3619
  }) => any;
3621
3620
 
3622
3621
  /**
3623
- * Themed text-entry field (Theme Settings > Muted). Every TextInput prop works.
3624
- *
3625
- * @example
3626
- * <TextField value={name} onChangeText={setName} placeholder="Your name" accessibilityLabel="Name" />
3622
+ * What every form control shares: the label drawn with it (and read by a
3623
+ * screen reader), a hint below it, and an error that replaces the hint and
3624
+ * marks the control. `error: true` marks it without a message.
3627
3625
  */
3628
- export const TextField: (props: {
3626
+ export interface FormControlProps {
3627
+ label?: ReactNode;
3628
+ helper?: ReactNode;
3629
+ error?: ReactNode | boolean;
3630
+ disabled?: boolean;
3631
+ /** Required when there is no text `label`. */
3632
+ accessibilityLabel?: string;
3633
+ testID?: string;
3634
+ }
3635
+
3636
+ /** Per-control overrides of the theme's `muted` scope. */
3637
+ export interface FormFieldOverrides {
3629
3638
  background?: string;
3630
3639
  textColor?: string;
3631
3640
  borderColor?: string;
3632
3641
  radius?: number;
3633
3642
  height?: number;
3643
+ }
3644
+
3645
+ /**
3646
+ * Themed text-entry field (Design > Components > Muted). Every TextInput prop
3647
+ * works.
3648
+ *
3649
+ * @example
3650
+ * <TextField label="Name" value={name} onChangeText={setName} placeholder="Your name" />
3651
+ */
3652
+ export const TextField: (props: FormControlProps & FormFieldOverrides & {
3634
3653
  multiline?: boolean;
3635
3654
  style?: any;
3655
+ /** The box around a labelled field. */
3656
+ containerStyle?: any;
3657
+ [prop: string]: any;
3658
+ }) => any;
3659
+
3660
+ /**
3661
+ * Themed tick box: the input surface, filled with the primary colour when
3662
+ * checked. One 44px target with the checkbox role and state on both hosts.
3663
+ *
3664
+ * @example
3665
+ * <Checkbox label="Remember me" value={remember} onValueChange={setRemember} />
3666
+ */
3667
+ export const Checkbox: (props: FormControlProps & {
3668
+ value?: boolean;
3669
+ onValueChange?: (next: boolean) => void;
3670
+ /** The row. */
3671
+ style?: any;
3672
+ /** The box around a row that shows a helper or an error. */
3673
+ containerStyle?: any;
3674
+ [prop: string]: any;
3675
+ }) => any;
3676
+
3677
+ /**
3678
+ * Themed on/off toggle: the platform's own switch with the theme's track
3679
+ * colours. Every React Native `Switch` prop works: a `trackColor` overrides
3680
+ * the theme for the states it names, and the thumb is the platform's unless
3681
+ * `thumbColor` is passed. Without a label, helper or error it renders the
3682
+ * bare switch.
3683
+ *
3684
+ * @example
3685
+ * <Switch label="Email me updates" value={on} onValueChange={setOn} />
3686
+ */
3687
+ export const Switch: (props: FormControlProps & {
3688
+ value?: boolean;
3689
+ onValueChange?: (next: boolean) => void;
3690
+ trackColor?: { false?: string; true?: string };
3691
+ thumbColor?: string;
3692
+ style?: any;
3693
+ /** The box around a labelled switch. */
3694
+ containerStyle?: any;
3695
+ [prop: string]: any;
3696
+ }) => any;
3697
+
3698
+ export interface SelectOption<V extends string | number = string | number> {
3699
+ value: V;
3700
+ label: string;
3701
+ disabled?: boolean;
3702
+ testID?: string;
3703
+ }
3704
+
3705
+ /**
3706
+ * Themed single-choice picker. The trigger is the input field, a button that
3707
+ * opens a dialog; its options open at screen level (through `Overlay`) in a
3708
+ * sheet painted from the `popover` scope, so no container clips them and a
3709
+ * long list scrolls. Each option has the option role; the list is a listbox
3710
+ * on the web only. Other props land on the trigger.
3711
+ *
3712
+ * @example
3713
+ * <Select label="Status" value={status} onValueChange={setStatus}
3714
+ * options={[{ value: "open", label: "Open" }, { value: "done", label: "Done" }]} />
3715
+ */
3716
+ export const Select: <V extends string | number = string | number>(
3717
+ props: FormControlProps & FormFieldOverrides & {
3718
+ options: Array<SelectOption<V>>;
3719
+ value?: V | null;
3720
+ onValueChange?: (value: V, option: SelectOption<V>) => void;
3721
+ /** Shown while nothing is chosen. Default "Select". */
3722
+ placeholder?: string;
3723
+ /** The sheet's heading. Defaults to a text `label`. */
3724
+ title?: string;
3725
+ /** Shown in the sheet when `options` is empty. Default "No options". */
3726
+ emptyText?: string;
3727
+ closeAccessibilityLabel?: string;
3728
+ /** The trigger. */
3729
+ style?: any;
3730
+ containerStyle?: any;
3731
+ [prop: string]: any;
3732
+ },
3733
+ ) => any;
3734
+
3735
+ /**
3736
+ * Space-key activation for a control whose role is not a button: on the web a
3737
+ * `Pressable` answers Space only for a button. Native ignores `onKeyDown`.
3738
+ *
3739
+ * @example
3740
+ * <Pressable accessibilityRole="radio" onPress={pick} onKeyDown={spaceActivates(pick)} />
3741
+ */
3742
+ export function spaceActivates(
3743
+ onPress: () => void,
3744
+ ): (event: { key: string; repeat?: boolean; preventDefault: () => void }) => void;
3745
+
3746
+ /**
3747
+ * Themed search box: a `TextField` with a search glyph, the search key and a
3748
+ * clear control. Every `TextField` prop works.
3749
+ *
3750
+ * @example
3751
+ * <SearchField value={query} onChangeText={setQuery} placeholder="Search orders" />
3752
+ */
3753
+ export const SearchField: (props: FormControlProps & {
3754
+ value?: string;
3755
+ onChangeText?: (text: string) => void;
3756
+ /** Called after the clear control empties the field. */
3757
+ onClear?: () => void;
3758
+ /** Default "Search". */
3759
+ placeholder?: string;
3760
+ /** Default "Clear search". */
3761
+ clearAccessibilityLabel?: string;
3762
+ /** The input. */
3763
+ style?: any;
3764
+ /** The box around it; use it for `flex: 1` in a toolbar. */
3765
+ containerStyle?: any;
3636
3766
  [prop: string]: any;
3637
3767
  }) => any;
3638
3768
 
package/dist/index.js CHANGED
@@ -167,7 +167,6 @@ export {
167
167
  FlatList,
168
168
  SectionList,
169
169
  ActivityIndicator,
170
- Switch,
171
170
  StyleSheet,
172
171
  Linking,
173
172
  Icon,
@@ -266,6 +265,15 @@ export {
266
265
  export { TextField } from "./text-field.js";
267
266
  // Themed collapsible sections, drawn from the `card` and `list` scopes.
268
267
  export { Accordion, AccordionItem } from "./accordion.js";
268
+ // The themed form controls; they follow the theme's `muted` scope as
269
+ // `<TextField>` does, and `popover` for the Select's options. `<Switch>` is
270
+ // the platform's own.
271
+ export { Checkbox } from "./checkbox.js";
272
+ export { Switch } from "./switch.js";
273
+ export { Select } from "./select.js";
274
+ export { SearchField } from "./search-field.js";
275
+ // Space for a control whose role is not a button; a no-op on native.
276
+ export { spaceActivates } from "./form-control-logic.js";
269
277
  // A themed tab strip; follows the theme's `tabs` scope.
270
278
  export { Tabs } from "./tabs.js";
271
279
  // Themed loading states; follow the theme's `loading` scope.
@@ -160,7 +160,6 @@ export {
160
160
  FlatList,
161
161
  SectionList,
162
162
  ActivityIndicator,
163
- Switch,
164
163
  StyleSheet,
165
164
  Linking,
166
165
  Icon,
@@ -259,6 +258,15 @@ export {
259
258
  export { TextField } from "./text-field.native.js";
260
259
  // Themed collapsible sections, drawn from the `card` and `list` scopes.
261
260
  export { Accordion, AccordionItem } from "./accordion.native.js";
261
+ // The themed form controls; they follow the theme's `muted` scope as
262
+ // `<TextField>` does, and `popover` for the Select's options. `<Switch>` is
263
+ // the platform's own.
264
+ export { Checkbox } from "./checkbox.native.js";
265
+ export { Switch } from "./switch.native.js";
266
+ export { Select } from "./select.native.js";
267
+ export { SearchField } from "./search-field.native.js";
268
+ // Space for a control whose role is not a button; a no-op on native.
269
+ export { spaceActivates } from "./form-control-logic.js";
262
270
  // A themed tab strip; follows the theme's `tabs` scope.
263
271
  export { Tabs } from "./tabs.native.js";
264
272
  // Themed loading states; follow the theme's `loading` scope.
@@ -0,0 +1,155 @@
1
+ // The ONE implementation behind the `<SearchField>` SDK primitive. It is
2
+ // `<TextField>` with a search glyph and a clear control, so it wears the same
3
+ // field and takes the same props; the platform bindings (`search-field.js` /
4
+ // `search-field.native.js`) differ only in where the primitives come from.
5
+
6
+ import React from "react";
7
+ import { useHostTheme } from "./hooks.js";
8
+ import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
9
+ import { Icon } from "./icon.js";
10
+ import { resolveFormControlTokens } from "./form-control-tokens.js";
11
+ import {
12
+ errorMessage,
13
+ hasNote,
14
+ isShown,
15
+ shareRef,
16
+ spokenLabel,
17
+ } from "./form-control-logic.js";
18
+ import { makeFieldFrame } from "./form-control-view.js";
19
+
20
+ const h = React.createElement;
21
+ const GLYPH_SIZE = 18;
22
+ // The glyph and the clear control each own a strip this wide inside the field.
23
+ const ADORNMENT_WIDTH = 40;
24
+ // A real target: the web host ignores hitSlop, so size carries it.
25
+ const CLEAR_MIN_SIZE = 36;
26
+
27
+ /**
28
+ * Bind the shared search field to one platform's primitives.
29
+ *
30
+ * @param {{View: any, Text: any, Pressable: any, TextField: any}} rn
31
+ */
32
+ export function makeSearchField(rn) {
33
+ const { View, Pressable, TextField } = rn;
34
+ const FieldFrame = makeFieldFrame(rn);
35
+
36
+ const SearchField = React.forwardRef(function SearchField(props, ref) {
37
+ const {
38
+ value,
39
+ onChangeText,
40
+ onClear,
41
+ placeholder = "Search",
42
+ clearAccessibilityLabel = "Clear search",
43
+ label,
44
+ helper,
45
+ error,
46
+ disabled = false,
47
+ accessibilityLabel,
48
+ style,
49
+ containerStyle,
50
+ testID,
51
+ ...rest
52
+ } = props;
53
+ const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
54
+ const t = resolveFormControlTokens(theme);
55
+ const inputRef = React.useRef(null);
56
+ const setRef = React.useMemo(() => shareRef(ref, inputRef), [ref]);
57
+ const noteId = React.useId();
58
+ const focusInput = () => {
59
+ const input = inputRef.current;
60
+ if (input && typeof input.focus === "function") input.focus();
61
+ };
62
+ const hasText = typeof value === "string" && value.length > 0;
63
+ const clear = () => {
64
+ if (typeof onChangeText === "function") onChangeText("");
65
+ if (typeof onClear === "function") onClear();
66
+ focusInput();
67
+ };
68
+
69
+ return h(
70
+ FieldFrame,
71
+ {
72
+ t,
73
+ label,
74
+ helper,
75
+ error: errorMessage(error),
76
+ disabled,
77
+ noteId,
78
+ onLabelPress: focusInput,
79
+ style: containerStyle,
80
+ },
81
+ h(
82
+ View,
83
+ { style: { justifyContent: "center" } },
84
+ h(TextField, {
85
+ ref: setRef,
86
+ value,
87
+ onChangeText,
88
+ placeholder,
89
+ // The frame dims the whole control, glyph included.
90
+ ...(disabled ? { editable: false, "aria-disabled": true } : null),
91
+ error: isShown(error),
92
+ ...(hasNote({ helper, error }) ? { "aria-describedby": noteId } : null),
93
+ // `role` names it on the web; `accessibilityRole` on a device.
94
+ role: "searchbox",
95
+ accessibilityRole: "search",
96
+ accessibilityLabel: spokenLabel(accessibilityLabel, label) || placeholder,
97
+ enterKeyHint: "search",
98
+ autoCapitalize: "none",
99
+ autoCorrect: false,
100
+ testID,
101
+ ...rest,
102
+ style: [
103
+ { paddingLeft: ADORNMENT_WIDTH },
104
+ hasText && !disabled ? { paddingRight: ADORNMENT_WIDTH } : null,
105
+ style,
106
+ ],
107
+ }),
108
+ h(
109
+ View,
110
+ {
111
+ style: {
112
+ position: "absolute",
113
+ left: 0,
114
+ top: 0,
115
+ bottom: 0,
116
+ width: ADORNMENT_WIDTH,
117
+ alignItems: "center",
118
+ justifyContent: "center",
119
+ pointerEvents: "none",
120
+ },
121
+ },
122
+ h(Icon, { name: "search", size: GLYPH_SIZE, color: t.placeholderColor }),
123
+ ),
124
+ hasText && !disabled
125
+ ? h(
126
+ Pressable,
127
+ {
128
+ onPress: clear,
129
+ accessibilityRole: "button",
130
+ accessibilityLabel: clearAccessibilityLabel,
131
+ testID: testID ? `${testID}-clear` : undefined,
132
+ style: ({ pressed }) => [
133
+ {
134
+ position: "absolute",
135
+ right: 2,
136
+ top: 0,
137
+ bottom: 0,
138
+ minWidth: CLEAR_MIN_SIZE,
139
+ minHeight: CLEAR_MIN_SIZE,
140
+ alignItems: "center",
141
+ justifyContent: "center",
142
+ },
143
+ pressed ? { opacity: 0.6 } : null,
144
+ ],
145
+ },
146
+ h(Icon, { name: "x", size: GLYPH_SIZE, color: t.placeholderColor }),
147
+ )
148
+ : null,
149
+ ),
150
+ );
151
+ });
152
+
153
+ SearchField.displayName = "SearchField";
154
+ return SearchField;
155
+ }
@@ -0,0 +1,13 @@
1
+ // `<SearchField>` (web). Binds ./search-field-view.js to react-native-web and
2
+ // the web `<TextField>`; see ./primitives.js for why not the bare specifier.
3
+
4
+ import * as ReactNative from "react-native-web";
5
+ import { makeSearchField } from "./search-field-view.js";
6
+ import { TextField } from "./text-field.js";
7
+
8
+ export const SearchField = makeSearchField({
9
+ View: ReactNative.View,
10
+ Text: ReactNative.Text,
11
+ Pressable: ReactNative.Pressable,
12
+ TextField,
13
+ });
@@ -0,0 +1,9 @@
1
+ // `<SearchField>` (native). Binds ./search-field-view.js to React Native's own
2
+ // primitives and the native `<TextField>`; only the imports differ from
3
+ // ./search-field.js.
4
+
5
+ import { View, Text, Pressable } from "react-native";
6
+ import { makeSearchField } from "./search-field-view.js";
7
+ import { TextField } from "./text-field.native.js";
8
+
9
+ export const SearchField = makeSearchField({ View, Text, Pressable, TextField });