@colixsystems/widget-sdk 0.210.0 → 0.213.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/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
  /**
@@ -3361,6 +3360,70 @@ export const Modal: (props: {
3361
3360
  children?: ReactNode;
3362
3361
  }) => any;
3363
3362
 
3363
+ export type ConfirmDialogTone = "default" | "destructive";
3364
+
3365
+ /**
3366
+ * The standard confirmation prompt: a preset of `<Modal>` with a message, a
3367
+ * Cancel and a Confirm `<Button>`. Use it before a delete or any other action
3368
+ * that cannot be undone, so every confirmation in the app looks and behaves
3369
+ * the same. Cancelling is the safe default: the Cancel button, the close
3370
+ * button, the backdrop, Escape (web) and the Android back button all fire
3371
+ * `onCancel`, never `onConfirm`.
3372
+ *
3373
+ * It adds no theme scope: the panel follows `modal`, the actions `button`,
3374
+ * and a destructive confirm the `destructive` scope. It never closes itself;
3375
+ * set `visible` false from `onCancel` and once the action is done.
3376
+ *
3377
+ * @example
3378
+ * <ConfirmDialog
3379
+ * visible={!!pending}
3380
+ * tone="destructive"
3381
+ * title="Delete this order?"
3382
+ * message="This cannot be undone."
3383
+ * confirmLabel="Delete"
3384
+ * onCancel={() => setPending(null)}
3385
+ * onConfirm={async () => {
3386
+ * await remove(pending.id);
3387
+ * setPending(null);
3388
+ * }}
3389
+ * />
3390
+ */
3391
+ export const ConfirmDialog: (props: {
3392
+ /** Nothing renders (and nothing is mounted) while this is false. */
3393
+ visible?: boolean;
3394
+ /** The question, e.g. "Delete this order?". */
3395
+ title: string;
3396
+ /** What confirming does. Text is drawn in the theme's body style. */
3397
+ message?: ReactNode;
3398
+ /** Defaults to the app's translated "Confirm". Name the action instead. */
3399
+ confirmLabel?: string;
3400
+ /** Defaults to the app's translated "Cancel". */
3401
+ cancelLabel?: string;
3402
+ /**
3403
+ * `destructive` draws the confirm button from the theme's Destructive
3404
+ * colours and, on web, opens with focus on Cancel. `default` opens with
3405
+ * focus on Confirm. Focus placement is web only.
3406
+ */
3407
+ tone?: ConfirmDialogTone;
3408
+ /**
3409
+ * The confirmed action is running: both buttons are disabled, a loading
3410
+ * indicator shows, and the backdrop, Escape and Android back do nothing.
3411
+ */
3412
+ busy?: boolean;
3413
+ /**
3414
+ * Return the action's promise and the dialog stays busy until it settles,
3415
+ * ignoring a second press. A rejection is not caught: handle the failure
3416
+ * here, or it is reported as an unhandled rejection.
3417
+ */
3418
+ onConfirm?: () => void | Promise<unknown>;
3419
+ /** The Cancel and close buttons, backdrop, Escape (web) and Android back. */
3420
+ onCancel?: () => void;
3421
+ /** Label for the close button. Defaults to the app's translated "Close". */
3422
+ closeAccessibilityLabel?: string;
3423
+ /** On the panel; the parts take `-message`, `-cancel`, `-confirm` and `-busy`. */
3424
+ testID?: string;
3425
+ }) => any;
3426
+
3364
3427
  /** Per-list row overrides; unset values follow Theme Settings > Lists. */
3365
3428
  export interface ListRowTokens {
3366
3429
  rowPadding?: number;
@@ -3617,22 +3680,155 @@ export const Button: (props: {
3617
3680
  labelStyle?: any;
3618
3681
  accessibilityLabel?: string;
3619
3682
  testID?: string;
3683
+ /** Forwarded to the pressable, so you can focus the button. */
3684
+ ref?: { current: any } | ((node: any) => void);
3620
3685
  }) => any;
3621
3686
 
3622
3687
  /**
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" />
3688
+ * What every form control shares: the label drawn with it (and read by a
3689
+ * screen reader), a hint below it, and an error that replaces the hint and
3690
+ * marks the control. `error: true` marks it without a message.
3627
3691
  */
3628
- export const TextField: (props: {
3692
+ export interface FormControlProps {
3693
+ label?: ReactNode;
3694
+ helper?: ReactNode;
3695
+ error?: ReactNode | boolean;
3696
+ disabled?: boolean;
3697
+ /** Required when there is no text `label`. */
3698
+ accessibilityLabel?: string;
3699
+ testID?: string;
3700
+ }
3701
+
3702
+ /** Per-control overrides of the theme's `muted` scope. */
3703
+ export interface FormFieldOverrides {
3629
3704
  background?: string;
3630
3705
  textColor?: string;
3631
3706
  borderColor?: string;
3632
3707
  radius?: number;
3633
3708
  height?: number;
3709
+ }
3710
+
3711
+ /**
3712
+ * Themed text-entry field (Design > Components > Muted). Every TextInput prop
3713
+ * works.
3714
+ *
3715
+ * @example
3716
+ * <TextField label="Name" value={name} onChangeText={setName} placeholder="Your name" />
3717
+ */
3718
+ export const TextField: (props: FormControlProps & FormFieldOverrides & {
3634
3719
  multiline?: boolean;
3635
3720
  style?: any;
3721
+ /** The box around a labelled field. */
3722
+ containerStyle?: any;
3723
+ [prop: string]: any;
3724
+ }) => any;
3725
+
3726
+ /**
3727
+ * Themed tick box: the input surface, filled with the primary colour when
3728
+ * checked. One 44px target with the checkbox role and state on both hosts.
3729
+ *
3730
+ * @example
3731
+ * <Checkbox label="Remember me" value={remember} onValueChange={setRemember} />
3732
+ */
3733
+ export const Checkbox: (props: FormControlProps & {
3734
+ value?: boolean;
3735
+ onValueChange?: (next: boolean) => void;
3736
+ /** The row. */
3737
+ style?: any;
3738
+ /** The box around a row that shows a helper or an error. */
3739
+ containerStyle?: any;
3740
+ [prop: string]: any;
3741
+ }) => any;
3742
+
3743
+ /**
3744
+ * Themed on/off toggle: the platform's own switch with the theme's track
3745
+ * colours. Every React Native `Switch` prop works: a `trackColor` overrides
3746
+ * the theme for the states it names, and the thumb is the platform's unless
3747
+ * `thumbColor` is passed. Without a label, helper or error it renders the
3748
+ * bare switch.
3749
+ *
3750
+ * @example
3751
+ * <Switch label="Email me updates" value={on} onValueChange={setOn} />
3752
+ */
3753
+ export const Switch: (props: FormControlProps & {
3754
+ value?: boolean;
3755
+ onValueChange?: (next: boolean) => void;
3756
+ trackColor?: { false?: string; true?: string };
3757
+ thumbColor?: string;
3758
+ style?: any;
3759
+ /** The box around a labelled switch. */
3760
+ containerStyle?: any;
3761
+ [prop: string]: any;
3762
+ }) => any;
3763
+
3764
+ export interface SelectOption<V extends string | number = string | number> {
3765
+ value: V;
3766
+ label: string;
3767
+ disabled?: boolean;
3768
+ testID?: string;
3769
+ }
3770
+
3771
+ /**
3772
+ * Themed single-choice picker. The trigger is the input field, a button that
3773
+ * opens a dialog; its options open at screen level (through `Overlay`) in a
3774
+ * sheet painted from the `popover` scope, so no container clips them and a
3775
+ * long list scrolls. Each option has the option role; the list is a listbox
3776
+ * on the web only. Other props land on the trigger.
3777
+ *
3778
+ * @example
3779
+ * <Select label="Status" value={status} onValueChange={setStatus}
3780
+ * options={[{ value: "open", label: "Open" }, { value: "done", label: "Done" }]} />
3781
+ */
3782
+ export const Select: <V extends string | number = string | number>(
3783
+ props: FormControlProps & FormFieldOverrides & {
3784
+ options: Array<SelectOption<V>>;
3785
+ value?: V | null;
3786
+ onValueChange?: (value: V, option: SelectOption<V>) => void;
3787
+ /** Shown while nothing is chosen. Default "Select". */
3788
+ placeholder?: string;
3789
+ /** The sheet's heading. Defaults to a text `label`. */
3790
+ title?: string;
3791
+ /** Shown in the sheet when `options` is empty. Default "No options". */
3792
+ emptyText?: string;
3793
+ closeAccessibilityLabel?: string;
3794
+ /** The trigger. */
3795
+ style?: any;
3796
+ containerStyle?: any;
3797
+ [prop: string]: any;
3798
+ },
3799
+ ) => any;
3800
+
3801
+ /**
3802
+ * Space-key activation for a control whose role is not a button: on the web a
3803
+ * `Pressable` answers Space only for a button. Native ignores `onKeyDown`.
3804
+ *
3805
+ * @example
3806
+ * <Pressable accessibilityRole="radio" onPress={pick} onKeyDown={spaceActivates(pick)} />
3807
+ */
3808
+ export function spaceActivates(
3809
+ onPress: () => void,
3810
+ ): (event: { key: string; repeat?: boolean; preventDefault: () => void }) => void;
3811
+
3812
+ /**
3813
+ * Themed search box: a `TextField` with a search glyph, the search key and a
3814
+ * clear control. Every `TextField` prop works.
3815
+ *
3816
+ * @example
3817
+ * <SearchField value={query} onChangeText={setQuery} placeholder="Search orders" />
3818
+ */
3819
+ export const SearchField: (props: FormControlProps & {
3820
+ value?: string;
3821
+ onChangeText?: (text: string) => void;
3822
+ /** Called after the clear control empties the field. */
3823
+ onClear?: () => void;
3824
+ /** Default "Search". */
3825
+ placeholder?: string;
3826
+ /** Default "Clear search". */
3827
+ clearAccessibilityLabel?: string;
3828
+ /** The input. */
3829
+ style?: any;
3830
+ /** The box around it; use it for `flex: 1` in a toolbar. */
3831
+ containerStyle?: any;
3636
3832
  [prop: string]: any;
3637
3833
  }) => any;
3638
3834
 
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.
@@ -274,6 +282,8 @@ export { Spinner, Skeleton } from "./loading.js";
274
282
  export { Banner } from "./banner.js";
275
283
  // The standard dialog, on `<Overlay>`; follows the theme's `modal` scope.
276
284
  export { Modal } from "./modal.js";
285
+ // The standard confirmation, a preset of `<Modal>` and `<Button>`.
286
+ export { ConfirmDialog } from "./confirm-dialog.js";
277
287
  // The semantic text tiers, shared with the built-in widgets.
278
288
  export {
279
289
  textRoleStyle,
@@ -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.
@@ -267,6 +275,8 @@ export { Spinner, Skeleton } from "./loading.native.js";
267
275
  export { Banner } from "./banner.native.js";
268
276
  // The standard dialog, on `<Overlay>`; follows the theme's `modal` scope.
269
277
  export { Modal } from "./modal.native.js";
278
+ // The standard confirmation, a preset of `<Modal>` and `<Button>`.
279
+ export { ConfirmDialog } from "./confirm-dialog.native.js";
270
280
  // The semantic text tiers, shared with the built-in widgets.
271
281
  export {
272
282
  textRoleStyle,
@@ -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 });