@zestl/zvolv-widgets 0.0.0-stage → 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SKILL.md ADDED
@@ -0,0 +1,248 @@
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
+ - Nested layout: for `section`, `tabs` and `steps` kinds, render `<FormFields form={form} fields={form.childrenOf(field)} />`.
226
+ - 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`.
227
+ - 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.
228
+ - 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)`.
229
+
230
+ When asked to build a dashboard that captures data:
231
+ 1. Create the form: build the payload with `el.*` / `when` / `defineForm`, then call `create_form`.
232
+ 2. Re-read it with `get_form_by_id` to confirm elementIds and rules.
233
+ 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.
234
+
235
+ `create_form` quirks:
236
+ - Formulas must use bare, single-word labels: `MUL(Quantity, Rate)`.
237
+ - Never use `AVG`: every write to the form 502s. Use `DIVI(ADD(...), n)`.
238
+ - Required SIGNATURE fields are enforced even with `skipValidations`. Seed data needs a data-URL value.
239
+ - `width` is a percentage, placed at the element root and in `attributes.Width`.
240
+ - Rules go on the driving field; each target starts `hidden: true`.
241
+
242
+ ## Checklist before you finish
243
+
244
+ - [ ] Every plotted, grouped, filtered, searched or sorted name exists as a `dataSource.fields` header, and series names equal the pivot titles/values.
245
+ - [ ] No fetch, query, `.filter()` or `.sort()` over widget rows in your code. Interactions go through props or slots.
246
+ - [ ] Controlled interaction props each have their `on*Change`.
247
+ - [ ] No hard-coded colours. `render` is supplied for ZaiChat, GoogleMap and Html markup, and a FormKit is provided when forms are shown.
248
+ - [ ] Each widget sits in a sized container, and `ChildTableWidget` is width-clamped.