@zestl/zvolv-widgets 0.0.0-stage → 0.0.1
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/README.md +821 -2
- package/SKILL.md +251 -0
- package/dist/index.cjs +9003 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +3070 -0
- package/dist/index.d.ts +3070 -0
- package/dist/index.js +8785 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -4
package/SKILL.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: zvolv-widgets
|
|
3
|
+
description: Build Zvolv dashboards with @zestl/zvolv-widgets. Use whenever designing or coding a dashboard, KPI, chart, table, list, timeline, calendar or any Zvolv widget — including adding search, column filters, sorting or widget-level filters. The library owns data fetching, query generation, filtering and sorting; you own all visual design, configuration and the UI of every control.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# @zestl/zvolv-widgets — contract
|
|
7
|
+
|
|
8
|
+
**The library owns the data. You own the UI.** You write widget configuration,
|
|
9
|
+
design tokens, render props and control slots. You never write data logic.
|
|
10
|
+
|
|
11
|
+
## Hard rules — read first
|
|
12
|
+
|
|
13
|
+
1. **Never** write a raw API call, build an Elasticsearch query, recompute a pivot, or filter/search/sort rows yourself. Pass `search` / `columnFilters` / `sort` / `widgetFilters` (or turn on the built-in controls). The library applies them on the server or on loaded rows, whichever is correct.
|
|
14
|
+
2. **Names must match.** `series[].argumentField` = `pivot.rows[].title`. `series[].valueField` = `pivot.summary[].value` (or a `derive()` title). `WidgetFilter.field`, `SortState.key` and `ColumnFilters` keys are each a `header` in `dataSource.fields`. A mismatch shows an empty widget with no error.
|
|
15
|
+
3. **Declare every field** you plot, group, filter, search or sort on in `dataSource.fields`.
|
|
16
|
+
4. **`taskSource()` never has a `formId`.** Never invent a `formId` or field name. Ask for it, or read it from an existing widget's stored `DataSource`.
|
|
17
|
+
5. **`unconfigured` ≠ `empty`.** Never fall back to demo data.
|
|
18
|
+
6. **No colour/font/shadow literals** in templates or slots. Use `tokens` / `useTokens()`.
|
|
19
|
+
7. Pass stored `dataSource` / `configurations` through **untouched**. Give widgets a sized parent.
|
|
20
|
+
8. Never call `getZvolvClient()` at module scope. The runtime's `client` is a getter.
|
|
21
|
+
|
|
22
|
+
## Setup
|
|
23
|
+
|
|
24
|
+
```tsx
|
|
25
|
+
import { ZvolvProvider, ZvolvThemeProvider, TemplateProvider, createZvolvRuntime, createMockRuntime } from "@zestl/zvolv-widgets";
|
|
26
|
+
|
|
27
|
+
// Real data: an SDK client after workspace.init(domain) + login. Pass a getter.
|
|
28
|
+
const runtime = createZvolvRuntime({ client: () => zvolvClient }); // or createMockRuntime() for previews
|
|
29
|
+
|
|
30
|
+
<ZvolvProvider runtime={runtime}>
|
|
31
|
+
<ZvolvThemeProvider tokens={myDesign}>
|
|
32
|
+
<TemplateProvider templates={myTemplates}> {/* optional */}
|
|
33
|
+
<Dashboard />
|
|
34
|
+
</TemplateProvider>
|
|
35
|
+
</ZvolvThemeProvider>
|
|
36
|
+
</ZvolvProvider>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Props every data widget takes
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
<AnyWidget
|
|
43
|
+
dataSource={ds} // WHAT is queried — formSource()/taskSource(), or stored value untouched
|
|
44
|
+
configurations={cfg} // HOW it looks — object or JSON string
|
|
45
|
+
dashFilters={dashState} // dashboard filter state (optional)
|
|
46
|
+
cdateFilters={dateFilters} // dashboard date filters (optional)
|
|
47
|
+
title="…" description="…" // optional chrome
|
|
48
|
+
render={(args) => …} // optional: own the visuals (args per family below)
|
|
49
|
+
/>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
By type string: `<ZvolvWidget type="BAR_CHART" {...props} />`.
|
|
53
|
+
|
|
54
|
+
## Search, column filters, sort, widget filters
|
|
55
|
+
|
|
56
|
+
| Control | Widgets | Turn on (any one) | Value shape |
|
|
57
|
+
| --- | --- | --- | --- |
|
|
58
|
+
| Search | all data widgets | `showSearch` · `config.showSearchbar` · pass `renderSearch` | `string` |
|
|
59
|
+
| Widget filter | all data widgets | `showWidgetFilter` · `config.showWidgetFilter` · pass `renderWidgetFilter` | `WidgetFilter[]` |
|
|
60
|
+
| Column filter | tables | `showColumnFilter` · `config.showColumnFilter` · pass `renderColumnFilter` | `Record<columnKey, any[]>`, where `""` means blanks |
|
|
61
|
+
| Sort | tables | on by default. Off: `enableSort={false}` | `{ key, direction: "asc" \| "desc" } \| null` |
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
interface WidgetFilter {
|
|
65
|
+
field: string; // a dataSource.fields header
|
|
66
|
+
operator: "IN" | "NOT_IN" | "EQUALS" | "CONTAINS" | "STARTS_WITH"
|
|
67
|
+
| "GREATER_THAN" | "LESS_THAN" | "BETWEEN" | "EMPTY" | "NOT_EMPTY" | "DATE_IS";
|
|
68
|
+
value?: any; // IN/NOT_IN: array · BETWEEN: [from, to] · DATE_IS: "TODAY" | "YESTERDAY" |
|
|
69
|
+
// "LAST_SEVEN_DAYS" | "LAST_THIRTY_DAYS" | "THIS_WEEK" | "THIS_MONTH" | "THIS_YEAR"
|
|
70
|
+
// EMPTY/NOT_EMPTY: omit · user fields: user ids, not names
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**State.** Choose one approach per control:
|
|
75
|
+
- Uncontrolled: `defaultSearch`, `defaultWidgetFilters`, `defaultColumnFilters`, `defaultSort`.
|
|
76
|
+
- Controlled: pass the value **and** its change handler: `search` + `onSearchChange`, `widgetFilters` + `onWidgetFiltersChange`, `columnFilters` + `onColumnFiltersChange`, `sort` + `onSortChange`. A controlled value without its handler is read-only.
|
|
77
|
+
- `onInteractionsChange(state)` fires with the full state after any change.
|
|
78
|
+
|
|
79
|
+
**UI is yours: slots.** Each receives the value and setters. Whatever it sets is applied to the data.
|
|
80
|
+
|
|
81
|
+
| Slot | Args |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| `renderSearch` | `{ value, setValue, clear, placeholder, mode, loading, tokens }` |
|
|
84
|
+
| `renderWidgetFilter` | `{ filters, setFilters, addFilter, removeFilter, clear, fields: {field,label,kind,operators}[], declared, mode, tokens }` |
|
|
85
|
+
| `renderColumnFilter` (tables) | `{ column, selected, setSelected, clear, options: {value,label,count}[], mode, tokens }` |
|
|
86
|
+
| `renderSortIndicator` (tables) | `{ column, direction, toggle, setDirection, tokens }` |
|
|
87
|
+
| `renderToolbar` | `{ search, widgetFilter, interactions, tokens }`. Place the resolved nodes yourself |
|
|
88
|
+
|
|
89
|
+
If you omit a slot, a minimal token-styled default is used.
|
|
90
|
+
**Slots must return elements.** Write `renderColumnFilter={(a) => <MyFilter {...a} />}`, never `renderColumnFilter={MyFilter}`. Slots are called as functions, so a hook-using component passed directly breaks hook order.
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
<DashboardTableWidget
|
|
94
|
+
dataSource={ds} configurations={cfg} showColumnFilter
|
|
95
|
+
renderSearch={({ value, setValue, placeholder }) =>
|
|
96
|
+
<MyInput value={value} onChange={(e) => setValue(e.target.value)} placeholder={placeholder} />}
|
|
97
|
+
renderColumnFilter={({ column, options, selected, setSelected }) =>
|
|
98
|
+
<MyMultiSelect label={column.header} options={options} value={selected} onChange={setSelected} />}
|
|
99
|
+
renderSortIndicator={({ direction, toggle }) => <MySortIcon dir={direction} onClick={toggle} />}
|
|
100
|
+
renderWidgetFilter={({ fields, filters, addFilter, removeFilter }) =>
|
|
101
|
+
<MyFilterBuilder fields={fields} value={filters} onAdd={addFilter} onRemove={removeFilter} />}
|
|
102
|
+
/>
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
**Where it runs.** Automatic; don't fight it:
|
|
106
|
+
|
|
107
|
+
| Source | Search / column filter / sort | Widget filters |
|
|
108
|
+
| --- | --- | --- |
|
|
109
|
+
| form/task, no `pivot.rows` | server (full dataset) | server |
|
|
110
|
+
| form/task with `pivot.rows` | loaded rows (buckets) | server |
|
|
111
|
+
| legacy widget-id, `automationRows` | loaded rows | loaded rows |
|
|
112
|
+
| `ChildTableWidget` | loaded rows | server |
|
|
113
|
+
|
|
114
|
+
`meta.tableMode` / `meta.filterMode` (and each slot's `mode`) report which one applies.
|
|
115
|
+
|
|
116
|
+
`FilterPanelWidget` is a **dashboard-level** control. It is controlled via `value`/`onChange` and issues no queries. Feed its output into other widgets' `dashFilters`. Use widget filters to narrow a single widget.
|
|
117
|
+
|
|
118
|
+
## Rendering a stored dashboard
|
|
119
|
+
|
|
120
|
+
For `getDashboard(dashId, zappId).elements[0].widgets`, never parse or lay out by hand:
|
|
121
|
+
`normalizeWidget(w)` (deep-parses `Configuration`/`DataSource`, hoists legacy `isTask`/`formId`) ·
|
|
122
|
+
`gridWidgets(ws)` · `resolveDashboardLayout(ws)` + `gridHeight(h)` (60-col grid, legacy ×5) ·
|
|
123
|
+
`dashboardFilterDefs(ws)` + `buildDashFilters(values, range)` → `dashFilters` ·
|
|
124
|
+
`cdateFiltersFor(w, ws)` → `cdateFilters` · `dateRangeFor(token)` · `buildDashboardTree(getDashboards().elements, isAdmin)`.
|
|
125
|
+
Stored table columns are `{ label, dataPoint, type }` and are read as-is.
|
|
126
|
+
|
|
127
|
+
## Authoring a data source
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
import { formSource, taskSource, field, taskField, groupBy, aggregate, derive, where, pivot } from "@zestl/zvolv-widgets";
|
|
131
|
+
|
|
132
|
+
const region = field("Region", "DROP_DOWN"); // use the REAL element type
|
|
133
|
+
const revenue = field("Revenue", "INPUT_CURRENCY");
|
|
134
|
+
|
|
135
|
+
formSource(1042, [region, revenue]); // raw rows (tables, lists)
|
|
136
|
+
formSource(1042, [region, revenue], { // grouped (charts)
|
|
137
|
+
pivot: pivot({
|
|
138
|
+
rows: [groupBy(region)],
|
|
139
|
+
summary: [aggregate(revenue, "sum")], // sum avg min max count value_count cardinality
|
|
140
|
+
postOperations: [derive("Share", "PERCENT", ["Revenue", "Total"])], // ADD SUB MUL DIV PERCENT; order matters
|
|
141
|
+
}),
|
|
142
|
+
});
|
|
143
|
+
taskSource([taskField("Status"), taskField("Priority")]); // tasks index, no formId
|
|
144
|
+
|
|
145
|
+
field("Status", "DROP_DOWN", { filter: where("IN", "Open") }); // baked-in author filter, always applied
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Element type → ES type: `INPUT_NUMBER`/`INPUT_CURRENCY`/`INPUT_PERCENT` → FLOAT · `FORM_SEARCH`/`INPUT_TAG` → ARRAY · `CHECK_BOX` → BOOLEAN · else STRING.
|
|
149
|
+
|
|
150
|
+
## Writing configuration
|
|
151
|
+
|
|
152
|
+
```tsx
|
|
153
|
+
import { defineConfig, type ChartConfig, type KpiConfig, type TableConfig } from "@zestl/zvolv-widgets";
|
|
154
|
+
|
|
155
|
+
const chart = defineConfig<ChartConfig>({
|
|
156
|
+
title: "Revenue by region", template: "horizontal",
|
|
157
|
+
series: [{ name: "Revenue", valueField: "Revenue", argumentField: "Region" }],
|
|
158
|
+
axis: { valueAxis: { valueFormat: "currency" } }, legends: { show: false },
|
|
159
|
+
showWidgetFilter: true,
|
|
160
|
+
});
|
|
161
|
+
const table = defineConfig<TableConfig>({
|
|
162
|
+
title: "Deals", showSearchbar: true, showColumnFilter: true,
|
|
163
|
+
columns: [{ key: "Region", header: "Region" }, { key: "Revenue", header: "Revenue", kind: "number" }],
|
|
164
|
+
});
|
|
165
|
+
const kpi = defineConfig<KpiConfig>({
|
|
166
|
+
contents: [{ title: "Orders", dataPoint: "Orders", template: "trend", targetPoint: "Target", deltaMode: "percent" }],
|
|
167
|
+
});
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Schemas: `ChartConfig` `KpiConfig` `TableConfig` `CollectionConfig` `TemporalConfig` `StepperConfig` `GaugeConfig` `HeatMapConfig` `RankingConfig` `TreeConfig` `MapConfig` `HtmlConfig` `IframeConfig` `ActionConfig` `TabConfig` `FormConfig` `FilterPanelConfig`.
|
|
171
|
+
Shared chrome keys: `title` `description` `hideHeader` `customMessage` `enableFullScreen` `enableRefreshButtons` `bgColor` `template` `style` `showSearchbar` `showWidgetFilter` `showColumnFilter` `enableSort` `searchPlaceholder`.
|
|
172
|
+
Temporal configs keep the zvolv-web spellings `projectTitle` `taskTitle` `taskStatus` `startDate` `endDate`.
|
|
173
|
+
KPI templates: `standard` `trend` `comparison` `progress` `financial` `status` `chart` `hero_chart` `icon`.
|
|
174
|
+
|
|
175
|
+
## Widget catalogue and `render` args
|
|
176
|
+
|
|
177
|
+
| Family | Components | `render` args |
|
|
178
|
+
| --- | --- | --- |
|
|
179
|
+
| KPI | `KpiWidget` | templates get `{ vm, index }`. `vm` is fully resolved; never read `vm.raw` |
|
|
180
|
+
| Charts | `BarChartWidget` `LineChartWidget` `AreaChartWidget` `PieChartWidget` `DoughnutChartWidget` `ScatterChartWidget` | `{ rows, xKey, series, colorFor, toLabel, template, config, tokens, meta }` |
|
|
181
|
+
| Ranking | `RankingBarWidget` | `{ rows: {rank,label,value,ratio}[], max }` |
|
|
182
|
+
| Gauge | `GaugeMeterWidget` | `{ value, min, max, ratio, color, band }` |
|
|
183
|
+
| Heat map | `HeatMapWidget` | `{ cells: {zone,count,color,band}[], grid }` |
|
|
184
|
+
| Tables | `DashboardTableWidget` `DatabaseTableWidget` `TaskTrackerTableWidget` `ChildTableWidget` `PivotGridWidget` | `{ rows, columns, renderCell, toLabel, interactions, headerControls(col), getColumnOptions(key), enableSort, showColumnFilter, mode }` |
|
|
185
|
+
| Collections | `CardListWidget` `ActivityFeedWidget` `EntityGalleryWidget` `GridCardWidget` `CarouselWidget` `BannerWidget` | `{ items: CollectionItem[] }` |
|
|
186
|
+
| Temporal | `TimelineWidget` `GanttChartWidget` `CalendarWidget` `MeetingSchedulerWidget` | `{ events, range, lanes }` |
|
|
187
|
+
| Stepper | `StepperWidget` | `{ steps, currentIndex }` |
|
|
188
|
+
| Display | `HtmlWidget` `RawHtmlWidget` `IframeWidget` `GoogleMapWidget` `TreeWidget` `ActionCardWidget` | see types |
|
|
189
|
+
| Containers | `TabWidget` `FormWidget` `FilterPanelWidget` `ZaiChatWidget` | see types |
|
|
190
|
+
|
|
191
|
+
**`render` is REQUIRED** for `ZaiChatWidget`, `GoogleMapWidget` and `HtmlWidget` markup. `FormWidget` renders `<ZvolvForm>` by default (see Forms). The package never calls `dangerouslySetInnerHTML`.
|
|
192
|
+
|
|
193
|
+
In chart `render`, always use `xKey` and `series[].valueField` from the args, pass axis and user values through `toLabel`, and colour with `colorFor(i, s)`.
|
|
194
|
+
In a custom table `render`, put `headerControls(column)` in each header cell to keep the sort and filter controls. `rows` are already searched, filtered and sorted.
|
|
195
|
+
|
|
196
|
+
## Styling, cheapest first
|
|
197
|
+
|
|
198
|
+
1. Tokens: `<ZvolvThemeProvider tokens={{ colors, typography, widget, surfaces, overrides, widgetDefaults }}>`. `widgetDefaults` layers **under** each widget's own config.
|
|
199
|
+
2. `render` prop per widget.
|
|
200
|
+
3. `<TemplateProvider templates={{ "KPI_CARD:standard": MyCard, "BAR_CHART:*": MyBars }}>`.
|
|
201
|
+
4. Interaction slots for the controls.
|
|
202
|
+
|
|
203
|
+
## Headless
|
|
204
|
+
|
|
205
|
+
```tsx
|
|
206
|
+
const ix = useWidgetInteractions({ defaultSort: { key: "Revenue", direction: "desc" } });
|
|
207
|
+
const { rows, sourceRows, state, meta, refresh } = useWidgetData({
|
|
208
|
+
dataSource, configurations, widgetType: "TABLE",
|
|
209
|
+
search: ix.search, columnFilters: ix.columnFilters, sort: ix.sort, widgetFilters: ix.widgetFilters,
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Also: `useInteractiveWidgetData`, `useKpiContents`, `useCollectionItems`, `useTemporalEvents`, `useTableColumns`.
|
|
214
|
+
|
|
215
|
+
## Previewing without a backend
|
|
216
|
+
|
|
217
|
+
`createMockRuntime({ rows?, delay?, onQuery? })` applies filters, search and sort to its rows, so controls visibly work. It does not aggregate pivots.
|
|
218
|
+
|
|
219
|
+
## Forms
|
|
220
|
+
|
|
221
|
+
Forms work like widgets: Zvolv owns the behaviour and you own the look.
|
|
222
|
+
|
|
223
|
+
- Render with `<ZvolvForm formId … />`, or let a `FORM` / `FORM_WIDGET` dashboard widget do it.
|
|
224
|
+
- Style every field once with `<FormKitSlot.Provider value={{ field, form, status, grid }}>`. Switch on `field.kind`, and use `useFieldOptions`, `useFileField`, `useSheetField` and `form.runAction` for behaviour. Never re-implement rules, formulas, lookups or the submit body.
|
|
225
|
+
- Sheet row formulas: call `useSheetField().updateRow` — it recalculates formula cells. For a debug/inspector panel use `useFormInspector(form)` (rules, filters, validation, formulas, defaults, payload).
|
|
226
|
+
- Write formulas with `{Label}` references (`MUL({Qty}, {Price})`). The submission API re-evaluates formulas and reads bare labels as 0, so `MUL(Qty, Price)` is rejected with "Evaluated value \"0\" … do not match". Check with `isServerSafeFormula`.
|
|
227
|
+
- Nested layout: for `section`, `tabs` and `steps` kinds, render `<FormFields form={form} fields={form.childrenOf(field)} />`.
|
|
228
|
+
- Kind `"trials"` (ABA widgets) → draw with `useTrialField`; action `variant === "dropdown"` (ACTION_DROPDOWN) → a select of `field.options`, call `form.runAction(field, label)`.
|
|
229
|
+
- Lookup / people controls must call `o.onFocus()` on focus — options load on focus (zvolv-web / ZAI parity). Single vs multi is `maxResults`; respect `o.atLimit`; INPUT_TAG free text via `o.addFreeTag`. ExtraLabels auto-fill is opt-in (`autofillExtraLabels`); prefer FETCH_OPTIONS_FILTERS. Draw kind `"onbehalf"` read-only with `field.displayText`; checkbox `variant === "switch"`; sheets honour `canAdd` / `canDelete` / `limitMessage`. SAVE_SUBMISSION and other open-UI actions need `onAction`.
|
|
230
|
+
- Triage / show-hide-mandatory rules follow ZAI-Dashboard: `properties.triage` overrides `dependencies`; array values must match the selection exactly; a matching rule beats a non-matching one; hiding keeps the value; `field.error` appears only after submit and clears when the field changes. Don't add reset-on-hide or live "touched" errors in the app.
|
|
231
|
+
- Dependent lookups (`FETCH_OPTIONS_FILTERS`) use the platform shape: the rule sits **on the dependent lookup** — `{ type: "FETCH_OPTIONS_FILTERS", elementId: <source field>, masterField: <label on its source form> }`. `useFieldOptions` filters by the source's value, reports `waitingFor` until it is set, clears the selection when it changes, and auto-selects when exactly one row matches. Build it with `cascade(source, target, masterField)`.
|
|
232
|
+
|
|
233
|
+
When asked to build a dashboard that captures data:
|
|
234
|
+
1. Create the form: build the payload with `el.*` / `when` / `defineForm`, then call `create_form`.
|
|
235
|
+
2. Re-read it with `get_form_by_id` to confirm elementIds and rules.
|
|
236
|
+
3. Add it with `formWidget({ formId, title, layout })` → `create_widget`, sized to its fields. An `ACTION_CARD` with `actionType: NEW_SUBMISSION` also opens the form in the host.
|
|
237
|
+
|
|
238
|
+
`create_form` quirks:
|
|
239
|
+
- Formulas must use bare, single-word labels: `MUL(Quantity, Rate)`.
|
|
240
|
+
- Never use `AVG`: every write to the form 502s. Use `DIVI(ADD(...), n)`.
|
|
241
|
+
- Required SIGNATURE fields are enforced even with `skipValidations`. Seed data needs a data-URL value.
|
|
242
|
+
- `width` is a percentage, placed at the element root and in `attributes.Width`.
|
|
243
|
+
- Rules go on the driving field; each target starts `hidden: true`.
|
|
244
|
+
|
|
245
|
+
## Checklist before you finish
|
|
246
|
+
|
|
247
|
+
- [ ] Every plotted, grouped, filtered, searched or sorted name exists as a `dataSource.fields` header, and series names equal the pivot titles/values.
|
|
248
|
+
- [ ] No fetch, query, `.filter()` or `.sort()` over widget rows in your code. Interactions go through props or slots.
|
|
249
|
+
- [ ] Controlled interaction props each have their `on*Change`.
|
|
250
|
+
- [ ] No hard-coded colours. `render` is supplied for ZaiChat, GoogleMap and Html markup, and a FormKit is provided when forms are shown.
|
|
251
|
+
- [ ] Each widget sits in a sized container, and `ChildTableWidget` is width-clamped.
|