@colixsystems/widget-sdk 0.246.0 → 0.247.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,131 @@
1
+ // The colours and corners `<Chart>` paints with, resolved from the theme's
2
+ // `chart` scope and falling back to the theme's own colours. Platform-free,
3
+ // so both hosts draw one chart from the same values.
4
+
5
+ import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
6
+ import { CONTRACT, isHexColor, mixHex } from "./contract.js";
7
+ import { normaliseThemeComponents } from "./theme-components.js";
8
+
9
+ export const CHART_SERIES_TOKENS = Object.freeze(["series1", "series2", "series3", "series4", "series5", "series6"]);
10
+
11
+ // Distinct hues first, so two series rarely share a family.
12
+ const PALETTE_ROLES = Object.freeze(["primary", "warning", "secondary", "danger", "info", "success"]);
13
+ // How far a seventh-and-later series is lightened toward the surface.
14
+ const REPEAT_TINT = 0.45;
15
+
16
+ function themeColors(theme) {
17
+ return { ...DEFAULT_THEME_TOKENS.colors, ...((theme && theme.colors) || {}) };
18
+ }
19
+
20
+ /** The six series colours an unthemed chart uses: the theme's own, deduplicated. */
21
+ function defaultPalette(colors) {
22
+ const seen = new Set();
23
+ const palette = [];
24
+ for (const role of PALETTE_ROLES) {
25
+ const colour = colors[role];
26
+ const key = typeof colour === "string" ? colour.toLowerCase() : "";
27
+ if (!key || seen.has(key)) continue;
28
+ seen.add(key);
29
+ palette.push(colour);
30
+ }
31
+ for (let i = 0; palette.length < CHART_SERIES_TOKENS.length; i += 1) {
32
+ palette.push(mixHex(colors.primary, i % 2 ? colors.surface : colors.onSurface, 0.3 + 0.15 * Math.floor(i / 2)));
33
+ }
34
+ return palette;
35
+ }
36
+
37
+ /**
38
+ * Resolve one chart's look: the `chart` scope first, then the theme.
39
+ *
40
+ * @param {object} theme — the resolved workspace theme (`useTheme()` shape).
41
+ * @returns {{ palette: string[], axisColor: string, gridColor: string, barRadius: number, surface: string, fontFamily: string, fontSize: number }}
42
+ */
43
+ export function resolveChartTokens(theme) {
44
+ const base = theme && typeof theme === "object" ? theme : DEFAULT_THEME_TOKENS;
45
+ const colors = themeColors(base);
46
+ const radii = base.radii || DEFAULT_THEME_TOKENS.radii;
47
+ const typography = base.typography || DEFAULT_THEME_TOKENS.typography;
48
+ const sizes = typography.sizes || DEFAULT_THEME_TOKENS.typography.sizes;
49
+ const scope = normaliseThemeComponents(base.components).chart || {};
50
+ const fallback = defaultPalette(colors);
51
+ return {
52
+ palette: CHART_SERIES_TOKENS.map((token, i) => scope[token] ?? fallback[i]),
53
+ axisColor: scope.axisColor ?? colors.onSurfaceMuted,
54
+ gridColor: scope.gridColor ?? colors.border,
55
+ barRadius: scope.barRadius ?? radii.sm,
56
+ surface: colors.surface,
57
+ fontFamily: typography.fontFamily || DEFAULT_THEME_TOKENS.typography.fontFamily,
58
+ fontSize: sizes.xs || DEFAULT_THEME_TOKENS.typography.sizes.xs,
59
+ };
60
+ }
61
+
62
+ const RGB = /^rgba?\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*(?:,\s*[\d.]+%?\s*)?\)$/i;
63
+
64
+ // A `#rrggbb` for a hex or rgb() colour, so it can be mixed; else null.
65
+ function mixable(colour) {
66
+ if (isHexColor(colour)) return colour;
67
+ const rgb = typeof colour === "string" ? RGB.exec(colour.trim()) : null;
68
+ if (!rgb) return null;
69
+ return `#${rgb.slice(1, 4).map((n) => Math.min(255, Number(n)).toString(16).padStart(2, "0")).join("")}`;
70
+ }
71
+
72
+ /**
73
+ * How series (or donut slice) `index` is painted: its palette colour, a
74
+ * seventh and later one lighter. A colour that cannot be mixed (a named or
75
+ * hsl() colour) repeats at a lower opacity instead.
76
+ *
77
+ * @returns {{ color: string, opacity: number }}
78
+ */
79
+ export function chartSeriesPaint(tokens, index) {
80
+ const palette = tokens.palette;
81
+ const colour = palette[index % palette.length];
82
+ const round = Math.floor(index / palette.length);
83
+ if (round === 0) return { color: colour, opacity: 1 };
84
+ const tint = Math.min(0.8, REPEAT_TINT * round);
85
+ const hex = mixable(colour);
86
+ return hex ? { color: mixHex(hex, tokens.surface, tint), opacity: 1 } : { color: colour, opacity: 1 - tint };
87
+ }
88
+
89
+ /** The colour of series (or donut slice) `index`; see `chartSeriesPaint`. */
90
+ export function chartSeriesColor(tokens, index) {
91
+ return chartSeriesPaint(tokens, index).color;
92
+ }
93
+
94
+ /**
95
+ * The colours `<Chart>` paints with, for a chart a widget draws itself (a
96
+ * scatter plot, a timeline): `series(i)` and `seriesOpacity(i)` for the i-th
97
+ * series' marks, `axis` for labels and the axis line, `grid` for gridlines.
98
+ * Pass it `useTheme()`, so a hand-drawn chart follows Design > Components >
99
+ * Charts exactly as `<Chart>` does.
100
+ *
101
+ * @param {object} theme — the resolved workspace theme (`useTheme()` shape).
102
+ */
103
+ export function chartColors(theme) {
104
+ const tokens = resolveChartTokens(theme);
105
+ return {
106
+ palette: tokens.palette.slice(),
107
+ axis: tokens.axisColor,
108
+ grid: tokens.gridColor,
109
+ series: (index) => chartSeriesPaint(tokens, index).color,
110
+ seriesOpacity: (index) => chartSeriesPaint(tokens, index).opacity,
111
+ };
112
+ }
113
+
114
+ /**
115
+ * What each `chart` token resolves to while unset, keyed by token name, so
116
+ * the Studio previews the colour each control would replace.
117
+ *
118
+ * @param {object} theme — the resolved workspace theme (`useTheme()` shape).
119
+ */
120
+ export function chartScopeDefaults(theme) {
121
+ const base = theme && typeof theme === "object" ? theme : DEFAULT_THEME_TOKENS;
122
+ const components = { ...(base.components || {}) };
123
+ delete components.chart;
124
+ const unset = resolveChartTokens({ ...base, components });
125
+ const out = {};
126
+ for (const token of Object.keys(CONTRACT.themeComponents.chart.tokens)) {
127
+ const series = CHART_SERIES_TOKENS.indexOf(token);
128
+ out[token] = series === -1 ? unset[token] : unset.palette[series];
129
+ }
130
+ return out;
131
+ }
@@ -0,0 +1,262 @@
1
+ // The ONE implementation behind the `<Chart>` SDK primitive. The platform
2
+ // bindings (`chart.js` / `chart.native.js`) differ only in where the
3
+ // primitives and the SVG elements come from, so the hosts cannot drift.
4
+ //
5
+ // Every dashboard widget drew its own chart, each with its own colours, axes
6
+ // and empty state. This one reads the theme's `chart` scope.
7
+
8
+ import React from "react";
9
+ import { useHostLocale, useHostTheme, useHostSharedText } from "./hooks.js";
10
+ import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
11
+ import { CONTRACT } from "./contract.js";
12
+ import { textRoleStyle } from "./text-roles.js";
13
+ import { resolveChartTokens, chartSeriesPaint } from "./chart-tokens.js";
14
+ import {
15
+ normaliseChartType,
16
+ chartHeight,
17
+ normaliseChartData,
18
+ isChartEmpty,
19
+ cartesianLayout,
20
+ donutLayout,
21
+ barPath,
22
+ chartSummary,
23
+ formatChartValue,
24
+ } from "./chart-logic.js";
25
+
26
+ const h = React.createElement;
27
+ const NO_RESULTS_FALLBACK = CONTRACT.sharedTranslationKeys.no_results.en;
28
+ const AREA_OPACITY = 0.18;
29
+ const LINE_WIDTH = 2;
30
+ const DOT_RADIUS = 3;
31
+ const SWATCH_SIZE = 10;
32
+ const HIDDEN = { "aria-hidden": true, importantForAccessibility: "no-hide-descendants" };
33
+
34
+ /**
35
+ * Bind the shared chart to one platform's primitives.
36
+ *
37
+ * @param {{View: any, Text: any, Svg: any, G: any, Path: any, Line: any, Circle: any, SvgText: any, Skeleton: any, EmptyState: any}} rn
38
+ */
39
+ export function makeChart(rn) {
40
+ const { View, Text, Svg, G, Path, Line, Circle, SvgText, Skeleton, EmptyState } = rn;
41
+
42
+ function Legend({ items, theme, testID }) {
43
+ const typography = theme.typography || DEFAULT_THEME_TOKENS.typography;
44
+ const colors = theme.colors || DEFAULT_THEME_TOKENS.colors;
45
+ const spacing = theme.spacing || DEFAULT_THEME_TOKENS.spacing;
46
+ const label = textRoleStyle("caption", typography, colors);
47
+ return h(
48
+ View,
49
+ {
50
+ style: { flexDirection: "row", flexWrap: "wrap", columnGap: spacing.md, rowGap: spacing.xs, marginTop: spacing.sm },
51
+ testID: testID ? `${testID}-legend` : undefined,
52
+ },
53
+ items.map((item) =>
54
+ h(
55
+ View,
56
+ { key: item.key, style: { flexDirection: "row", alignItems: "center", gap: spacing.xs, minWidth: 0 } },
57
+ h(View, {
58
+ style: {
59
+ width: SWATCH_SIZE,
60
+ height: SWATCH_SIZE,
61
+ borderRadius: SWATCH_SIZE / 2,
62
+ backgroundColor: item.paint.color,
63
+ opacity: item.paint.opacity,
64
+ },
65
+ }),
66
+ h(Text, { style: label, numberOfLines: 1 }, item.text),
67
+ ),
68
+ ),
69
+ );
70
+ }
71
+
72
+ function axisText(label, i, tokens, prefix) {
73
+ return h(
74
+ SvgText,
75
+ {
76
+ key: `${prefix}-${i}`,
77
+ x: label.x,
78
+ y: label.y,
79
+ fill: tokens.axisColor,
80
+ fontSize: tokens.fontSize,
81
+ fontFamily: tokens.fontFamily,
82
+ textAnchor: label.anchor,
83
+ },
84
+ label.text,
85
+ );
86
+ }
87
+
88
+ function CartesianMarks({ kind, width, height, model, tokens, valueFormat, locale, paintOf }) {
89
+ const layout = cartesianLayout({ type: kind, width, height, model, fontSize: tokens.fontSize, valueFormat, locale });
90
+ const grid = layout.gridLines.map((line, i) =>
91
+ line.y === undefined
92
+ ? h(Line, { key: `grid-${i}`, x1: line.x, x2: line.x, y1: line.y1, y2: line.y2, stroke: tokens.gridColor, strokeWidth: 1 })
93
+ : h(Line, { key: `grid-${i}`, x1: line.x1, x2: line.x2, y1: line.y, y2: line.y, stroke: tokens.gridColor, strokeWidth: 1 }),
94
+ );
95
+ const base = layout.baseline;
96
+ const axis = !base
97
+ ? null
98
+ : base.y === undefined
99
+ ? h(Line, { x1: base.x, x2: base.x, y1: base.y1, y2: base.y2, stroke: tokens.axisColor, strokeWidth: 1 })
100
+ : h(Line, { x1: base.x1, x2: base.x2, y1: base.y, y2: base.y, stroke: tokens.axisColor, strokeWidth: 1 });
101
+ return h(
102
+ G,
103
+ null,
104
+ kind === "sparkline" ? null : grid,
105
+ layout.areas.map((area) =>
106
+ h(Path, {
107
+ key: `area-${area.seriesIndex}`,
108
+ d: area.d,
109
+ fill: paintOf(area.seriesIndex).color,
110
+ fillOpacity: AREA_OPACITY * paintOf(area.seriesIndex).opacity,
111
+ }),
112
+ ),
113
+ layout.bars.map((bar) =>
114
+ h(Path, {
115
+ key: `bar-${bar.seriesIndex}-${bar.index}`,
116
+ d: barPath(bar.rect, tokens.barRadius, bar.edge),
117
+ fill: paintOf(bar.seriesIndex).color,
118
+ fillOpacity: paintOf(bar.seriesIndex).opacity,
119
+ }),
120
+ ),
121
+ axis,
122
+ layout.lines.map((line) =>
123
+ h(Path, {
124
+ key: `line-${line.seriesIndex}`,
125
+ d: line.d,
126
+ fill: "none",
127
+ stroke: paintOf(line.seriesIndex).color,
128
+ strokeOpacity: paintOf(line.seriesIndex).opacity,
129
+ strokeWidth: LINE_WIDTH,
130
+ strokeLinejoin: "round",
131
+ strokeLinecap: "round",
132
+ }),
133
+ ),
134
+ layout.dots.map((dot, i) =>
135
+ h(Circle, {
136
+ key: `dot-${i}`,
137
+ cx: dot.cx,
138
+ cy: dot.cy,
139
+ r: DOT_RADIUS,
140
+ fill: paintOf(dot.seriesIndex).color,
141
+ fillOpacity: paintOf(dot.seriesIndex).opacity,
142
+ }),
143
+ ),
144
+ layout.tickLabels.map((label, i) => axisText(label, i, tokens, "tick")),
145
+ layout.categoryLabels.map((label, i) => axisText(label, i, tokens, "label")),
146
+ );
147
+ }
148
+
149
+ function DonutMarks({ width, height, model, tokens, valueFormat, locale, paintOf }) {
150
+ const layout = donutLayout({ width, height, model });
151
+ return h(
152
+ G,
153
+ null,
154
+ layout.slices.map((slice) =>
155
+ h(Path, {
156
+ key: `slice-${slice.index}`,
157
+ d: slice.d,
158
+ fill: paintOf(slice.index).color,
159
+ fillOpacity: paintOf(slice.index).opacity,
160
+ fillRule: "evenodd",
161
+ }),
162
+ ),
163
+ h(
164
+ SvgText,
165
+ {
166
+ x: layout.cx,
167
+ y: layout.cy + tokens.fontSize / 3,
168
+ fill: tokens.axisColor,
169
+ fontSize: tokens.fontSize + 2,
170
+ fontWeight: "600",
171
+ fontFamily: tokens.fontFamily,
172
+ textAnchor: "middle",
173
+ },
174
+ formatChartValue(layout.total, valueFormat, locale),
175
+ ),
176
+ );
177
+ }
178
+
179
+ function Chart(props) {
180
+ const {
181
+ type,
182
+ data,
183
+ series,
184
+ valueFormat,
185
+ height,
186
+ loading,
187
+ emptyTitle,
188
+ emptyMessage,
189
+ accessibilityLabel,
190
+ moreText,
191
+ theme: explicitTheme,
192
+ style,
193
+ testID,
194
+ } = props;
195
+ const hostTheme = useHostTheme();
196
+ // A host drawing outside a widget context passes its own theme.
197
+ const theme = explicitTheme || hostTheme || DEFAULT_THEME_TOKENS;
198
+ const noResults = useHostSharedText("no_results", NO_RESULTS_FALLBACK);
199
+ const locale = useHostLocale();
200
+ const [width, setWidth] = React.useState(0);
201
+ const kind = normaliseChartType(type);
202
+ const plotHeight = chartHeight(kind, height);
203
+ const model = React.useMemo(() => normaliseChartData(data, series), [data, series]);
204
+
205
+ if (loading) {
206
+ return h(Skeleton, { shape: "block", height: plotHeight, theme: explicitTheme, style, testID });
207
+ }
208
+ if (isChartEmpty(model, kind)) {
209
+ return h(EmptyState, { title: emptyTitle || noResults, message: emptyMessage, style, testID });
210
+ }
211
+
212
+ const tokens = resolveChartTokens(theme);
213
+ const paintOf = (index) => {
214
+ const own = kind === "donut" ? undefined : model.series[index] && model.series[index].color;
215
+ return own ? { color: own, opacity: 1 } : chartSeriesPaint(tokens, index);
216
+ };
217
+ const onLayout = (event) => {
218
+ const next = Math.floor(event.nativeEvent.layout.width);
219
+ if (next !== width) setWidth(next);
220
+ };
221
+ const marks =
222
+ width <= 0
223
+ ? null
224
+ : h(
225
+ Svg,
226
+ { width, height: plotHeight },
227
+ kind === "donut"
228
+ ? h(DonutMarks, { width, height: plotHeight, model, tokens, valueFormat, locale, paintOf })
229
+ : h(CartesianMarks, { kind, width, height: plotHeight, model, tokens, valueFormat, locale, paintOf }),
230
+ );
231
+ const legendItems =
232
+ kind === "donut"
233
+ ? donutLayout({ width: 1, height: 1, model }).slices.map((slice) => ({
234
+ key: `slice-${slice.index}`,
235
+ paint: paintOf(slice.index),
236
+ text: `${model.labels[slice.index]} ${formatChartValue(model.series[0].values[slice.index], valueFormat, locale)}`,
237
+ }))
238
+ : model.series.length > 1 && kind !== "sparkline"
239
+ ? model.series.map((s, i) => ({ key: s.key, paint: paintOf(i), text: s.label }))
240
+ : null;
241
+
242
+ return h(
243
+ View,
244
+ {
245
+ accessible: true,
246
+ accessibilityRole: "image",
247
+ accessibilityLabel: chartSummary({ type: kind, model, valueFormat, lead: accessibilityLabel, locale, moreText }),
248
+ style: [{ width: "100%" }, style],
249
+ testID,
250
+ },
251
+ h(
252
+ View,
253
+ { ...HIDDEN, onLayout, style: { width: "100%", height: plotHeight }, testID: testID ? `${testID}-plot` : undefined },
254
+ marks,
255
+ ),
256
+ legendItems ? h(Legend, { items: legendItems, theme, testID }) : null,
257
+ );
258
+ }
259
+ Chart.displayName = "Chart";
260
+
261
+ return { Chart };
262
+ }
package/dist/chart.js ADDED
@@ -0,0 +1,21 @@
1
+ // `<Chart>` (web). Binds ./chart-view.js to react-native-web and the host's
2
+ // react-native-svg; see ./primitives.js for why not the bare specifier.
3
+
4
+ import * as ReactNative from "react-native-web";
5
+ import Svg, { G, Path, Line, Circle, Text as SvgText } from "react-native-svg";
6
+ import { Skeleton } from "./loading.js";
7
+ import { EmptyState } from "./list.js";
8
+ import { makeChart } from "./chart-view.js";
9
+
10
+ export const { Chart } = makeChart({
11
+ View: ReactNative.View,
12
+ Text: ReactNative.Text,
13
+ Svg,
14
+ G,
15
+ Path,
16
+ Line,
17
+ Circle,
18
+ SvgText,
19
+ Skeleton,
20
+ EmptyState,
21
+ });
@@ -0,0 +1,10 @@
1
+ // `<Chart>` (native). Binds ./chart-view.js to React Native and
2
+ // react-native-svg; only the imports differ from ./chart.js.
3
+
4
+ import { View, Text } from "react-native";
5
+ import Svg, { G, Path, Line, Circle, Text as SvgText } from "react-native-svg";
6
+ import { Skeleton } from "./loading.native.js";
7
+ import { EmptyState } from "./list.native.js";
8
+ import { makeChart } from "./chart-view.js";
9
+
10
+ export const { Chart } = makeChart({ View, Text, Svg, G, Path, Line, Circle, SvgText, Skeleton, EmptyState });
package/dist/contract.cjs 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",
@@ -3377,6 +3397,13 @@ const PRIMITIVES = [
3377
3397
  rnComponent: null,
3378
3398
  docsUrl: null,
3379
3399
  },
3400
+ {
3401
+ name: "Chart",
3402
+ description:
3403
+ '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.',
3404
+ rnComponent: null,
3405
+ docsUrl: null,
3406
+ },
3380
3407
  {
3381
3408
  name: "Divider",
3382
3409
  description:
@@ -3384,6 +3411,13 @@ const PRIMITIVES = [
3384
3411
  rnComponent: null,
3385
3412
  docsUrl: null,
3386
3413
  },
3414
+ {
3415
+ name: "Grid",
3416
+ description:
3417
+ '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.',
3418
+ rnComponent: null,
3419
+ docsUrl: null,
3420
+ },
3387
3421
  {
3388
3422
  name: "Banner",
3389
3423
  description:
@@ -3464,7 +3498,7 @@ const PRIMITIVES = [
3464
3498
  {
3465
3499
  name: "Select",
3466
3500
  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.',
3501
+ '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
3502
  rnComponent: null,
3469
3503
  docsUrl: null,
3470
3504
  },
@@ -3475,6 +3509,13 @@ const PRIMITIVES = [
3475
3509
  rnComponent: null,
3476
3510
  docsUrl: null,
3477
3511
  },
3512
+ {
3513
+ name: "PeoplePicker",
3514
+ description:
3515
+ '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.',
3516
+ rnComponent: null,
3517
+ docsUrl: null,
3518
+ },
3478
3519
  {
3479
3520
  name: "RadioGroup",
3480
3521
  description:
@@ -4544,7 +4585,7 @@ const VETTED_IMPORTS = [
4544
4585
  platforms: ["web", "native"],
4545
4586
  category: "drawing",
4546
4587
  description:
4547
- "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.",
4588
+ "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.",
4548
4589
  },
4549
4590
  {
4550
4591
  specifier: "d3-array",
@@ -6214,7 +6255,10 @@ const CONTRACT = deepFreeze({
6214
6255
  // `<Menu>`, `<Popover>`, `<Tooltip>` (popover scope gains radius,
6215
6256
  // shadow), `<IconButton>`, Button ghost/link/size/busy/selected and
6216
6257
  // trigger pass-through, `<FileImage>`, `Avatar fileId`, `<RadioGroup>`.
6217
- version: "1.200.0",
6258
+ // 1.201.0: additive (sc-12070, sc-12071, sc-12072) — new `<Grid>`,
6259
+ // `<Chart>` + `chart` theme scope + `chartColors`, `<PeoplePicker>`,
6260
+ // searchable `<Select>`; `useGroups` takes `ids`.
6261
+ version: "1.201.0",
6218
6262
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6219
6263
  hooks: HOOKS,
6220
6264
  primitives: PRIMITIVES,