@colixsystems/widget-sdk 0.246.0 → 0.248.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.
Files changed (54) hide show
  1. package/README.md +76 -6
  2. package/dist/board-auto-scroll.js +59 -0
  3. package/dist/board-view.js +327 -0
  4. package/dist/chart-logic.js +575 -0
  5. package/dist/chart-tokens.js +131 -0
  6. package/dist/chart-view.js +262 -0
  7. package/dist/chart.js +21 -0
  8. package/dist/chart.native.js +10 -0
  9. package/dist/contract.cjs +119 -7
  10. package/dist/contract.js +119 -7
  11. package/dist/data-table-logic.js +179 -0
  12. package/dist/data-table-view.js +388 -0
  13. package/dist/data-table.js +22 -0
  14. package/dist/data-table.native.js +14 -0
  15. package/dist/drag-kit-view.js +310 -0
  16. package/dist/file-upload-view.js +13 -2
  17. package/dist/filter-bar-logic.js +143 -0
  18. package/dist/filter-bar-view.js +381 -0
  19. package/dist/filter-bar.js +15 -0
  20. package/dist/filter-bar.native.js +13 -0
  21. package/dist/form-control-logic.js +20 -3
  22. package/dist/form-control-view.js +18 -0
  23. package/dist/form-layout-logic.js +92 -0
  24. package/dist/form-view.js +268 -0
  25. package/dist/form.js +15 -0
  26. package/dist/form.native.js +9 -0
  27. package/dist/grid-logic.js +222 -0
  28. package/dist/grid-view.js +109 -0
  29. package/dist/grid.js +7 -0
  30. package/dist/grid.native.js +7 -0
  31. package/dist/host.d.ts +6 -0
  32. package/dist/host.js +2 -0
  33. package/dist/index.d.ts +606 -7
  34. package/dist/index.js +15 -0
  35. package/dist/index.native.js +15 -0
  36. package/dist/menu-view.js +5 -2
  37. package/dist/people-picker-logic.js +203 -0
  38. package/dist/people-picker-view.js +250 -0
  39. package/dist/people-picker.js +11 -0
  40. package/dist/people-picker.native.js +9 -0
  41. package/dist/radio-group-view.js +7 -3
  42. package/dist/select-search-logic.js +150 -0
  43. package/dist/select-view.js +251 -18
  44. package/dist/select.js +6 -2
  45. package/dist/select.native.js +5 -3
  46. package/dist/sortable-list-view.js +228 -0
  47. package/dist/sortable-logic.js +273 -0
  48. package/dist/sortable-tokens.js +120 -0
  49. package/dist/sortable-view.js +24 -0
  50. package/dist/sortable.js +39 -0
  51. package/dist/sortable.native.js +30 -0
  52. package/dist/text-field-view.js +7 -2
  53. package/package.json +1 -1
  54. package/src/contract.js +119 -7
@@ -0,0 +1,30 @@
1
+ // `<SortableList>` and `<Board>` (native). Bind ./sortable-view.js to React
2
+ // Native's own primitives; only the imports and the handle style differ from
3
+ // ./sortable.js.
4
+
5
+ import { View, Text, ScrollView, Animated, PanResponder, Platform, AccessibilityInfo } from "react-native";
6
+ import { List, ListRow } from "./list.native.js";
7
+ import { Card } from "./card.native.js";
8
+ import { Menu } from "./popover.native.js";
9
+ import { makeDragAndDrop } from "./sortable-view.js";
10
+
11
+ // A device scrolls nothing from a handle that claims the gesture.
12
+ const handleSurface = null;
13
+ // Nor can the page scroll under a held handle.
14
+ const onOutsideScroll = null;
15
+
16
+ export const { SortableList, Board } = makeDragAndDrop({
17
+ View,
18
+ Text,
19
+ ScrollView,
20
+ Animated,
21
+ PanResponder,
22
+ Platform,
23
+ AccessibilityInfo,
24
+ List,
25
+ ListRow,
26
+ Card,
27
+ Menu,
28
+ handleSurface,
29
+ onOutsideScroll,
30
+ });
@@ -3,7 +3,7 @@
3
3
  // from, so the hosts cannot drift.
4
4
 
5
5
  import React from "react";
6
- import { useHostTheme } from "./hooks.js";
6
+ import { useHostSharedText, useHostTheme } from "./hooks.js";
7
7
  import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
8
8
  import { TEXT_FIELD_TOKEN_NAMES } from "./text-field-tokens.js";
9
9
  import {
@@ -12,6 +12,7 @@ import {
12
12
  resolveFormControlTokens,
13
13
  } from "./form-control-tokens.js";
14
14
  import {
15
+ DRAWS_FIELD_FRAME,
15
16
  errorMessage,
16
17
  hasNote,
17
18
  isShown,
@@ -44,11 +45,13 @@ export function makeTextField(rn) {
44
45
  helper,
45
46
  error,
46
47
  disabled = false,
48
+ required = false,
47
49
  containerStyle,
48
50
  accessibilityLabel,
49
51
  ...others
50
52
  } = props;
51
53
  const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
54
+ const requiredWord = useHostSharedText("required", "Required");
52
55
  const [focused, setFocused] = React.useState(false);
53
56
  const overrides = {};
54
57
  const rest = {};
@@ -67,7 +70,7 @@ export function makeTextField(rn) {
67
70
  ref: setRef,
68
71
  multiline,
69
72
  placeholderTextColor: placeholderTextColor || t.placeholderColor,
70
- accessibilityLabel: spokenLabel(accessibilityLabel, label),
73
+ accessibilityLabel: spokenLabel(accessibilityLabel, label, required ? requiredWord : null),
71
74
  ...(disabled ? { editable: false, "aria-disabled": true } : null),
72
75
  ...(invalid ? { "aria-invalid": true } : null),
73
76
  ...(hasNote({ helper, error }) ? { "aria-describedby": noteId } : null),
@@ -113,6 +116,7 @@ export function makeTextField(rn) {
113
116
  helper,
114
117
  error: errorMessage(error),
115
118
  disabled,
119
+ required,
116
120
  noteId,
117
121
  onLabelPress: () => {
118
122
  const node = inputRef.current;
@@ -125,5 +129,6 @@ export function makeTextField(rn) {
125
129
  });
126
130
 
127
131
  TextField.displayName = "TextField";
132
+ TextField[DRAWS_FIELD_FRAME] = true;
128
133
  return TextField;
129
134
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.246.0",
3
+ "version": "0.248.0",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "homepage": "https://github.com/Colix-AB/AppStudio",
6
6
  "type": "module",
package/src/contract.js CHANGED
@@ -622,7 +622,7 @@ const THEME_COMPONENTS = Object.freeze({
622
622
  muted: Object.freeze({
623
623
  label: "Inputs",
624
624
  // The SDK's form controls read this scope directly, in every widget.
625
- sdkComponents: Object.freeze(["TextField", "Checkbox", "Switch", "Select", "SearchField", "FileUpload", "RadioGroup"]),
625
+ sdkComponents: Object.freeze(["TextField", "Checkbox", "Switch", "Select", "SearchField", "FileUpload", "RadioGroup", "PeoplePicker"]),
626
626
  tokens: Object.freeze({
627
627
  background: Object.freeze({ type: "color", uiDefault: "colors.surfaceMuted" }),
628
628
  textColor: Object.freeze({ type: "color", uiDefault: "colors.onSurfaceMuted" }),
@@ -641,7 +641,7 @@ const THEME_COMPONENTS = Object.freeze({
641
641
  // radius and shadow keep the `<Overlay>` panel's own.
642
642
  popover: Object.freeze({
643
643
  label: "Dropdown menus",
644
- sdkComponents: Object.freeze(["Select", "Menu", "Popover", "Tooltip"]),
644
+ sdkComponents: Object.freeze(["Select", "Menu", "Popover", "Tooltip", "PeoplePicker"]),
645
645
  tokens: Object.freeze({
646
646
  background: Object.freeze({ type: "color", uiDefault: "colors.surface" }),
647
647
  textColor: Object.freeze({ type: "color", uiDefault: "colors.onSurface" }),
@@ -784,6 +784,25 @@ const THEME_COMPONENTS = Object.freeze({
784
784
  }),
785
785
  targets: Object.freeze({}),
786
786
  }),
787
+ // Charts: `<Chart>` reads this scope directly, so it names the SDK
788
+ // component it styles and binds no widget field. Unset series colours
789
+ // follow the theme's own colours, so their defaults are derived.
790
+ chart: Object.freeze({
791
+ label: "Charts",
792
+ sdkComponents: Object.freeze(["Chart"]),
793
+ tokens: Object.freeze({
794
+ series1: Object.freeze({ type: "color" }),
795
+ series2: Object.freeze({ type: "color" }),
796
+ series3: Object.freeze({ type: "color" }),
797
+ series4: Object.freeze({ type: "color" }),
798
+ series5: Object.freeze({ type: "color" }),
799
+ series6: Object.freeze({ type: "color" }),
800
+ axisColor: Object.freeze({ type: "color", uiDefault: "colors.onSurfaceMuted" }),
801
+ gridColor: Object.freeze({ type: "color", uiDefault: "colors.border" }),
802
+ barRadius: Object.freeze({ type: "size", min: 0, max: 24, uiDefault: "radii.sm" }),
803
+ }),
804
+ targets: Object.freeze({}),
805
+ }),
787
806
  });
788
807
 
789
808
  // sc-4505 — the value types a widget event payload field may declare. Narrower
@@ -2536,7 +2555,8 @@ const HOOKS = [
2536
2555
  "Returns { groups, loading, error, refetch, create, remove, addMember, " +
2537
2556
  "removeMember }. list returns the { data, meta } envelope verbatim — the " +
2538
2557
  "hook unwraps res.data; rows are snake_case. Reads need groups.read:*; " +
2539
- "mutations need groups.write:*.",
2558
+ "mutations need groups.write:*. query { q?, ids?, limit?, offset? }: " +
2559
+ "`ids` (1 to 100) names the groups behind ids you already hold.",
2540
2560
  returnShape: {
2541
2561
  groups: "Array<{ id, name, member_count }> // snake_case rows; unwrapped from { data, meta }",
2542
2562
  loading: "boolean",
@@ -3341,6 +3361,13 @@ const PRIMITIVES = [
3341
3361
  rnComponent: null,
3342
3362
  docsUrl: null,
3343
3363
  },
3364
+ {
3365
+ name: "DataTable",
3366
+ description:
3367
+ 'A list of records laid out for its own width. `<DataTable columns={[{ key: "name", label: "Name", flex: 2, sortable: true }, { key: "status", label: "Status", render: (row) => <Badge label={row.status} /> }, { key: "total", label: "Total", align: "right" }]} rows={data} loading={loading} onRowPress={(row) => open(row.id)} />` — use it by default for a list or table of records (datastore rows): it is stacked label/value cards when narrow, one card per row at medium (the first column as the title, the rest on one line), and aligned columns under a sticky header when wide, from its own measured width, so it needs no tier code of yours. When the table needs what it cannot do (editable cells, grouped or collapsible rows, resizable columns, a pivot), build your own from `<ListHeader>` over a `<List>` of `<ListRow>`s with the same `flex` per column. Columns: `key`, `label`, `flex` (default 1, the same in the header and the rows), `align` ("left" / "center" / "right"), `minWidth`, `sortable`, `render(row, column)` (a string or number becomes the cell\'s text). Props: `columns`, `rows`, `keyField` (default "id"), `renderCell(row, column)` (every cell without its own `render`; return `undefined` for the default), `onRowPress(row)`, `sort` / `defaultSort` (`{ key, direction: "asc" | "desc" }`), `onSortChange(sort)`, `maxHeight` (the rows\' scroll box, default 480), `loading` (placeholder rows while `rows` is empty), `emptyTitle`, `emptyMessage` (an empty state, "No results" by default), `sortLabels` (`{ sort, ascending, descending }` in the widget\'s language), the Lists overrides (`rowPadding`, `gap`, …), `style`, `testID`, `accessibilityLabel`. A sortable header is a 44px button named by its column and described by its sort, and a change of sort is announced once; the card layouts offer the same sort from a menu. With `onSortChange` you order the rows (re-query with the new sort) and the table draws `rows` as given; without it the table sorts them itself (a `Date` value sorts as a date). A controlled `sort` without `onSortChange` is display-only: the rows follow it and the headers are plain labels. The sticky header is filled with the rows\' Lists background, else the theme surface. It is styled by Design > Components > Lists (and Cards for the card layouts).',
3368
+ rnComponent: null,
3369
+ docsUrl: null,
3370
+ },
3344
3371
  {
3345
3372
  name: "Badge",
3346
3373
  description:
@@ -3377,6 +3404,13 @@ const PRIMITIVES = [
3377
3404
  rnComponent: null,
3378
3405
  docsUrl: null,
3379
3406
  },
3407
+ {
3408
+ name: "Chart",
3409
+ description:
3410
+ 'Themed chart. `<Chart type="bar" data={groups.map((g) => ({ label: String(g.group), value: Number(g.sum) || 0 }))} loading={loading} valueFormat="compact" />`. Use it by default to plot a table\'s figures: feed it ONE `useDatastoreAggregate` call (`groups` for a breakdown by a column, `buckets` for a series over time) mapped to `[{ label, value }]`. `type` is "bar" (default), "hbar" (horizontal bars, for long category names), "line", "area", "donut" (the parts of a whole) or "sparkline" (a small line inside a tile, with no axes). It measures its own width and redraws when its container resizes, shows a `<Skeleton>` while `loading` and an `<EmptyState>` when there is nothing to plot, draws a legend for a donut and for several series, and gives a screen reader a one-line summary followed by the values. Its series colours, axis and label colour, gridline colour and bar corners come from the app\'s chart look (Design > Components > Charts), falling back to the theme\'s colours, so pass no colours of your own. Props: `data` (`[{ label, value }]`, or one number per series key), `series` (`[{ key, label, color? }]` for several series sharing one `data`: bars stand side by side, lines and areas overlay; a donut and a sparkline draw the first), `type`, `valueFormat` ("number" default | "compact", 1.2k | "percent", which appends % to a value already in percent, 42 reads 42% | a function of the number; used for the axis, the legend and the spoken values, grouped for the app\'s language), `height` (px, default 220; 48 for a sparkline), `loading`, `emptyTitle` (default the shared "No results" text) and `emptyMessage`, `accessibilityLabel` (replaces the spoken summary sentence; pass your translated title), `moreText` (`(count) => string`, the tail of a long spoken value list, default "and N more"; pass it translated), `style`, `testID`. When the request needs a chart it cannot draw (a scatter plot, a gantt or timeline, brushing or tapping a bar to filter), build your own with react-native-svg in the app\'s look (see DRAWING A CHART), painting it with `chartColors(useTheme())`: `series(i)` and `seriesOpacity(i)` for the i-th series, `axis` and `grid`, the colours `<Chart>` uses.',
3411
+ rnComponent: null,
3412
+ docsUrl: null,
3413
+ },
3380
3414
  {
3381
3415
  name: "Divider",
3382
3416
  description:
@@ -3384,6 +3418,48 @@ const PRIMITIVES = [
3384
3418
  rnComponent: null,
3385
3419
  docsUrl: null,
3386
3420
  },
3421
+ {
3422
+ name: "Grid",
3423
+ description:
3424
+ 'Balanced lines of cells sized from the grid\'s own measured width. `<Grid minCellWidth={240}>{items.map((item) => <Card key={item.id} style={{ flex: 1 }}>…</Card>)}</Grid>` — use it by default for a card grid, a tile or stat row, and a grid of definite-size cells (calendar days, seats, swatches); when the layout needs what it cannot express (masonry, a bento layout whose cells span lines), lay it out yourself with `useContainerWidth()`. It measures an inner box with no padding, so padding in its `style` is never counted into the width. `balance` "cards" (default): as many cells per line as fit at `minCellWidth` (240 when unset), every line but the last full and the last padded with empty cells so its cards keep the width above (6 that fit 5 are 3 + 3). "tiles": the remainder spreads over every line and each line stretches (5 at 4 per line are 3 + 2, never 4 + 1); the per-line ceiling is `maxColumns`, or what fits at `minCellWidth`, or else 4 / 3 / 2 for the wide / medium / narrow width tier. `columns` fixes the count per line. `cell={{ min, max }}` makes definite-size cells as large as fit up to `max` (never below `min`, 32 when unset) in a block centred in the grid (a block whose `min` cells cannot fit starts at the leading edge, so only its far end is cut off), `square` makes them as tall as wide: `<Grid columns={7} cell={{ min: 32, max: 64 }} square gap="xs">`. Props: `children` (one cell each; each keeps its own look and theme scope, and a child with `flex: 1` fills its cell\'s height so a line of cards ends level), `balance`, `minCellWidth`, `maxColumns`, `columns`, `cell`, `square`, `gap` (a theme spacing name, default "md", or a number), `style` (the outer box), `testID`, `accessibilityLabel`. A layout that depends on the width draws nothing until it is measured. In a row, give it `style={{ flex: 1, minWidth: 0 }}` so it has a width to measure.',
3425
+ rnComponent: null,
3426
+ docsUrl: null,
3427
+ },
3428
+ {
3429
+ name: "Form",
3430
+ description:
3431
+ 'A form laid out in width tiers from its own measured width. `<Form><FieldRow><TextField label="First name" … /><TextField label="Last name" … /></FieldRow><TextField label="Email" required … /><FormActions><Button variant="secondary" title="Cancel" … /><Button title="Save" … /></FormActions></Form>` — use it by default for a widget that captures or edits data: its fields sit one to a line when narrow, two at medium and up to three when wide, and the form is capped near 1000px and centred, so it is never flush left, a narrow strip in an empty page, or a wall of giant inputs. A `<FormField>` or a control that frames its own label (`<TextField>`, `<Select>`, `<RadioGroup>`, `<PeoplePicker>`, `<FileUpload>`) is one field; a multiline field, a `<FormField span="full">`, a `<FieldRow>`, a `<FormSection>`, a `<FormActions>` and anything else (a `<Banner>`, a paragraph) take the whole line. Its direct children are read, so do not wrap them in a fragment. Props: `children`, `columns` ("auto" — three when wide — or 1–3, the most fields to a line), `maxWidth` (1000 by default, `null` for none, for a form in a side panel), `gap` (a theme spacing name, default "md", or a number), `style` (the outer box), `testID`, `accessibilityLabel`. When the form needs what it cannot do (a multi-step wizard, a form beside a live preview, fields laid over a picture), lay it out yourself in the app\'s look with `useContainerWidth()` and `widthTier()`, keeping the three tiers.',
3432
+ rnComponent: null,
3433
+ docsUrl: null,
3434
+ },
3435
+ {
3436
+ name: "FieldRow",
3437
+ description:
3438
+ 'Short related fields on one line inside a `<Form>`: first + last name, city + postal code, expiry + CVC. `<FieldRow><TextField label="City" … /><TextField label="Postal code" … /></FieldRow>`. Up to three fields share the line; when narrow they stack. Props: `children`, `gap`, `style`, `testID`.',
3439
+ rnComponent: null,
3440
+ docsUrl: null,
3441
+ },
3442
+ {
3443
+ name: "FormSection",
3444
+ description:
3445
+ 'A titled group of fields inside a `<Form>`, laid out in the form\'s own columns. `<FormSection title="Payment" description="Charged when the order ships">…</FormSection>`. Group a form past about six fields into sections. The title is a heading for screen readers too, in the subtitle text role; the description is in the helper colour. Props: `title`, `description`, `children`, `gap`, `style`, `testID`.',
3446
+ rnComponent: null,
3447
+ docsUrl: null,
3448
+ },
3449
+ {
3450
+ name: "FormActions",
3451
+ description:
3452
+ 'A form\'s buttons in normal flow after its fields, never over them. `<FormActions><Button variant="secondary" title="Cancel" onPress={cancel} /><Button title="Save" onPress={save} busy={saving} /></FormActions>`. Pass the secondary first and the primary last: at medium and wide they sit in a row at the trailing edge, the primary last; when narrow they stack full-width in the same order, the primary nearest the thumb. Props: `children`, `style`, `testID`. In a `<Modal>`, pass the buttons to its `footer` instead.',
3453
+ rnComponent: null,
3454
+ docsUrl: null,
3455
+ },
3456
+ {
3457
+ name: "FormField",
3458
+ description:
3459
+ 'The label, required mark, helper and error of the form controls, round any control. `<FormField label="Colour" required helper="Shown on the card" error={colourError}><SwatchPicker … /></FormField>`. A control that frames its own label (`<TextField>`, `<Select>`, `<RadioGroup>`, `<PeoplePicker>`, `<FileUpload>`) is handed them instead, so one label is drawn (a prop the control sets itself wins, `disabled={false}` included; the field\'s `testID` goes to the control, or to a wrapper round it when the control has its own); those five also take `required` directly. Any other child is framed here and named to a screen reader: on the web as a group, with the helper or error as its description; on a device the one child itself takes the label (with "Required") as its name and the helper or error as its hint, unless it names itself, when the label speaks the required word. Give that child no label of its own. `required` draws the mark and adds the app\'s "Required" to the spoken name; it does not validate, so pass `error` yourself. Props: `label`, `required`, `helper`, `error`, `disabled`, `span` ("full" takes the form\'s whole line), `style`, `testID`, `children`.',
3460
+ rnComponent: null,
3461
+ docsUrl: null,
3462
+ },
3387
3463
  {
3388
3464
  name: "Banner",
3389
3465
  description:
@@ -3464,7 +3540,7 @@ const PRIMITIVES = [
3464
3540
  {
3465
3541
  name: "Select",
3466
3542
  description:
3467
- 'Themed picker for ONE value of a known list, or several with `multiple`. `<Select label="Status" value={status} onValueChange={setStatus} options={[{ value: "open", label: "Open" }, { value: "done", label: "Done" }]} placeholder="Choose a status" />` — the default way to let the user pick ONE of a known list. The trigger is the app\'s input field (Design > Components > Inputs) showing the chosen label; pressing it opens the options at SCREEN level, in a sheet painted from Design > Components > Dropdown menus, so no card or scroll container can clip them, and a long list scrolls inside the sheet. The trigger is a button that opens a dialog (expanded state included) and each option carries the option role and its selected state on both hosts; the list around them is a listbox on the web only. On the web Enter, Space or an arrow key opens it, the arrow keys, Home and End move between options, Enter or Space chooses, Escape closes, and focus returns to the trigger; on Android the back button closes it. Props: `options` (`[{ value, label, disabled?, testID? }]`, `value` a string or number, unique), `value`, `onValueChange(value, option)`, `placeholder` (default "Select"), `title` (the sheet\'s heading; defaults to a text `label`), `emptyText` (default "No options"), `label`, `helper`, `error`, `disabled`, `accessibilityLabel`, `closeAccessibilityLabel`, `background` / `textColor` / `borderColor` / `radius` / `height` to override one field, `style` (the trigger), `containerStyle`, `testID`; any other prop lands on the trigger. Use it rather than a dropdown of your own; when the request needs options it cannot show (colour swatches, photos with two-line captions), put a small swatch or photo picker in a `<Popover>` at its button, or a long or full-screen option list in a `<Modal>` (an emoji choice is `<EmojiPicker>`) — never an absolutely positioned list, which the widget\'s container clips on both hosts. MULTIPLE: `<Select multiple label="Tags" values={tags} onValuesChange={setTags} options={tagOptions} />` — the default way to let the user pick SEVERAL of a known list. The same trigger and sheet; each option shows a tick box and toggles, the listbox is multiselectable on the web, and the sheet\'s Done button commits the choice through `onValuesChange(values, options)` (values in listed order, called only when they changed); closing the sheet any other way (backdrop, Escape, Android back) keeps the previous choice. Space toggles the focused option on the web. The trigger shows the picked labels as small chips while they fit on its one line, otherwise "N selected" (`selectedCountText(count)` to translate it), and `placeholder` while nothing is picked; `doneText` (default "Done") names the button. In this mode `value` / `onValueChange` are ignored. A handful of options always shown inline is a `<ChipGroup selection="multiple">` of `<Chip>`s instead.',
3543
+ 'Themed picker for ONE value of a known list, or several with `multiple`. `<Select label="Status" value={status} onValueChange={setStatus} options={[{ value: "open", label: "Open" }, { value: "done", label: "Done" }]} placeholder="Choose a status" />` — the default way to let the user pick ONE of a known list. The trigger is the app\'s input field (Design > Components > Inputs) showing the chosen label; pressing it opens the options at SCREEN level, in a sheet painted from Design > Components > Dropdown menus, so no card or scroll container can clip them, and a long list scrolls inside the sheet. The trigger is a button that opens a dialog (expanded state included) and each option carries the option role and its selected state on both hosts; the list around them is a listbox on the web only. On the web Enter, Space or an arrow key opens it, the arrow keys, Home and End move between options, Enter or Space chooses, Escape closes, and focus returns to the trigger; on Android the back button closes it. Props: `options` (`[{ value, label, disabled?, testID? }]`, `value` a string or number, unique), `value`, `onValueChange(value, option)`, `placeholder` (default "Select"), `title` (the sheet\'s heading; defaults to a text `label`), `emptyText` (default "No options"), `label`, `helper`, `error`, `disabled`, `accessibilityLabel`, `closeAccessibilityLabel`, `background` / `textColor` / `borderColor` / `radius` / `height` to override one field, `style` (the trigger), `containerStyle`, `testID`; any other prop lands on the trigger. Use it rather than a dropdown of your own; when the request needs options it cannot show (colour swatches, photos with two-line captions), put a small swatch or photo picker in a `<Popover>` at its button, or a long or full-screen option list in a `<Modal>` (an emoji choice is `<EmojiPicker>`) — never an absolutely positioned list, which the widget\'s container clips on both hosts. MULTIPLE: `<Select multiple label="Tags" values={tags} onValuesChange={setTags} options={tagOptions} />` — the default way to let the user pick SEVERAL of a known list. The same trigger and sheet; each option shows a tick box and toggles, the listbox is multiselectable on the web, and the sheet\'s Done button commits the choice through `onValuesChange(values, options)` (values in listed order, called only when they changed); closing the sheet any other way (backdrop, Escape, Android back) keeps the previous choice. Space toggles the focused option on the web. The trigger shows the picked labels as small chips while they fit on its one line, otherwise "N selected" (`selectedCountText(count)` to translate it), and `placeholder` while nothing is picked; `doneText` (default "Done") names the button. In this mode `value` / `onValueChange` are ignored. A handful of options always shown inline is a `<ChipGroup selection="multiple">` of `<Chip>`s instead. SEARCHABLE: a long list the user should type to narrow. `searchable` filters the given `options` by label as they type; `onSearch(query)` hands you the trimmed query when the sheet opens (\"\") and once typing pauses (`searchDebounceMs`, default 250), and you answer with new `options` and `loading` meanwhile (`optionsError` when it failed); or pass `options` as an async function of the query and Select fetches, keeping only the latest answer. The sheet then leads with the app\'s search field (Design > Components > Search fields), shows a spinner while loading, `noResultsText` (default \"No results\") when a query finds nothing and `loadErrorText` on failure, and its rows take the Lists row height and padding when the theme sets them. It remembers the label of every option it has shown; pass `selectedOptions` for a saved value the first page may not hold, or the trigger shows the placeholder. An option may carry `leading` (an `<Avatar>`, an icon) drawn before its label. Picking people, groups or a table\'s records is `<PeoplePicker>`, which does all of this for you.',
3468
3544
  rnComponent: null,
3469
3545
  docsUrl: null,
3470
3546
  },
@@ -3475,6 +3551,20 @@ const PRIMITIVES = [
3475
3551
  rnComponent: null,
3476
3552
  docsUrl: null,
3477
3553
  },
3554
+ {
3555
+ name: "PeoplePicker",
3556
+ description:
3557
+ 'Searchable picker of people, groups or records. `<PeoplePicker label="Assignee" value={assignee} onValueChange={setAssignee} />` — use it by default whenever the user picks a workspace user (an assignee, a reviewer, a recipient), a group (`source="groups"`), or a related record of a table they should search (`source="records" table={tableId}`, named by the table\'s identification column or `labelColumn`). It is a searchable `<Select>`: the same field, sheet, `label` / `helper` / `error` / `disabled`, and `multiple` with `values` / `onValuesChange`. Nothing loads until the sheet opens, each pause in typing searches on the server (never one page filtered in the widget), each user or group row leads with an `<Avatar>` (`avatarColumn` names a record\'s photo column; `showAvatars={false}` hides them), `excludeIds` leaves ids out (the current members), `groupId` limits users to one group, `filter` narrows records, and `pageSize` (default 50) sets the page. A saved value outside the first page is looked up once by id, so the trigger shows its name — `loadingText` meanwhile, `unknownText` (default "Unknown") when it is gone — never the raw id. Values are the row ids, exactly what a USER, USER_GROUP or RELATION column stores. Scopes: `directory.read:users` for users, `groups.read:*` for groups, the table\'s `datastore.read` scope for records. When the request needs what it cannot do — a people list that stays on screen with an action per row, a picker of users with presence dots or a role column — build your own with `useDirectory` / `useGroups` and the app\'s look.',
3558
+ rnComponent: null,
3559
+ docsUrl: null,
3560
+ },
3561
+ {
3562
+ name: "FilterBar",
3563
+ description:
3564
+ 'Search plus facet filters that lay themselves out for their own width. `<FilterBar query={query} onQueryChange={setQuery} facets={[{ key: "status", label: t("status", "Status"), options: [{ value: "open", label: t("open", "Open") }] }]} value={filters} onChange={setFilters} sort={{ options: sortOptions, value: sortBy, onChange: setSortBy }} />` — use it by default for a list\'s search and filters; when the filters need what it cannot do (a date-range facet, a price slider, a map-area filter, saved filter presets), build your own toolbar from `<SearchField>`, `<Select>`, `<Chip>` and `<Modal>` in the same shape. It measures its own unpadded box: medium and wide are ONE toolbar row (the search field taking the free width, one compact `<Select>` per facet whose placeholder reads "Status: Any", then the sort) with each applied facet beneath as a removable chip ("Status: Open") and a "Clear all" that resets the facets and the query; narrow, or a row whose dropdowns would not fit, is the search field plus ONE "Filters · 2" button opening the facets as full-width Selects in a `<Modal>` sheet, applied on Apply (Clear empties the sheet\'s facets and puts its sort back on the first option). The applied chips and "Clear all" show beneath at every width, so a narrow user can drop one filter without the sheet. A facet is `{ key, label, options, multiple?, searchable? }`; `value` is `{ [key]: value }`, a list for a `multiple` facet, and `onChange(next)` fires once per edit with a cleared facet\'s key removed. `onQueryChange(query)` gets the trimmed query once typing pauses (`queryDebounceMs`, default 250) and at once on clear or submit; the search field shows only with it. Both are controlled, so seed `query` and `value` from route params or `useWidgetRoute`. Its parts keep their own looks (Design > Components > Search fields, Inputs, Dropdown menus, Chips, Dialogs); it adds no theme scope. Translate the copy with `t()`: `searchPlaceholder`, `anyText` ("Any"), `filtersText` ("Filters"), `activeCountText` ("{count} active"), `clearAllText`, `applyText`, `clearText`, `activeFiltersText`, `removeText` ("Remove {label}"). Other props: `style` (the outer box), `testID`.',
3565
+ rnComponent: null,
3566
+ docsUrl: null,
3567
+ },
3478
3568
  {
3479
3569
  name: "RadioGroup",
3480
3570
  description:
@@ -3507,7 +3597,7 @@ const PRIMITIVES = [
3507
3597
  {
3508
3598
  name: "FileUpload",
3509
3599
  description:
3510
- 'Themed file chooser. `<FileUpload label="Attachments" accept="image/*,.pdf" multiple files={files} onFilesChange={setFiles} />` — the default way to let the user pick files to upload, whether they upload at once or with a form. It is built on `<FilePicker>`: a button (Design > Components > Buttons) inside the app\'s input field (Design > Components > Inputs), the chosen files listed under it with their name, size and a remove control, and the same `label` / `helper` / `error` / `disabled` as every form control. The button opens the system file picker on both hosts and is the only way in on a device; on the web a file can also be dropped onto the field. A picked or dropped file that `accept` rules out is not added and the field shows an error. It uploads nothing itself: call `upload(file)` from `useFilestoreUpload` in `onPick` (or when the form is submitted) and pass `uploading` while it runs; the field then shows a `<Spinner>` with "Uploading…" and locks (no percentage is reported). Props: `accept` (an HTML accept value: "image/*", ".pdf", "application/pdf"), `multiple` (default false: a new pick replaces the file), `files` + `onFilesChange(files)` to own the list (omit both and it keeps its own; pass `files={[]}` to list nothing), `onPick(file)` (an array when `multiple`) once per pick or drop with only the new files, `onRemove(file, index)`, `onReject(files, reason)` (reason "type" for a file `accept` rules out, "count" for the files beyond the first when not `multiple`), `uploading`, `label`, `helper`, `error` (a message, or `true`), `disabled`, `buttonLabel` (default "Choose file" / "Choose files"), `buttonVariant` ("primary" | "secondary" | "outlined", the default), `uploadingLabel`, `emptyLabel`, `dropLabel`, `rejectedMessage`, `singleFileMessage`, `unsupportedMessage`, `removeAccessibilityLabel`, `accessibilityLabel`, `style` (the field), `containerStyle`, `testID` (also tags `<testID>-button`, `<testID>-file-<i>` and `<testID>-remove-<i>`). Pass translated copy for every label. Use it rather than a `<FilePicker>` round your own button, file list or "Uploading…" text; when the request needs what it cannot do (a grid of photo thumbnails, a cover image replaced on tap), wrap `<FilePicker>` round your own trigger in the app\'s look and still upload with `useFilestoreUpload`.',
3600
+ 'Themed file chooser. `<FileUpload label="Attachments" accept="image/*,.pdf" multiple files={files} onFilesChange={setFiles} />` — the default way to let the user pick files to upload, whether they upload at once or with a form. It is built on `<FilePicker>`: a button (Design > Components > Buttons) inside the app\'s input field (Design > Components > Inputs), the chosen files listed under it with their name, size and a remove control, and the same `label` / `required` / `helper` / `error` / `disabled` as every form control. The button opens the system file picker on both hosts and is the only way in on a device; on the web a file can also be dropped onto the field. A picked or dropped file that `accept` rules out is not added and the field shows an error. It uploads nothing itself: call `upload(file)` from `useFilestoreUpload` in `onPick` (or when the form is submitted) and pass `uploading` while it runs; the field then shows a `<Spinner>` with "Uploading…" and locks (no percentage is reported). Props: `accept` (an HTML accept value: "image/*", ".pdf", "application/pdf"), `multiple` (default false: a new pick replaces the file), `files` + `onFilesChange(files)` to own the list (omit both and it keeps its own; pass `files={[]}` to list nothing), `onPick(file)` (an array when `multiple`) once per pick or drop with only the new files, `onRemove(file, index)`, `onReject(files, reason)` (reason "type" for a file `accept` rules out, "count" for the files beyond the first when not `multiple`), `uploading`, `label`, `required` (a mark after the label and "Required" in the picker\'s spoken name; it does not validate), `helper`, `error` (a message, or `true`), `disabled`, `buttonLabel` (default "Choose file" / "Choose files"), `buttonVariant` ("primary" | "secondary" | "outlined", the default), `uploadingLabel`, `emptyLabel`, `dropLabel`, `rejectedMessage`, `singleFileMessage`, `unsupportedMessage`, `removeAccessibilityLabel`, `accessibilityLabel`, `style` (the field), `containerStyle`, `testID` (also tags `<testID>-button`, `<testID>-file-<i>` and `<testID>-remove-<i>`). Pass translated copy for every label. Use it rather than a `<FilePicker>` round your own button, file list or "Uploading…" text; when the request needs what it cannot do (a grid of photo thumbnails, a cover image replaced on tap), wrap `<FilePicker>` round your own trigger in the app\'s look and still upload with `useFilestoreUpload`.',
3511
3601
  rnComponent: null,
3512
3602
  docsUrl: null,
3513
3603
  },
@@ -3533,6 +3623,21 @@ const PRIMITIVES = [
3533
3623
  rnComponent: null,
3534
3624
  docsUrl: null,
3535
3625
  },
3626
+ // Drag-and-drop: rows reordered and cards moved between columns.
3627
+ {
3628
+ name: "SortableList",
3629
+ description:
3630
+ 'A list the user reorders by dragging. `<SortableList items={steps} onReorder={(next) => setSteps(next)} renderItem={({ item }) => <Text>{item.title}</Text>} />` — use it by default for a list the user reorders (a ranking, a playlist, the steps of a routine, priorities); when the request needs what it cannot do (free-form positioning on a canvas, a drag between two widgets, a drop onto a calendar slot, dragging several rows at once), build your own with `Animated` + `PanResponder` from react-native. Each row is a `<ListRow>` (the theme\'s `list` scope; `variant="card"` draws card rows) led by a grip handle that owns the drag on both hosts: the row lifts and follows the finger, the other rows step aside, the gap where it will land shows in the soft primary, and a release settles it there, or springs it back and calls nothing when nothing changed. A tap or Enter on the handle opens its move options (Move up, Move down, Move to top, Move to bottom), so dragging is never the only way; on the web the arrow keys move a focused handle, a screen reader is offered the same moves as actions, and every move is announced. `onReorder(nextItems, { from, to, item })` runs once per real change: show `nextItems` at once and save the new order yourself. Props: `items`, `onReorder`, `renderItem({ item, index, dragging })` (a row\'s content; with none the item\'s title shows), `keyExtractor` (default `item.id`), `getItemLabel` (the name a move announces; default its title, name or label), `canDrag` (a boolean or `(item) => boolean`; a row it refuses has no handle; it is read on every render, so pass `useCanWrite`\'s flag as it is), `variant` "plain" (default) | "card", `labels` (translated words: `handle` "Move {item}", `moveUp`, `moveDown`, `moveToTop`, `moveToBottom`, `moved` "{item} moved to position {position} of {total}", `hint`), `style`, `testID`, `accessibilityLabel`. In a list taller than the screen a drag reaches the rows in view; the move options reach any place.',
3631
+ rnComponent: null,
3632
+ docsUrl: null,
3633
+ },
3634
+ {
3635
+ name: "Board",
3636
+ description:
3637
+ 'Cards the user moves between columns: a kanban, a pipeline, a status board. `<Board columns={[{ key: "todo", title: t("todo", "To do") }, { key: "done", title: t("done", "Done") }]} items={rows} getColumn={(row) => row[props.statusField]} onMove={(row, { toColumn }) => move(row, toColumn)} renderCard={({ item }) => <Text>{item[props.titleField]}</Text>} canDrag={canWrite} />` — use it by default for cards the user moves between columns; when the request needs what it cannot do (free-form positioning on a canvas, a drag between two widgets, a drop onto a calendar slot, swimlanes in two directions), build your own with `Animated` + `PanResponder` from react-native. Each card is a `<Card>` (the theme\'s `card` scope) with a grip handle. The board hit-tests the drop itself, highlights the one column the card is over in the soft primary with a line where it will land, and on a narrow box lays fixed-width columns in a sideways scroll that scrolls by itself while a card is held at its edge. A tap or Enter on the handle opens its move options (Move up, Move down, Move to <column>), so dragging is never the only way; on the web the arrow keys move a focused card (left and right to the end of the next column), a screen reader is offered the same moves as actions, and every move is announced. `onMove(item, { fromColumn, toColumn, index })` runs once per real change (`index` is its place in the target column); a drag released over no column, or where it started, springs back and calls nothing. The board draws `items` as you pass them, so show a moved card in its new column at once: keep an optimistic map until your write lands and you refetch, and on failure drop it with a toast so the card returns. Props: `columns` (`[{ key, title }]`), `items`, `getColumn(item)` (its column key), `onMove`, `renderCard({ item, index, column, dragging })` (a card\'s content), `keyExtractor`, `getItemLabel`, `canDrag` (a boolean or per item, read on every render), `reorderInColumn` (default true; false when cards keep no order of their own, and a drop in its own column then changes nothing), `columnWidth` (the narrow width of a column, default 280), `labels` (as SortableList, plus `moveToColumn` "Move to {column}" and `movedToColumn` "{item} moved to {column}, position {position}"), `style`, `testID`, `accessibilityLabel`.',
3638
+ rnComponent: null,
3639
+ docsUrl: null,
3640
+ },
3536
3641
  ];
3537
3642
 
3538
3643
  const CATEGORIES = [
@@ -4540,7 +4645,7 @@ const VETTED_IMPORTS = [
4540
4645
  platforms: ["web", "native"],
4541
4646
  category: "drawing",
4542
4647
  description:
4543
- "SVG path generators — `line`, `area`, `arc`, `pie`, `curve*`. Each returns a path string you hand to the vetted react-native-svg's `<Path d={…} />`, so a bespoke line/area/donut chart renders identically in the Player and the Expo export from ONE source file. Pure JS.",
4648
+ "SVG path generators — `line`, `area`, `arc`, `pie`, `curve*`. Each returns a path string you hand to the vetted react-native-svg's `<Path d={…} />`, so a chart `<Chart>` does not draw renders identically in the Player and the Expo export from ONE source file. Pure JS.",
4544
4649
  },
4545
4650
  {
4546
4651
  specifier: "d3-array",
@@ -6210,7 +6315,14 @@ const CONTRACT = deepFreeze({
6210
6315
  // `<Menu>`, `<Popover>`, `<Tooltip>` (popover scope gains radius,
6211
6316
  // shadow), `<IconButton>`, Button ghost/link/size/busy/selected and
6212
6317
  // trigger pass-through, `<FileImage>`, `Avatar fileId`, `<RadioGroup>`.
6213
- version: "1.200.0",
6318
+ // 1.201.0: additive (sc-12070, sc-12071, sc-12072) — new `<Grid>`,
6319
+ // `<Chart>` + `chart` theme scope + `chartColors`, `<PeoplePicker>`,
6320
+ // searchable `<Select>`; `useGroups` takes `ids`.
6321
+ // 1.202.0: additive (sc-12073, sc-12074, sc-12075, sc-12076) — new
6322
+ // `<DataTable>`, `<FilterBar>`, `<Form>` / `<FieldRow>` / `<FormSection>`
6323
+ // / `<FormActions>` / `<FormField>`, `<SortableList>`, `<Board>`;
6324
+ // `required` on TextField, Select, RadioGroup, PeoplePicker, FileUpload.
6325
+ version: "1.202.0",
6214
6326
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6215
6327
  hooks: HOOKS,
6216
6328
  primitives: PRIMITIVES,