@colixsystems/widget-sdk 0.237.0 → 0.239.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
@@ -39,6 +39,9 @@ export type WidgetPropertyType =
39
39
  // REQ-WDG-PAGEREF: page picker that emits a bare AppPage UUID; a widget
40
40
  // reads it and navigates via useNavigation().goTo(pageId, params).
41
41
  | "pageRef"
42
+ // REQ-BLD-19: section picker for the widget's own page → bare node id; a
43
+ // widget scrolls to it via useNavigation().scrollToSection(id).
44
+ | "sectionRef"
42
45
  // REQ-WDG-RICHTEXT: rich-text (HTML string) edited via a Tiptap editor.
43
46
  | "richText"
44
47
  // REQ-WDG-ASSET: file/asset picker → bare File UUID (Asset Manager picker).
@@ -841,6 +844,7 @@ export interface WidgetContext<TProps = unknown> {
841
844
  navigation: {
842
845
  goTo(pageId: string, params?: Record<string, string>): void;
843
846
  goToStartPage(): void;
847
+ scrollToSection(sectionId: string): void;
844
848
  goBack(): void;
845
849
  push(pageId: string, params?: Record<string, unknown>): void;
846
850
  replace(pageId: string, params?: Record<string, unknown>): void;
@@ -2210,12 +2214,15 @@ export function useFitScreenInsets(): FitScreenInsets;
2210
2214
  * The host-provided navigation surface. `goTo(pageId, params?)` navigates
2211
2215
  * to an internal app page; `goBack()` pops the stack. `goToStartPage()`
2212
2216
  * opens the signed-in user's own start page, which can differ per user.
2217
+ * `scrollToSection(sectionId)` scrolls this page to one of its sections (the
2218
+ * id comes from a `sectionRef` prop); an unknown section is a no-op.
2213
2219
  * Missing methods degrade to no-ops on the Studio canvas preview where no
2214
2220
  * live router is mounted. For external URLs use the `Linking` primitive.
2215
2221
  */
2216
2222
  export function useNavigation(): {
2217
2223
  goTo(pageId: string, params?: Record<string, unknown>): void;
2218
2224
  goToStartPage(): void;
2225
+ scrollToSection(sectionId: string): void;
2219
2226
  goBack(): void;
2220
2227
  push(pageId: string, params?: Record<string, unknown>): void;
2221
2228
  replace(pageId: string, params?: Record<string, unknown>): void;
@@ -3711,8 +3718,18 @@ export interface ListRowTokens {
3711
3718
  /**
3712
3719
  * Themed list container. Spaces and insets its `<ListRow>` children the way the
3713
3720
  * app's look says, and hands its overrides down to every row.
3721
+ *
3722
+ * @example
3723
+ * // A list of cards: the app's card look outside, its list look inside.
3724
+ * <List variant="card">
3725
+ * {orders.map((o) => <ListRow key={o.id} title={o.name} subtitle={o.status} onPress={() => open(o)} />)}
3726
+ * </List>
3714
3727
  */
3715
3728
  export const List: (props: ListRowTokens & {
3729
+ /** "card" draws every row as its own card (Theme Settings > Cards). Defaults to "plain". */
3730
+ variant?: "plain" | "card";
3731
+ /** The widget's own row look, used only where Theme Settings > Lists leaves a value unset. */
3732
+ defaults?: ListRowTokens;
3716
3733
  style?: any;
3717
3734
  testID?: string;
3718
3735
  children?: ReactNode;
@@ -3736,6 +3753,13 @@ export const ListRow: (props: ListRowTokens & {
3736
3753
  trailing?: ReactNode;
3737
3754
  /** Makes the row a button with a press state. */
3738
3755
  onPress?: () => void;
3756
+ /**
3757
+ * The row's own buttons (remove, move, sign), after `trailing`. On a row
3758
+ * with `onPress` they stay outside its tap target, each its own button.
3759
+ */
3760
+ actions?: ReactNode;
3761
+ /** As `<List defaults>`, for this row. */
3762
+ defaults?: ListRowTokens;
3739
3763
  selected?: boolean;
3740
3764
  disabled?: boolean;
3741
3765
  /** Defaults to true; pass false on the last row. */
@@ -4059,22 +4083,10 @@ export interface SelectOption<V extends string | number = string | number> {
4059
4083
  testID?: string;
4060
4084
  }
4061
4085
 
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 & {
4086
+ /** What every `<Select>` takes, in either mode. */
4087
+ export type SelectBaseProps<V extends string | number = string | number> = FormControlProps &
4088
+ FormFieldOverrides & {
4075
4089
  options: Array<SelectOption<V>>;
4076
- value?: V | null;
4077
- onValueChange?: (value: V, option: SelectOption<V>) => void;
4078
4090
  /** Shown while nothing is chosen. Default "Select". */
4079
4091
  placeholder?: string;
4080
4092
  /** The sheet's heading. Defaults to a text `label`. */
@@ -4086,7 +4098,46 @@ export const Select: <V extends string | number = string | number>(
4086
4098
  style?: any;
4087
4099
  containerStyle?: any;
4088
4100
  [prop: string]: any;
4089
- },
4101
+ };
4102
+
4103
+ /**
4104
+ * Themed picker for one value of a known list, or several with `multiple`.
4105
+ * The trigger is the input field, a button that opens a dialog; its options
4106
+ * open at screen level (through `Overlay`) in a sheet painted from the
4107
+ * `popover` scope, so no container clips them and a long list scrolls. Each
4108
+ * option has the option role; the list is a listbox on the web only
4109
+ * (multiselectable with `multiple`). Other props land on the trigger.
4110
+ *
4111
+ * With `multiple` each option carries a tick box and toggles a draft; the
4112
+ * sheet's Done button commits it through `onValuesChange`, and closing it any
4113
+ * other way (backdrop, Escape, Android back) keeps the previous choice. The
4114
+ * trigger shows the picked labels as small chips while they fit on its line,
4115
+ * else "N selected".
4116
+ *
4117
+ * @example
4118
+ * <Select label="Status" value={status} onValueChange={setStatus}
4119
+ * options={[{ value: "open", label: "Open" }, { value: "done", label: "Done" }]} />
4120
+ * <Select multiple label="Tags" values={tags} onValuesChange={setTags}
4121
+ * options={[{ value: "red", label: "Red" }, { value: "blue", label: "Blue" }]} />
4122
+ */
4123
+ export const Select: <V extends string | number = string | number>(
4124
+ props:
4125
+ | (SelectBaseProps<V> & {
4126
+ multiple?: false;
4127
+ value?: V | null;
4128
+ onValueChange?: (value: V, option: SelectOption<V>) => void;
4129
+ })
4130
+ | (SelectBaseProps<V> & {
4131
+ multiple: true;
4132
+ /** The picked values; any order. */
4133
+ values?: V[] | null;
4134
+ /** Called on Done with the new values in listed order, only when they changed. */
4135
+ onValuesChange?: (values: V[], options: Array<SelectOption<V>>) => void;
4136
+ /** The trigger's text when the chips do not fit. Default `${count} selected`. */
4137
+ selectedCountText?: (count: number) => string;
4138
+ /** The sheet's confirm button. Default "Done". */
4139
+ doneText?: string;
4140
+ }),
4090
4141
  ) => any;
4091
4142
 
4092
4143
  /**
@@ -4263,6 +4314,101 @@ export const ChipGroup: (props: {
4263
4314
  testID?: string;
4264
4315
  }) => any;
4265
4316
 
4317
+ /**
4318
+ * A file as `<FilePicker>` and `<FileUpload>` hand it over: the browser's
4319
+ * `File` on the web, expo-file-system's `File` on the Expo export. Pass it to
4320
+ * `upload(file)` from `useFilestoreUpload` as it is.
4321
+ */
4322
+ export interface PickedFile {
4323
+ name: string;
4324
+ type?: string;
4325
+ size?: number;
4326
+ [key: string]: any;
4327
+ }
4328
+
4329
+ /**
4330
+ * The system file picker. Its children are the visible trigger. Web wraps a
4331
+ * hidden file input; native opens the OS document picker. `isSupported` is
4332
+ * false on a host with no picker. For an upload field use `<FileUpload>`.
4333
+ */
4334
+ export const FilePicker: ((props: {
4335
+ /** An HTML accept value: "image/*", ".pdf", "application/pdf". */
4336
+ accept?: string;
4337
+ multiple?: boolean;
4338
+ disabled?: boolean;
4339
+ /** One file, or an array when `multiple`. */
4340
+ onPick?: (picked: PickedFile | PickedFile[]) => void;
4341
+ /** The picker's keyboard focus, for a trigger that draws its own ring. */
4342
+ onFocus?: () => void;
4343
+ onBlur?: () => void;
4344
+ accessibilityLabel?: string;
4345
+ /** The id of the note that describes the picker, such as a field's error. */
4346
+ "aria-describedby"?: string;
4347
+ "aria-invalid"?: boolean;
4348
+ children?: ReactNode;
4349
+ }) => any) & { isSupported: boolean };
4350
+
4351
+ /**
4352
+ * Themed file chooser on `<FilePicker>`: a button (the `button` scope) in the
4353
+ * input field (the `muted` scope), the chosen files with their size and a
4354
+ * remove control, and the form controls' `label` / `helper` / `error` /
4355
+ * `disabled`. On the web a file can also be dropped onto the field; native
4356
+ * has no file drop, so the button is the control there. A file `accept` rules
4357
+ * out is not added and shows an error. It uploads nothing: upload in
4358
+ * `onPick` with `useFilestoreUpload` and pass `uploading` while it runs, which
4359
+ * shows an indeterminate `<Spinner>` and locks the field.
4360
+ *
4361
+ * @example
4362
+ * const { upload, uploading } = useFilestoreUpload({ spaceType: "project" });
4363
+ * const [photos, setPhotos] = useState([]);
4364
+ * <FileUpload label="Photos" accept="image/*" multiple uploading={uploading}
4365
+ * files={photos} onFilesChange={setPhotos}
4366
+ * onPick={(picked) => Promise.all(picked.map((f) => upload(f))).then(() => setPhotos([]))} />
4367
+ */
4368
+ export const FileUpload: (props: FormControlProps & {
4369
+ accept?: string;
4370
+ /** A new pick joins the list instead of replacing the file. Default false. */
4371
+ multiple?: boolean;
4372
+ /**
4373
+ * The listed files; pass with `onFilesChange` to own the list. A list the
4374
+ * field keeps itself still shows a file after its upload, so own it to clear it.
4375
+ */
4376
+ files?: PickedFile[];
4377
+ /** After every pick, drop and removal. */
4378
+ onFilesChange?: (files: PickedFile[]) => void;
4379
+ /** Once per pick or drop, with only the new files (an array when `multiple`). */
4380
+ onPick?: (picked: PickedFile | PickedFile[]) => void;
4381
+ onRemove?: (file: PickedFile, index: number) => void;
4382
+ /**
4383
+ * Files that were not added: `"type"` for ones `accept` ruled out, `"count"`
4384
+ * for the files beyond the first dropped on a field without `multiple`.
4385
+ */
4386
+ onReject?: (files: PickedFile[], reason: "type" | "count") => void;
4387
+ /** Shows the uploading state and locks the field. */
4388
+ uploading?: boolean;
4389
+ /** Default "Choose file", or "Choose files" when `multiple`. */
4390
+ buttonLabel?: string;
4391
+ /** Default "outlined". */
4392
+ buttonVariant?: "primary" | "secondary" | "outlined";
4393
+ /** Default "Uploading…". */
4394
+ uploadingLabel?: string;
4395
+ /** Shown while nothing is chosen, where no drop is possible. */
4396
+ emptyLabel?: string;
4397
+ /** Web only: the drop hint beside the button. */
4398
+ dropLabel?: string;
4399
+ /** The error a ruled-out file shows. */
4400
+ rejectedMessage?: string;
4401
+ /** Shown when several files reach a field without `multiple`. */
4402
+ singleFileMessage?: string;
4403
+ /** Replaces the helper on a host with no file picker. */
4404
+ unsupportedMessage?: string;
4405
+ /** Prefixed to each file's name. Defaults to the app's own "Remove". */
4406
+ removeAccessibilityLabel?: string;
4407
+ /** The field. */
4408
+ style?: any;
4409
+ containerStyle?: any;
4410
+ }) => any;
4411
+
4266
4412
  /**
4267
4413
  * Themed collapsible sections. The surface comes from the theme's `card`
4268
4414
  * 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
+ }