@colixsystems/widget-sdk 0.237.0 → 0.238.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
@@ -3711,8 +3711,18 @@ export interface ListRowTokens {
3711
3711
  /**
3712
3712
  * Themed list container. Spaces and insets its `<ListRow>` children the way the
3713
3713
  * app's look says, and hands its overrides down to every row.
3714
+ *
3715
+ * @example
3716
+ * // A list of cards: the app's card look outside, its list look inside.
3717
+ * <List variant="card">
3718
+ * {orders.map((o) => <ListRow key={o.id} title={o.name} subtitle={o.status} onPress={() => open(o)} />)}
3719
+ * </List>
3714
3720
  */
3715
3721
  export const List: (props: ListRowTokens & {
3722
+ /** "card" draws every row as its own card (Theme Settings > Cards). Defaults to "plain". */
3723
+ variant?: "plain" | "card";
3724
+ /** The widget's own row look, used only where Theme Settings > Lists leaves a value unset. */
3725
+ defaults?: ListRowTokens;
3716
3726
  style?: any;
3717
3727
  testID?: string;
3718
3728
  children?: ReactNode;
@@ -3736,6 +3746,13 @@ export const ListRow: (props: ListRowTokens & {
3736
3746
  trailing?: ReactNode;
3737
3747
  /** Makes the row a button with a press state. */
3738
3748
  onPress?: () => void;
3749
+ /**
3750
+ * The row's own buttons (remove, move, sign), after `trailing`. On a row
3751
+ * with `onPress` they stay outside its tap target, each its own button.
3752
+ */
3753
+ actions?: ReactNode;
3754
+ /** As `<List defaults>`, for this row. */
3755
+ defaults?: ListRowTokens;
3739
3756
  selected?: boolean;
3740
3757
  disabled?: boolean;
3741
3758
  /** Defaults to true; pass false on the last row. */
@@ -4059,22 +4076,10 @@ export interface SelectOption<V extends string | number = string | number> {
4059
4076
  testID?: string;
4060
4077
  }
4061
4078
 
4062
- /**
4063
- * Themed single-choice picker. The trigger is the input field, a button that
4064
- * opens a dialog; its options open at screen level (through `Overlay`) in a
4065
- * sheet painted from the `popover` scope, so no container clips them and a
4066
- * long list scrolls. Each option has the option role; the list is a listbox
4067
- * on the web only. Other props land on the trigger.
4068
- *
4069
- * @example
4070
- * <Select label="Status" value={status} onValueChange={setStatus}
4071
- * options={[{ value: "open", label: "Open" }, { value: "done", label: "Done" }]} />
4072
- */
4073
- export const Select: <V extends string | number = string | number>(
4074
- props: FormControlProps & FormFieldOverrides & {
4079
+ /** What every `<Select>` takes, in either mode. */
4080
+ export type SelectBaseProps<V extends string | number = string | number> = FormControlProps &
4081
+ FormFieldOverrides & {
4075
4082
  options: Array<SelectOption<V>>;
4076
- value?: V | null;
4077
- onValueChange?: (value: V, option: SelectOption<V>) => void;
4078
4083
  /** Shown while nothing is chosen. Default "Select". */
4079
4084
  placeholder?: string;
4080
4085
  /** The sheet's heading. Defaults to a text `label`. */
@@ -4086,7 +4091,46 @@ export const Select: <V extends string | number = string | number>(
4086
4091
  style?: any;
4087
4092
  containerStyle?: any;
4088
4093
  [prop: string]: any;
4089
- },
4094
+ };
4095
+
4096
+ /**
4097
+ * Themed picker for one value of a known list, or several with `multiple`.
4098
+ * The trigger is the input field, a button that opens a dialog; its options
4099
+ * open at screen level (through `Overlay`) in a sheet painted from the
4100
+ * `popover` scope, so no container clips them and a long list scrolls. Each
4101
+ * option has the option role; the list is a listbox on the web only
4102
+ * (multiselectable with `multiple`). Other props land on the trigger.
4103
+ *
4104
+ * With `multiple` each option carries a tick box and toggles a draft; the
4105
+ * sheet's Done button commits it through `onValuesChange`, and closing it any
4106
+ * other way (backdrop, Escape, Android back) keeps the previous choice. The
4107
+ * trigger shows the picked labels as small chips while they fit on its line,
4108
+ * else "N selected".
4109
+ *
4110
+ * @example
4111
+ * <Select label="Status" value={status} onValueChange={setStatus}
4112
+ * options={[{ value: "open", label: "Open" }, { value: "done", label: "Done" }]} />
4113
+ * <Select multiple label="Tags" values={tags} onValuesChange={setTags}
4114
+ * options={[{ value: "red", label: "Red" }, { value: "blue", label: "Blue" }]} />
4115
+ */
4116
+ export const Select: <V extends string | number = string | number>(
4117
+ props:
4118
+ | (SelectBaseProps<V> & {
4119
+ multiple?: false;
4120
+ value?: V | null;
4121
+ onValueChange?: (value: V, option: SelectOption<V>) => void;
4122
+ })
4123
+ | (SelectBaseProps<V> & {
4124
+ multiple: true;
4125
+ /** The picked values; any order. */
4126
+ values?: V[] | null;
4127
+ /** Called on Done with the new values in listed order, only when they changed. */
4128
+ onValuesChange?: (values: V[], options: Array<SelectOption<V>>) => void;
4129
+ /** The trigger's text when the chips do not fit. Default `${count} selected`. */
4130
+ selectedCountText?: (count: number) => string;
4131
+ /** The sheet's confirm button. Default "Done". */
4132
+ doneText?: string;
4133
+ }),
4090
4134
  ) => any;
4091
4135
 
4092
4136
  /**
@@ -4263,6 +4307,101 @@ export const ChipGroup: (props: {
4263
4307
  testID?: string;
4264
4308
  }) => any;
4265
4309
 
4310
+ /**
4311
+ * A file as `<FilePicker>` and `<FileUpload>` hand it over: the browser's
4312
+ * `File` on the web, expo-file-system's `File` on the Expo export. Pass it to
4313
+ * `upload(file)` from `useFilestoreUpload` as it is.
4314
+ */
4315
+ export interface PickedFile {
4316
+ name: string;
4317
+ type?: string;
4318
+ size?: number;
4319
+ [key: string]: any;
4320
+ }
4321
+
4322
+ /**
4323
+ * The system file picker. Its children are the visible trigger. Web wraps a
4324
+ * hidden file input; native opens the OS document picker. `isSupported` is
4325
+ * false on a host with no picker. For an upload field use `<FileUpload>`.
4326
+ */
4327
+ export const FilePicker: ((props: {
4328
+ /** An HTML accept value: "image/*", ".pdf", "application/pdf". */
4329
+ accept?: string;
4330
+ multiple?: boolean;
4331
+ disabled?: boolean;
4332
+ /** One file, or an array when `multiple`. */
4333
+ onPick?: (picked: PickedFile | PickedFile[]) => void;
4334
+ /** The picker's keyboard focus, for a trigger that draws its own ring. */
4335
+ onFocus?: () => void;
4336
+ onBlur?: () => void;
4337
+ accessibilityLabel?: string;
4338
+ /** The id of the note that describes the picker, such as a field's error. */
4339
+ "aria-describedby"?: string;
4340
+ "aria-invalid"?: boolean;
4341
+ children?: ReactNode;
4342
+ }) => any) & { isSupported: boolean };
4343
+
4344
+ /**
4345
+ * Themed file chooser on `<FilePicker>`: a button (the `button` scope) in the
4346
+ * input field (the `muted` scope), the chosen files with their size and a
4347
+ * remove control, and the form controls' `label` / `helper` / `error` /
4348
+ * `disabled`. On the web a file can also be dropped onto the field; native
4349
+ * has no file drop, so the button is the control there. A file `accept` rules
4350
+ * out is not added and shows an error. It uploads nothing: upload in
4351
+ * `onPick` with `useFilestoreUpload` and pass `uploading` while it runs, which
4352
+ * shows an indeterminate `<Spinner>` and locks the field.
4353
+ *
4354
+ * @example
4355
+ * const { upload, uploading } = useFilestoreUpload({ spaceType: "project" });
4356
+ * const [photos, setPhotos] = useState([]);
4357
+ * <FileUpload label="Photos" accept="image/*" multiple uploading={uploading}
4358
+ * files={photos} onFilesChange={setPhotos}
4359
+ * onPick={(picked) => Promise.all(picked.map((f) => upload(f))).then(() => setPhotos([]))} />
4360
+ */
4361
+ export const FileUpload: (props: FormControlProps & {
4362
+ accept?: string;
4363
+ /** A new pick joins the list instead of replacing the file. Default false. */
4364
+ multiple?: boolean;
4365
+ /**
4366
+ * The listed files; pass with `onFilesChange` to own the list. A list the
4367
+ * field keeps itself still shows a file after its upload, so own it to clear it.
4368
+ */
4369
+ files?: PickedFile[];
4370
+ /** After every pick, drop and removal. */
4371
+ onFilesChange?: (files: PickedFile[]) => void;
4372
+ /** Once per pick or drop, with only the new files (an array when `multiple`). */
4373
+ onPick?: (picked: PickedFile | PickedFile[]) => void;
4374
+ onRemove?: (file: PickedFile, index: number) => void;
4375
+ /**
4376
+ * Files that were not added: `"type"` for ones `accept` ruled out, `"count"`
4377
+ * for the files beyond the first dropped on a field without `multiple`.
4378
+ */
4379
+ onReject?: (files: PickedFile[], reason: "type" | "count") => void;
4380
+ /** Shows the uploading state and locks the field. */
4381
+ uploading?: boolean;
4382
+ /** Default "Choose file", or "Choose files" when `multiple`. */
4383
+ buttonLabel?: string;
4384
+ /** Default "outlined". */
4385
+ buttonVariant?: "primary" | "secondary" | "outlined";
4386
+ /** Default "Uploading…". */
4387
+ uploadingLabel?: string;
4388
+ /** Shown while nothing is chosen, where no drop is possible. */
4389
+ emptyLabel?: string;
4390
+ /** Web only: the drop hint beside the button. */
4391
+ dropLabel?: string;
4392
+ /** The error a ruled-out file shows. */
4393
+ rejectedMessage?: string;
4394
+ /** Shown when several files reach a field without `multiple`. */
4395
+ singleFileMessage?: string;
4396
+ /** Replaces the helper on a host with no file picker. */
4397
+ unsupportedMessage?: string;
4398
+ /** Prefixed to each file's name. Defaults to the app's own "Remove". */
4399
+ removeAccessibilityLabel?: string;
4400
+ /** The field. */
4401
+ style?: any;
4402
+ containerStyle?: any;
4403
+ }) => any;
4404
+
4266
4405
  /**
4267
4406
  * Themed collapsible sections. The surface comes from the theme's `card`
4268
4407
  * scope and the headers and dividers from its `list` scope, so every
package/dist/index.js CHANGED
@@ -291,6 +291,8 @@ export { Modal } from "./modal.js";
291
291
  export { ConfirmDialog } from "./confirm-dialog.js";
292
292
  // Themed filter chips, tags and pills; follow the theme's `chip` scope.
293
293
  export { Chip, ChipGroup } from "./chip.js";
294
+ // A themed file chooser on `<FilePicker>`; follows the theme's `muted` scope.
295
+ export { FileUpload } from "./file-upload.js";
294
296
  // The semantic text tiers, shared with the built-in widgets.
295
297
  export {
296
298
  textRoleStyle,
@@ -284,6 +284,8 @@ export { Modal } from "./modal.native.js";
284
284
  export { ConfirmDialog } from "./confirm-dialog.native.js";
285
285
  // Themed filter chips, tags and pills; follow the theme's `chip` scope.
286
286
  export { Chip, ChipGroup } from "./chip.native.js";
287
+ // A themed file chooser on `<FilePicker>`; follows the theme's `muted` scope.
288
+ export { FileUpload } from "./file-upload.native.js";
287
289
  // The semantic text tiers, shared with the built-in widgets.
288
290
  export {
289
291
  textRoleStyle,
@@ -27,43 +27,60 @@ function isSet(value) {
27
27
  return value !== undefined && value !== null && value !== "";
28
28
  }
29
29
 
30
- function pick(overrides, scope, name) {
31
- if (overrides && isSet(overrides[name])) return overrides[name];
32
- if (scope && isSet(scope[name])) return scope[name];
30
+ // The ways `<List>` can draw its rows: flush bands / tiles, or one card each.
31
+ export const LIST_VARIANTS = Object.freeze(["plain", "card"]);
32
+
33
+ /** A `variant` value as `<List>` reads it; anything unknown is "plain". */
34
+ export function listVariant(value) {
35
+ return LIST_VARIANTS.includes(value) ? value : "plain";
36
+ }
37
+
38
+ function pick(name, ...layers) {
39
+ for (const layer of layers) {
40
+ if (layer && isSet(layer[name])) return layer[name];
41
+ }
33
42
  return undefined;
34
43
  }
35
44
 
45
+ function boundedList(values) {
46
+ return values ? normaliseThemeComponents({ list: values }).list : undefined;
47
+ }
48
+
36
49
  /**
37
50
  * Resolve one list's row values: explicit override, then the theme's `list`
38
- * scope, then the theme tokens. Both layers pass the contract's own bounds, so
39
- * a hand-edited or planned value cannot reach a native style unvalidated.
51
+ * scope, then the widget's own `defaults`, then the theme tokens. Every layer
52
+ * passes the contract's own bounds, so a hand-edited or planned value cannot
53
+ * reach a native style unvalidated.
40
54
  * Unthemed, a row is the flush band with a 1px hairline the Data List drew;
41
55
  * a `gap` makes separate tiles on the surface, outlined by the divider.
42
56
  *
43
57
  * @param {object} theme — the resolved workspace theme (`useTheme()` shape).
44
58
  * @param {object} [overrides] — per-list values keyed by LIST_TOKEN_NAMES.
59
+ * @param {object} [defaults] — a widget's own row look, keyed the same way,
60
+ * used only where the theme's Lists settings leave a value unset.
45
61
  */
46
- export function resolveListTokens(theme, overrides) {
62
+ export function resolveListTokens(theme, overrides, defaults) {
47
63
  const base = theme && typeof theme === "object" ? theme : DEFAULT_THEME_TOKENS;
48
64
  const fallback = DEFAULT_THEME_TOKENS;
49
65
  const colors = base.colors || fallback.colors;
50
66
  const spacing = base.spacing || fallback.spacing;
51
67
  const scope = base.components && base.components.list;
52
- const own = normaliseThemeComponents({ list: overrides }).list;
68
+ const layers = [boundedList(overrides), scope, boundedList(defaults)];
69
+ const value = (name) => pick(name, ...layers);
53
70
 
54
- const gap = pick(own, scope, "gap") ?? 0;
71
+ const gap = value("gap") ?? 0;
55
72
  const separated = gap > 0;
56
- const background = pick(own, scope, "background") ?? (separated ? colors.surface : undefined);
73
+ const background = value("background") ?? (separated ? colors.surface : undefined);
57
74
  return {
58
- rowPadding: pick(own, scope, "rowPadding") ?? spacing.md,
59
- rowMinHeight: pick(own, scope, "rowMinHeight") ?? LIST_ROW_MIN_HEIGHT,
75
+ rowPadding: value("rowPadding") ?? spacing.md,
76
+ rowMinHeight: value("rowMinHeight") ?? LIST_ROW_MIN_HEIGHT,
60
77
  gap,
61
78
  separated,
62
79
  // A flush band has square ends; a divider would curve into round ones.
63
- radius: separated ? pick(own, scope, "radius") ?? 0 : 0,
80
+ radius: separated ? value("radius") ?? 0 : 0,
64
81
  background,
65
- dividerColor: pick(own, scope, "dividerColor") ?? colors.border,
66
- dividerWidth: pick(own, scope, "dividerWidth") ?? 1,
82
+ dividerColor: value("dividerColor") ?? colors.border,
83
+ dividerWidth: value("dividerWidth") ?? 1,
67
84
  selectedBackground: colors.primarySoft || colors.surfaceMuted,
68
85
  selectedAccent: colors.primary,
69
86
  pressedBackground: mixHex(colors.onSurface, background || colors.surface, 0.94),
@@ -97,3 +114,26 @@ export function listRowFrame(tokens, spacing, selected = false) {
97
114
  const inset = tokens.rowPadding + listSideEdgeWidth(tokens) - LIST_SELECTED_BAR_WIDTH;
98
115
  return { ...frame, paddingLeft: Math.max(0, inset) };
99
116
  }
117
+
118
+ /**
119
+ * The row frame of a card-style list. The card scope paints the surface
120
+ * (fill, border, radius, padding, shadow, gradient); the list scope keeps
121
+ * only what is inside it: the height and the slots' arrangement.
122
+ *
123
+ * @param {ReturnType<typeof resolveListTokens>} tokens
124
+ * @param {{ md: number }} spacing — the theme's spacing scale.
125
+ * @param {boolean} [pressable] — a tap target keeps the 44px floor.
126
+ */
127
+ export function listCardRowFrame(tokens, spacing, pressable = false) {
128
+ return {
129
+ flexDirection: "row",
130
+ alignItems: "center",
131
+ gap: spacing.md,
132
+ minHeight: pressable ? Math.max(tokens.rowMinHeight, LIST_ROW_MIN_HEIGHT) : tokens.rowMinHeight,
133
+ };
134
+ }
135
+
136
+ /** The space between the cards of a card-style list: the Lists gap, else `spacing.sm`. */
137
+ export function listCardGap(tokens, spacing) {
138
+ return tokens.gap > 0 ? tokens.gap : spacing.sm;
139
+ }