@sybilion/uilib 1.3.65 → 1.3.67
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-glossary/content.generated.ts +253 -0
- package/dist/agent-glossary/content.md +252 -0
- package/dist/agent-glossary/workspace.generated.ts +272 -0
- package/dist/agent-glossary/workspace.md +271 -0
- package/dist/esm/components/ui/Chart/Chart.helpers.js +20 -1
- package/dist/esm/components/ui/Chart/components/BaseChartWrapper.js +2 -1
- package/dist/esm/components/ui/Chart/components/ChartTooltipContent.js +25 -22
- package/dist/esm/types/src/components/ui/Chart/Chart.helpers.d.ts +16 -0
- package/dist/esm/types/src/components/ui/Chart/components/ChartTooltipItem.d.ts +1 -8
- package/package.json +1 -1
- package/src/components/ui/Chart/Chart.helpers.ts +42 -0
- package/src/components/ui/Chart/components/BaseChartWrapper.tsx +2 -4
- package/src/components/ui/Chart/components/ChartTooltipContent.tsx +37 -32
- package/src/components/ui/Chart/components/ChartTooltipItem.tsx +1 -10
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/** Generated by scripts/build-agent-glossary.mjs — do not edit. */
|
|
2
|
+
export const WORKSPACE_AGENT_UI_GLOSSARY = `Sybilion workspace page glossary (workspace profile)
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
# Page layout system
|
|
6
|
+
|
|
7
|
+
Sybilion workspace and report surfaces use a small set of **Page** primitives from \`@sybilion/uilib\`. Agents must compose these instead of inventing layout with inline styles, Tailwind structure classes, or ad-hoc wrappers.
|
|
8
|
+
|
|
9
|
+
## Two modes
|
|
10
|
+
|
|
11
|
+
| Mode | When | Shell |
|
|
12
|
+
| ------------------ | ------------------------------------------------ | ------------------------------------------------------------------------------------- |
|
|
13
|
+
| **workspace-page** | Route inside \`AppShell\` / scrolled main column | \`PageHeader\` → \`PageContent\` → \`PageContentSection\`(s). Optional \`PageTabs\`. |
|
|
14
|
+
| **content** | Report tiles, json-dashboard, embedded body only | No \`PageHeader\` / \`AppShell\` chrome. Map React blocks to dashboard tiles (see below). |
|
|
15
|
+
|
|
16
|
+
Do not nest workspace shell components inside **content** mode output.
|
|
17
|
+
|
|
18
|
+
## Mandatory workspace-page skeleton
|
|
19
|
+
|
|
20
|
+
\`\`\`tsx
|
|
21
|
+
<PageHeader title="…" subheader="…" breadcrumbs={[…]} actions={…} />
|
|
22
|
+
<PageContent>
|
|
23
|
+
<PageContentSection>{/* section A */}</PageContentSection>
|
|
24
|
+
<PageContentSection grow={false}>{/* section B */}</PageContentSection>
|
|
25
|
+
</PageContent>
|
|
26
|
+
\`\`\`
|
|
27
|
+
|
|
28
|
+
- **PageHeader**: one per route view in the main column; title, optional subheader, breadcrumbs, actions.
|
|
29
|
+
- **PageContent**: vertical stack for the body below the header; \`variant="clean"\` when you need a flatter body (no extra chrome).
|
|
30
|
+
- **PageContentSection**: primary vertical band; applies horizontal **page-x-padding** via Stylus (\`pageXPadding()\`). Use multiple sections to separate logical blocks.
|
|
31
|
+
|
|
32
|
+
Global tokens (host app \`:root\`, e.g. \`standalone-global.css\`): \`--page-width\`, \`--page-x-padding\`, \`--page-y-padding\`. \`PageContent\` caps width at \`--page-width\`.
|
|
33
|
+
|
|
34
|
+
## PageTabs — rules and patterns
|
|
35
|
+
|
|
36
|
+
**NEVER** place \`PageTabs\` inside \`PageContentSection\`. Tabs manage their own horizontal inset via \`PageXScroll\` / \`tabsListProps.withPaddings\`.
|
|
37
|
+
|
|
38
|
+
### Unified (tabs + panels inside PageContent)
|
|
39
|
+
|
|
40
|
+
\`\`\`tsx
|
|
41
|
+
<PageHeader … />
|
|
42
|
+
<PageContent>
|
|
43
|
+
<PageTabs
|
|
44
|
+
items={[
|
|
45
|
+
{ value: 'a', label: 'Tab A', content: <PageContentSection>…</PageContentSection> },
|
|
46
|
+
{ value: 'b', label: 'Tab B', content: <PageContentSection>…</PageContentSection> },
|
|
47
|
+
]}
|
|
48
|
+
/>
|
|
49
|
+
</PageContent>
|
|
50
|
+
\`\`\`
|
|
51
|
+
|
|
52
|
+
Tab **panels** use \`PageContentSection\` so section padding stays correct.
|
|
53
|
+
|
|
54
|
+
### Split (bar-only sibling)
|
|
55
|
+
|
|
56
|
+
Use when the tab bar must sit between header and a custom scroll region:
|
|
57
|
+
|
|
58
|
+
\`\`\`tsx
|
|
59
|
+
<PageHeader … />
|
|
60
|
+
<PageTabs
|
|
61
|
+
items={[
|
|
62
|
+
{ value: 'a', label: 'Tab A', content: null },
|
|
63
|
+
{ value: 'b', label: 'Tab B', content: null },
|
|
64
|
+
]}
|
|
65
|
+
/>
|
|
66
|
+
<PageContent>…</PageContent>
|
|
67
|
+
\`\`\`
|
|
68
|
+
|
|
69
|
+
Set \`content: null\` (or omit) for bar-only items; render panel bodies in \`PageContent\` yourself.
|
|
70
|
+
|
|
71
|
+
## Spacing ownership
|
|
72
|
+
|
|
73
|
+
| Component | Owns horizontal page padding? |
|
|
74
|
+
| ---------------------- | --------------------------------------------------------- |
|
|
75
|
+
| **PageContentSection** | Yes (\`pageXPadding\`) |
|
|
76
|
+
| **PageTabs** | Via \`tabsListProps.withPaddings\` + internal \`PageXScroll\` |
|
|
77
|
+
| **PageXScroll** | Reads \`--page-x-padding\` for inset when used standalone |
|
|
78
|
+
| **PageHeader** | Own header inner layout (not \`PageContentSection\`) |
|
|
79
|
+
|
|
80
|
+
Do not duplicate \`padding-left/right\` on children that already sit in \`PageContentSection\`.
|
|
81
|
+
|
|
82
|
+
## Styling rules
|
|
83
|
+
|
|
84
|
+
- **No** \`style={{…}}\` for layout or spacing (margins, padding, flex, width, gap).
|
|
85
|
+
- **No** Tailwind (or other) utility classes for **structural** layout on page primitives.
|
|
86
|
+
- Custom layout CSS only in co-located \`ComponentName.styl\`, using design tokens (\`var(--p-*)\`, \`var(--page-x-padding)\`, \`pageXPadding()\`, etc.).
|
|
87
|
+
- \`className\` on Page components is for Stylus module classes or state hooks, not one-off pixel tweaks.
|
|
88
|
+
|
|
89
|
+
## Component reference
|
|
90
|
+
|
|
91
|
+
### PageHeader
|
|
92
|
+
|
|
93
|
+
Sticky-style page title row: breadcrumbs (optional sidebar trigger in full app), \`title\`, \`subheader\`, \`actions\`. Lives **above** \`PageContent\`, never inside a section.
|
|
94
|
+
|
|
95
|
+
### PageContent / PageContentSection
|
|
96
|
+
|
|
97
|
+
\`PageContent\`: column flex child, \`max-width: var(--page-width)\`.
|
|
98
|
+
\`PageContentSection\`: \`grow\` (default \`true\`) lets the section absorb remaining height in flex layouts.
|
|
99
|
+
|
|
100
|
+
### PageColumns
|
|
101
|
+
|
|
102
|
+
Side-by-side columns with gap. **\`fill\`** controls which column(s) grow:
|
|
103
|
+
|
|
104
|
+
| \`fill\` | Behavior |
|
|
105
|
+
| --------------- | ----------------------------------------------------------------------------------------------- |
|
|
106
|
+
| \`all\` (default) | Every column \`flex-grow: 1\`, \`max-width\` unset — use for equal split or fluid grids of columns. |
|
|
107
|
+
| \`left\` | First column grows; others stay at fixed min/max column width. |
|
|
108
|
+
| \`right\` | Last column grows. |
|
|
109
|
+
|
|
110
|
+
Pass \`columns={[node1, node2, …]}\`. On mobile, stacks vertically. Typical pattern: main + aside inside one \`PageContentSection\`.
|
|
111
|
+
|
|
112
|
+
### PageXScroll
|
|
113
|
+
|
|
114
|
+
Horizontal scroll strip; used inside \`PageTabs\` and anywhere a overflowing row needs page-aligned padding. Props: \`size\`, \`fullWidth\`, \`innerClassName\`, \`scrollbarClassName\`.
|
|
115
|
+
|
|
116
|
+
### SectionHeader
|
|
117
|
+
|
|
118
|
+
In-section title block (\`title\`, \`description\`, \`actions\`). Use **inside** \`PageContentSection\`, not as a replacement for \`PageHeader\`.
|
|
119
|
+
|
|
120
|
+
### PageEmptyCanvas
|
|
121
|
+
|
|
122
|
+
Centered empty state: \`title\`, \`hint\`, optional \`children\` (e.g. CTA button). Use when a route has no data yet.
|
|
123
|
+
|
|
124
|
+
### GridLayout
|
|
125
|
+
|
|
126
|
+
Responsive CSS grid (\`repeat(auto-fit, minmax(colWidth, 1fr))\`). Use for card/tile dashboards inside a section. Report mapping: dashboard **grid** rows / multi-column tile groups.
|
|
127
|
+
|
|
128
|
+
### Card / Foldable
|
|
129
|
+
|
|
130
|
+
Use inside sections for grouped content; see package \`AGENT.md\` on those components when present. Do not wrap the whole page in Card.
|
|
131
|
+
|
|
132
|
+
## Content mode — React → json-dashboard tiles
|
|
133
|
+
|
|
134
|
+
When generating **report** / dashboard specs (not workspace routes), map UI blocks to tiles:
|
|
135
|
+
|
|
136
|
+
| React / intent | Dashboard tile / markdown |
|
|
137
|
+
| ------------------------------- | ----------------------------------------------------------------- |
|
|
138
|
+
| \`PageHeader\` title text | First \`markdown\` tile \`# Title\` or report title field |
|
|
139
|
+
| \`SectionHeader\` \`title\` | \`markdown\` with \`## …\` |
|
|
140
|
+
| \`SectionHeader\` \`description\` | markdown paragraph under that heading |
|
|
141
|
+
| \`PageContentSection\` block | group of tiles or one markdown section |
|
|
142
|
+
| \`GridLayout\` children | grid row of widget tiles |
|
|
143
|
+
| \`ChartAreaInteractive\` / charts | \`dataset_card\`, \`performance_chart\`, etc. (see widget \`AGENT.md\`) |
|
|
144
|
+
| \`PageEmptyCanvas\` copy | \`markdown\` explanatory tile |
|
|
145
|
+
|
|
146
|
+
See \`AGENT.report-only-tiles.md\` in the app repo for tiles without a uilib widget.
|
|
147
|
+
|
|
148
|
+
## Shell components (workspace only)
|
|
149
|
+
|
|
150
|
+
Not part of the per-route skeleton but part of **workspace-page** host wiring:
|
|
151
|
+
|
|
152
|
+
- **AppShell** / **AppShellMainContent** — sidebar + main column + header slot + footer.
|
|
153
|
+
- **PageScroll** — scroll root for the main column.
|
|
154
|
+
- **SybilionAppHeader** — workspace app switcher + user menu (lives in header slot, not inside \`PageContent\`).
|
|
155
|
+
|
|
156
|
+
## Anti-patterns
|
|
157
|
+
|
|
158
|
+
- \`PageTabs\` inside \`PageContentSection\`.
|
|
159
|
+
- Inline styles or Tailwind for page structure.
|
|
160
|
+
- Second \`PageHeader\` nested in content.
|
|
161
|
+
- \`PageContent\` without sections for multi-block pages (prefer multiple \`PageContentSection\`).
|
|
162
|
+
- Wrapping data widgets in extra layout divs with hard-coded padding.
|
|
163
|
+
|
|
164
|
+
## Future candidates (not implemented)
|
|
165
|
+
|
|
166
|
+
- **PageToolbar** — dedicated action row between header and tabs.
|
|
167
|
+
- **PageFullBleed** — edge-to-edge section escape hatch.
|
|
168
|
+
|
|
169
|
+
Do not add these locally; extend uilib when product needs them.
|
|
170
|
+
|
|
171
|
+
# SidebarDatasetsItemsGrouped
|
|
172
|
+
|
|
173
|
+
Renders: expandable sidebar groups of datasets (by target type, regions, or categories).
|
|
174
|
+
|
|
175
|
+
Use when: app shell needs grouped dataset navigation.
|
|
176
|
+
Not when: in-page or report content (no sidebar slot).
|
|
177
|
+
|
|
178
|
+
Host provides:
|
|
179
|
+
|
|
180
|
+
- \`datasets\` list (\`SidebarDatasetsItemsGroupedDataset\`)
|
|
181
|
+
- \`groupBy\`, \`selectedDatasetId\`, \`onDatasetClick\`
|
|
182
|
+
- Optional \`preItems\` / \`postItems\`, \`defaultExpandedGroupNames\`
|
|
183
|
+
|
|
184
|
+
Report tile: Not used in report tiles.
|
|
185
|
+
|
|
186
|
+
Requires: \`datasets\`; \`groupBy\`; \`onDatasetClick\` for navigation.
|
|
187
|
+
|
|
188
|
+
Empty/loading: empty \`datasets\` renders no groups; loading handled by host before pass-in.
|
|
189
|
+
|
|
190
|
+
# ChartAreaInteractive
|
|
191
|
+
|
|
192
|
+
Renders: time-series chart with historical line, forecast lines, optional pin / quantile-band / threshold overlays.
|
|
193
|
+
|
|
194
|
+
Use when: custom forecast UI with overlays or full chart control.
|
|
195
|
+
Not when: packaged performance or drivers-comparison views (use PerformanceChart or DriversComparisonChart).
|
|
196
|
+
|
|
197
|
+
Host provides:
|
|
198
|
+
|
|
199
|
+
- \`chartData\`, \`forecastData\` built from API
|
|
200
|
+
- \`timeRange\` / \`onTimeRangeChange\` or brush-only range
|
|
201
|
+
- Optional \`mode\`: pin | intervals | thresholds + overlay state
|
|
202
|
+
- Analysis selector and fetch outside widget
|
|
203
|
+
|
|
204
|
+
Report tile: \`dataset_card\` — host loads dataset + analysis; chart inside dashboard card.
|
|
205
|
+
|
|
206
|
+
Requires: \`chartData\`; \`forecastData\`; \`loading\`; \`toggleLegendSeries\` / \`ensureAnalysisSeriesVisible\` when legend is external.
|
|
207
|
+
|
|
208
|
+
Empty/loading: \`loading\`, \`error\`; empty data shows chart empty state via host message props.
|
|
209
|
+
|
|
210
|
+
# DriverMap
|
|
211
|
+
|
|
212
|
+
Renders: world map with regional driver badges, bottom strip for world-level drivers, selection highlight.
|
|
213
|
+
|
|
214
|
+
Use when: geographic driver exploration for one analysis.
|
|
215
|
+
Not when: driver detail metrics card alone (pair with DriverCard) or normalized series chart (use DriversComparisonChart).
|
|
216
|
+
|
|
217
|
+
Host provides:
|
|
218
|
+
|
|
219
|
+
- \`drivers\` as \`DriverData[]\` from analysis API
|
|
220
|
+
- Controlled \`selectedDriver\` + \`setSelectedDriver\`
|
|
221
|
+
- \`isLoading\` while fetching drivers
|
|
222
|
+
|
|
223
|
+
Report tile: \`drivers_map\` — tile resolves analysis id, fetches drivers, passes list + selection (see EmbeddedAnalysisSelector pattern).
|
|
224
|
+
|
|
225
|
+
Requires: \`drivers\`; \`isLoading\`; \`selectedDriver\`; \`setSelectedDriver\`.
|
|
226
|
+
|
|
227
|
+
Empty/loading: \`isLoading\` shows overlay; empty \`drivers\` leaves map without badges.
|
|
228
|
+
|
|
229
|
+
# PerformanceChart
|
|
230
|
+
|
|
231
|
+
Renders: forecast performance on ChartAreaInteractive — per-horizon tab (24m window, MAE/MAPE table) and spaghetti tab (all horizons overlaid).
|
|
232
|
+
|
|
233
|
+
Use when: dataset performance tab with horizon selector and error metrics.
|
|
234
|
+
Not when: simple forecast card or driver backtests — use ChartAreaInteractive or DriversComparisonChart.
|
|
235
|
+
|
|
236
|
+
Host provides:
|
|
237
|
+
|
|
238
|
+
- \`performanceData\` (PerformanceChartPayload) and \`historicalData\` from performance API
|
|
239
|
+
- Analysis selection and fetch outside widget
|
|
240
|
+
- Optional \`forecastData\`, \`customPerformanceMatrix\`, \`userSeries\` for spaghetti
|
|
241
|
+
|
|
242
|
+
Report tile: \`performance_chart\` — host loads performance payload + dataset series; built-in analysis selector.
|
|
243
|
+
|
|
244
|
+
Requires: \`performanceData\` — model/drift forecasts and metrics; \`historicalData\` — baseline series; \`loading\` / \`chartLoading\` / \`performanceDataLoading\` — spinners; \`runAnalysisHint\` / \`statusHint\` — empty states.
|
|
245
|
+
|
|
246
|
+
Empty/loading: loading props show shimmer/spinner; null \`performanceData\` with \`runAnalysisHint\` prompts to run analysis.
|
|
247
|
+
|
|
248
|
+
# DriversComparisonChart
|
|
249
|
+
|
|
250
|
+
Renders: target vs drivers backtests chart with ChartAreaInteractive plus table; row click toggles series visibility.
|
|
251
|
+
|
|
252
|
+
Use when: drivers comparison tab with normalized target and driver series from backtests payload.
|
|
253
|
+
Not when: geographic map or performance horizons — use DriverMap or PerformanceChart.
|
|
254
|
+
|
|
255
|
+
Host provides:
|
|
256
|
+
|
|
257
|
+
- \`payload\`: BacktestsComponentPayload from platform SDK (host fetch per analysis)
|
|
258
|
+
- Optional \`datasetHistorical\` overlay
|
|
259
|
+
- \`seriesInitKey\` when selected analysis changes
|
|
260
|
+
- \`viewTab\` / \`onViewTabChange\`: \`lagged\` (calendar-aligned, raw dates) or \`overlapped\` (driver series shifted backward by parsed lag months)
|
|
261
|
+
|
|
262
|
+
View tabs: host should render uilib \`Tabs variant="button"\` with **Lagged** / **Overlapped** in the toolbar (analysis selector left, tabs right). Chart applies \`applyDriversComparisonViewToPayload\` internally.
|
|
263
|
+
|
|
264
|
+
Report tile: \`drivers_comparison_chart\` — host loads normalized backtests payload + dataset historical; built-in analysis selector.
|
|
265
|
+
|
|
266
|
+
Requires: \`payload\` — target + driver normalized_series; \`loading\` / \`chartLoading\` — spinners; \`seriesInitKey\` — reset visible series on analysis or view tab change; \`runAnalysisHint\` / \`statusHint\` — empty/error text.
|
|
267
|
+
|
|
268
|
+
Lag column: **Lagged** shows API \`lag\` string (may be a range). **Overlapped** shows single \`N month(s)\` from \`parseLagMonthsFromLabel\` (range uses max month).
|
|
269
|
+
|
|
270
|
+
Historical window: lead-in is anchored to the **lagged** (unshifted) view (\`computeDriversComparisonHistoricalWindowFloor\`); overlapped tab shifts driver lines backward only — switching tabs does not change historical extent when floor is pinned.
|
|
271
|
+
|
|
272
|
+
Empty/loading: loading props shimmer chart; null \`payload\` with \`runAnalysisHint\` prompts to run analysis.`;
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
Sybilion workspace page glossary (workspace profile)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# Page layout system
|
|
5
|
+
|
|
6
|
+
Sybilion workspace and report surfaces use a small set of **Page** primitives from `@sybilion/uilib`. Agents must compose these instead of inventing layout with inline styles, Tailwind structure classes, or ad-hoc wrappers.
|
|
7
|
+
|
|
8
|
+
## Two modes
|
|
9
|
+
|
|
10
|
+
| Mode | When | Shell |
|
|
11
|
+
| ------------------ | ------------------------------------------------ | ------------------------------------------------------------------------------------- |
|
|
12
|
+
| **workspace-page** | Route inside `AppShell` / scrolled main column | `PageHeader` → `PageContent` → `PageContentSection`(s). Optional `PageTabs`. |
|
|
13
|
+
| **content** | Report tiles, json-dashboard, embedded body only | No `PageHeader` / `AppShell` chrome. Map React blocks to dashboard tiles (see below). |
|
|
14
|
+
|
|
15
|
+
Do not nest workspace shell components inside **content** mode output.
|
|
16
|
+
|
|
17
|
+
## Mandatory workspace-page skeleton
|
|
18
|
+
|
|
19
|
+
```tsx
|
|
20
|
+
<PageHeader title="…" subheader="…" breadcrumbs={[…]} actions={…} />
|
|
21
|
+
<PageContent>
|
|
22
|
+
<PageContentSection>{/* section A */}</PageContentSection>
|
|
23
|
+
<PageContentSection grow={false}>{/* section B */}</PageContentSection>
|
|
24
|
+
</PageContent>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- **PageHeader**: one per route view in the main column; title, optional subheader, breadcrumbs, actions.
|
|
28
|
+
- **PageContent**: vertical stack for the body below the header; `variant="clean"` when you need a flatter body (no extra chrome).
|
|
29
|
+
- **PageContentSection**: primary vertical band; applies horizontal **page-x-padding** via Stylus (`pageXPadding()`). Use multiple sections to separate logical blocks.
|
|
30
|
+
|
|
31
|
+
Global tokens (host app `:root`, e.g. `standalone-global.css`): `--page-width`, `--page-x-padding`, `--page-y-padding`. `PageContent` caps width at `--page-width`.
|
|
32
|
+
|
|
33
|
+
## PageTabs — rules and patterns
|
|
34
|
+
|
|
35
|
+
**NEVER** place `PageTabs` inside `PageContentSection`. Tabs manage their own horizontal inset via `PageXScroll` / `tabsListProps.withPaddings`.
|
|
36
|
+
|
|
37
|
+
### Unified (tabs + panels inside PageContent)
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
<PageHeader … />
|
|
41
|
+
<PageContent>
|
|
42
|
+
<PageTabs
|
|
43
|
+
items={[
|
|
44
|
+
{ value: 'a', label: 'Tab A', content: <PageContentSection>…</PageContentSection> },
|
|
45
|
+
{ value: 'b', label: 'Tab B', content: <PageContentSection>…</PageContentSection> },
|
|
46
|
+
]}
|
|
47
|
+
/>
|
|
48
|
+
</PageContent>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Tab **panels** use `PageContentSection` so section padding stays correct.
|
|
52
|
+
|
|
53
|
+
### Split (bar-only sibling)
|
|
54
|
+
|
|
55
|
+
Use when the tab bar must sit between header and a custom scroll region:
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
<PageHeader … />
|
|
59
|
+
<PageTabs
|
|
60
|
+
items={[
|
|
61
|
+
{ value: 'a', label: 'Tab A', content: null },
|
|
62
|
+
{ value: 'b', label: 'Tab B', content: null },
|
|
63
|
+
]}
|
|
64
|
+
/>
|
|
65
|
+
<PageContent>…</PageContent>
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Set `content: null` (or omit) for bar-only items; render panel bodies in `PageContent` yourself.
|
|
69
|
+
|
|
70
|
+
## Spacing ownership
|
|
71
|
+
|
|
72
|
+
| Component | Owns horizontal page padding? |
|
|
73
|
+
| ---------------------- | --------------------------------------------------------- |
|
|
74
|
+
| **PageContentSection** | Yes (`pageXPadding`) |
|
|
75
|
+
| **PageTabs** | Via `tabsListProps.withPaddings` + internal `PageXScroll` |
|
|
76
|
+
| **PageXScroll** | Reads `--page-x-padding` for inset when used standalone |
|
|
77
|
+
| **PageHeader** | Own header inner layout (not `PageContentSection`) |
|
|
78
|
+
|
|
79
|
+
Do not duplicate `padding-left/right` on children that already sit in `PageContentSection`.
|
|
80
|
+
|
|
81
|
+
## Styling rules
|
|
82
|
+
|
|
83
|
+
- **No** `style={{…}}` for layout or spacing (margins, padding, flex, width, gap).
|
|
84
|
+
- **No** Tailwind (or other) utility classes for **structural** layout on page primitives.
|
|
85
|
+
- Custom layout CSS only in co-located `ComponentName.styl`, using design tokens (`var(--p-*)`, `var(--page-x-padding)`, `pageXPadding()`, etc.).
|
|
86
|
+
- `className` on Page components is for Stylus module classes or state hooks, not one-off pixel tweaks.
|
|
87
|
+
|
|
88
|
+
## Component reference
|
|
89
|
+
|
|
90
|
+
### PageHeader
|
|
91
|
+
|
|
92
|
+
Sticky-style page title row: breadcrumbs (optional sidebar trigger in full app), `title`, `subheader`, `actions`. Lives **above** `PageContent`, never inside a section.
|
|
93
|
+
|
|
94
|
+
### PageContent / PageContentSection
|
|
95
|
+
|
|
96
|
+
`PageContent`: column flex child, `max-width: var(--page-width)`.
|
|
97
|
+
`PageContentSection`: `grow` (default `true`) lets the section absorb remaining height in flex layouts.
|
|
98
|
+
|
|
99
|
+
### PageColumns
|
|
100
|
+
|
|
101
|
+
Side-by-side columns with gap. **`fill`** controls which column(s) grow:
|
|
102
|
+
|
|
103
|
+
| `fill` | Behavior |
|
|
104
|
+
| --------------- | ----------------------------------------------------------------------------------------------- |
|
|
105
|
+
| `all` (default) | Every column `flex-grow: 1`, `max-width` unset — use for equal split or fluid grids of columns. |
|
|
106
|
+
| `left` | First column grows; others stay at fixed min/max column width. |
|
|
107
|
+
| `right` | Last column grows. |
|
|
108
|
+
|
|
109
|
+
Pass `columns={[node1, node2, …]}`. On mobile, stacks vertically. Typical pattern: main + aside inside one `PageContentSection`.
|
|
110
|
+
|
|
111
|
+
### PageXScroll
|
|
112
|
+
|
|
113
|
+
Horizontal scroll strip; used inside `PageTabs` and anywhere a overflowing row needs page-aligned padding. Props: `size`, `fullWidth`, `innerClassName`, `scrollbarClassName`.
|
|
114
|
+
|
|
115
|
+
### SectionHeader
|
|
116
|
+
|
|
117
|
+
In-section title block (`title`, `description`, `actions`). Use **inside** `PageContentSection`, not as a replacement for `PageHeader`.
|
|
118
|
+
|
|
119
|
+
### PageEmptyCanvas
|
|
120
|
+
|
|
121
|
+
Centered empty state: `title`, `hint`, optional `children` (e.g. CTA button). Use when a route has no data yet.
|
|
122
|
+
|
|
123
|
+
### GridLayout
|
|
124
|
+
|
|
125
|
+
Responsive CSS grid (`repeat(auto-fit, minmax(colWidth, 1fr))`). Use for card/tile dashboards inside a section. Report mapping: dashboard **grid** rows / multi-column tile groups.
|
|
126
|
+
|
|
127
|
+
### Card / Foldable
|
|
128
|
+
|
|
129
|
+
Use inside sections for grouped content; see package `AGENT.md` on those components when present. Do not wrap the whole page in Card.
|
|
130
|
+
|
|
131
|
+
## Content mode — React → json-dashboard tiles
|
|
132
|
+
|
|
133
|
+
When generating **report** / dashboard specs (not workspace routes), map UI blocks to tiles:
|
|
134
|
+
|
|
135
|
+
| React / intent | Dashboard tile / markdown |
|
|
136
|
+
| ------------------------------- | ----------------------------------------------------------------- |
|
|
137
|
+
| `PageHeader` title text | First `markdown` tile `# Title` or report title field |
|
|
138
|
+
| `SectionHeader` `title` | `markdown` with `## …` |
|
|
139
|
+
| `SectionHeader` `description` | markdown paragraph under that heading |
|
|
140
|
+
| `PageContentSection` block | group of tiles or one markdown section |
|
|
141
|
+
| `GridLayout` children | grid row of widget tiles |
|
|
142
|
+
| `ChartAreaInteractive` / charts | `dataset_card`, `performance_chart`, etc. (see widget `AGENT.md`) |
|
|
143
|
+
| `PageEmptyCanvas` copy | `markdown` explanatory tile |
|
|
144
|
+
|
|
145
|
+
See `AGENT.report-only-tiles.md` in the app repo for tiles without a uilib widget.
|
|
146
|
+
|
|
147
|
+
## Shell components (workspace only)
|
|
148
|
+
|
|
149
|
+
Not part of the per-route skeleton but part of **workspace-page** host wiring:
|
|
150
|
+
|
|
151
|
+
- **AppShell** / **AppShellMainContent** — sidebar + main column + header slot + footer.
|
|
152
|
+
- **PageScroll** — scroll root for the main column.
|
|
153
|
+
- **SybilionAppHeader** — workspace app switcher + user menu (lives in header slot, not inside `PageContent`).
|
|
154
|
+
|
|
155
|
+
## Anti-patterns
|
|
156
|
+
|
|
157
|
+
- `PageTabs` inside `PageContentSection`.
|
|
158
|
+
- Inline styles or Tailwind for page structure.
|
|
159
|
+
- Second `PageHeader` nested in content.
|
|
160
|
+
- `PageContent` without sections for multi-block pages (prefer multiple `PageContentSection`).
|
|
161
|
+
- Wrapping data widgets in extra layout divs with hard-coded padding.
|
|
162
|
+
|
|
163
|
+
## Future candidates (not implemented)
|
|
164
|
+
|
|
165
|
+
- **PageToolbar** — dedicated action row between header and tabs.
|
|
166
|
+
- **PageFullBleed** — edge-to-edge section escape hatch.
|
|
167
|
+
|
|
168
|
+
Do not add these locally; extend uilib when product needs them.
|
|
169
|
+
|
|
170
|
+
# SidebarDatasetsItemsGrouped
|
|
171
|
+
|
|
172
|
+
Renders: expandable sidebar groups of datasets (by target type, regions, or categories).
|
|
173
|
+
|
|
174
|
+
Use when: app shell needs grouped dataset navigation.
|
|
175
|
+
Not when: in-page or report content (no sidebar slot).
|
|
176
|
+
|
|
177
|
+
Host provides:
|
|
178
|
+
|
|
179
|
+
- `datasets` list (`SidebarDatasetsItemsGroupedDataset`)
|
|
180
|
+
- `groupBy`, `selectedDatasetId`, `onDatasetClick`
|
|
181
|
+
- Optional `preItems` / `postItems`, `defaultExpandedGroupNames`
|
|
182
|
+
|
|
183
|
+
Report tile: Not used in report tiles.
|
|
184
|
+
|
|
185
|
+
Requires: `datasets`; `groupBy`; `onDatasetClick` for navigation.
|
|
186
|
+
|
|
187
|
+
Empty/loading: empty `datasets` renders no groups; loading handled by host before pass-in.
|
|
188
|
+
|
|
189
|
+
# ChartAreaInteractive
|
|
190
|
+
|
|
191
|
+
Renders: time-series chart with historical line, forecast lines, optional pin / quantile-band / threshold overlays.
|
|
192
|
+
|
|
193
|
+
Use when: custom forecast UI with overlays or full chart control.
|
|
194
|
+
Not when: packaged performance or drivers-comparison views (use PerformanceChart or DriversComparisonChart).
|
|
195
|
+
|
|
196
|
+
Host provides:
|
|
197
|
+
|
|
198
|
+
- `chartData`, `forecastData` built from API
|
|
199
|
+
- `timeRange` / `onTimeRangeChange` or brush-only range
|
|
200
|
+
- Optional `mode`: pin | intervals | thresholds + overlay state
|
|
201
|
+
- Analysis selector and fetch outside widget
|
|
202
|
+
|
|
203
|
+
Report tile: `dataset_card` — host loads dataset + analysis; chart inside dashboard card.
|
|
204
|
+
|
|
205
|
+
Requires: `chartData`; `forecastData`; `loading`; `toggleLegendSeries` / `ensureAnalysisSeriesVisible` when legend is external.
|
|
206
|
+
|
|
207
|
+
Empty/loading: `loading`, `error`; empty data shows chart empty state via host message props.
|
|
208
|
+
|
|
209
|
+
# DriverMap
|
|
210
|
+
|
|
211
|
+
Renders: world map with regional driver badges, bottom strip for world-level drivers, selection highlight.
|
|
212
|
+
|
|
213
|
+
Use when: geographic driver exploration for one analysis.
|
|
214
|
+
Not when: driver detail metrics card alone (pair with DriverCard) or normalized series chart (use DriversComparisonChart).
|
|
215
|
+
|
|
216
|
+
Host provides:
|
|
217
|
+
|
|
218
|
+
- `drivers` as `DriverData[]` from analysis API
|
|
219
|
+
- Controlled `selectedDriver` + `setSelectedDriver`
|
|
220
|
+
- `isLoading` while fetching drivers
|
|
221
|
+
|
|
222
|
+
Report tile: `drivers_map` — tile resolves analysis id, fetches drivers, passes list + selection (see EmbeddedAnalysisSelector pattern).
|
|
223
|
+
|
|
224
|
+
Requires: `drivers`; `isLoading`; `selectedDriver`; `setSelectedDriver`.
|
|
225
|
+
|
|
226
|
+
Empty/loading: `isLoading` shows overlay; empty `drivers` leaves map without badges.
|
|
227
|
+
|
|
228
|
+
# PerformanceChart
|
|
229
|
+
|
|
230
|
+
Renders: forecast performance on ChartAreaInteractive — per-horizon tab (24m window, MAE/MAPE table) and spaghetti tab (all horizons overlaid).
|
|
231
|
+
|
|
232
|
+
Use when: dataset performance tab with horizon selector and error metrics.
|
|
233
|
+
Not when: simple forecast card or driver backtests — use ChartAreaInteractive or DriversComparisonChart.
|
|
234
|
+
|
|
235
|
+
Host provides:
|
|
236
|
+
|
|
237
|
+
- `performanceData` (PerformanceChartPayload) and `historicalData` from performance API
|
|
238
|
+
- Analysis selection and fetch outside widget
|
|
239
|
+
- Optional `forecastData`, `customPerformanceMatrix`, `userSeries` for spaghetti
|
|
240
|
+
|
|
241
|
+
Report tile: `performance_chart` — host loads performance payload + dataset series; built-in analysis selector.
|
|
242
|
+
|
|
243
|
+
Requires: `performanceData` — model/drift forecasts and metrics; `historicalData` — baseline series; `loading` / `chartLoading` / `performanceDataLoading` — spinners; `runAnalysisHint` / `statusHint` — empty states.
|
|
244
|
+
|
|
245
|
+
Empty/loading: loading props show shimmer/spinner; null `performanceData` with `runAnalysisHint` prompts to run analysis.
|
|
246
|
+
|
|
247
|
+
# DriversComparisonChart
|
|
248
|
+
|
|
249
|
+
Renders: target vs drivers backtests chart with ChartAreaInteractive plus table; row click toggles series visibility.
|
|
250
|
+
|
|
251
|
+
Use when: drivers comparison tab with normalized target and driver series from backtests payload.
|
|
252
|
+
Not when: geographic map or performance horizons — use DriverMap or PerformanceChart.
|
|
253
|
+
|
|
254
|
+
Host provides:
|
|
255
|
+
|
|
256
|
+
- `payload`: BacktestsComponentPayload from platform SDK (host fetch per analysis)
|
|
257
|
+
- Optional `datasetHistorical` overlay
|
|
258
|
+
- `seriesInitKey` when selected analysis changes
|
|
259
|
+
- `viewTab` / `onViewTabChange`: `lagged` (calendar-aligned, raw dates) or `overlapped` (driver series shifted backward by parsed lag months)
|
|
260
|
+
|
|
261
|
+
View tabs: host should render uilib `Tabs variant="button"` with **Lagged** / **Overlapped** in the toolbar (analysis selector left, tabs right). Chart applies `applyDriversComparisonViewToPayload` internally.
|
|
262
|
+
|
|
263
|
+
Report tile: `drivers_comparison_chart` — host loads normalized backtests payload + dataset historical; built-in analysis selector.
|
|
264
|
+
|
|
265
|
+
Requires: `payload` — target + driver normalized_series; `loading` / `chartLoading` — spinners; `seriesInitKey` — reset visible series on analysis or view tab change; `runAnalysisHint` / `statusHint` — empty/error text.
|
|
266
|
+
|
|
267
|
+
Lag column: **Lagged** shows API `lag` string (may be a range). **Overlapped** shows single `N month(s)` from `parseLagMonthsFromLabel` (range uses max month).
|
|
268
|
+
|
|
269
|
+
Historical window: lead-in is anchored to the **lagged** (unshifted) view (`computeDriversComparisonHistoricalWindowFloor`); overlapped tab shifts driver lines backward only — switching tabs does not change historical extent when floor is pinned.
|
|
270
|
+
|
|
271
|
+
Empty/loading: loading props shimmer chart; null `payload` with `runAnalysisHint` prompts to run analysis.
|
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
function normalizeTooltipPayload(payload) {
|
|
2
|
+
if (!payload?.length)
|
|
3
|
+
return undefined;
|
|
4
|
+
const seen = new Set();
|
|
5
|
+
const result = [];
|
|
6
|
+
for (const item of payload) {
|
|
7
|
+
if (item.value === null ||
|
|
8
|
+
item.value === undefined ||
|
|
9
|
+
item.type === 'none') {
|
|
10
|
+
continue;
|
|
11
|
+
}
|
|
12
|
+
const key = String(item.dataKey ?? item.name ?? result.length);
|
|
13
|
+
if (seen.has(key))
|
|
14
|
+
continue;
|
|
15
|
+
seen.add(key);
|
|
16
|
+
result.push({ ...item });
|
|
17
|
+
}
|
|
18
|
+
return result.length ? result : undefined;
|
|
19
|
+
}
|
|
1
20
|
function getPayloadConfigFromPayload(config, payload, key) {
|
|
2
21
|
if (typeof payload !== 'object' || payload === null) {
|
|
3
22
|
return config[key];
|
|
@@ -6,4 +25,4 @@ function getPayloadConfigFromPayload(config, payload, key) {
|
|
|
6
25
|
return config[key] || undefined;
|
|
7
26
|
}
|
|
8
27
|
|
|
9
|
-
export { getPayloadConfigFromPayload };
|
|
28
|
+
export { getPayloadConfigFromPayload, normalizeTooltipPayload };
|
|
@@ -5,6 +5,7 @@ import { getForecastColor, ChartLines } from '../../ChartAreaInteractive/ChartLi
|
|
|
5
5
|
import { Skeleton } from '../../Skeleton/Skeleton.js';
|
|
6
6
|
import { chartRenderQueue } from '../../../../utils/chartRenderQueue.js';
|
|
7
7
|
import { Tooltip, LineChart, ComposedChart } from 'recharts';
|
|
8
|
+
import { normalizeTooltipPayload } from '../Chart.helpers.js';
|
|
8
9
|
import { resolveChartMargin, getPlotViewBox } from '../tools/chartPlotGeometry.js';
|
|
9
10
|
import { formatDate } from '../tools/formatters.js';
|
|
10
11
|
import S from './BaseChartWrapper.styl.js';
|
|
@@ -165,7 +166,7 @@ const BaseChartWrapperContent = forwardRef((props, ref) => {
|
|
|
165
166
|
const renderTooltipContent = (props) => {
|
|
166
167
|
// Filter payload to exclude items with null/undefined values
|
|
167
168
|
// This prevents showing stale data when hovering on dates without data points
|
|
168
|
-
const filteredPayload = props.payload
|
|
169
|
+
const filteredPayload = normalizeTooltipPayload(props.payload);
|
|
169
170
|
// If no valid payload items, render ChartTooltipContent with active=false and empty payload
|
|
170
171
|
// This allows ChartTooltipContent to clear its lastTooltipData state
|
|
171
172
|
if (!filteredPayload || filteredPayload.length === 0) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
2
2
|
import cn from 'classnames';
|
|
3
|
-
import { useState,
|
|
3
|
+
import { useState, useMemo, useEffect } from 'react';
|
|
4
4
|
import { useChart } from '../Chart.context.js';
|
|
5
|
-
import { getPayloadConfigFromPayload } from '../Chart.helpers.js';
|
|
5
|
+
import { normalizeTooltipPayload, getPayloadConfigFromPayload } from '../Chart.helpers.js';
|
|
6
6
|
import S from '../Chart.styl.js';
|
|
7
7
|
import { ChartTooltipItem } from './ChartTooltipItem.js';
|
|
8
8
|
|
|
@@ -10,28 +10,31 @@ function ChartTooltipContent({ active, className, indicator = 'dot', hideLabel =
|
|
|
10
10
|
const { config } = useChart();
|
|
11
11
|
// Keep last tooltip data in state to maintain position when inactive
|
|
12
12
|
const [lastTooltipData, setLastTooltipData] = useState(null);
|
|
13
|
+
const normalizedPayload = useMemo(() => normalizeTooltipPayload(payload), [payload]);
|
|
13
14
|
// Update last tooltip data when active
|
|
14
15
|
useEffect(() => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
16
|
+
setLastTooltipData(prev => {
|
|
17
|
+
// Clear when label changed (prevents stale data)
|
|
18
|
+
let next = prev;
|
|
19
|
+
if (prev && label && prev.label !== label) {
|
|
20
|
+
next = null;
|
|
21
|
+
}
|
|
22
|
+
if (active && normalizedPayload?.length) {
|
|
23
|
+
return {
|
|
24
|
+
active: true,
|
|
25
|
+
payload: normalizedPayload,
|
|
26
|
+
label,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
// Keep frozen snapshot when inactive (position maintenance in BaseChartWrapper)
|
|
30
|
+
if (!active && next) {
|
|
31
|
+
return { ...next, active: false };
|
|
32
|
+
}
|
|
33
|
+
return next;
|
|
34
|
+
});
|
|
35
|
+
}, [active, normalizedPayload, label]);
|
|
36
|
+
const displayPayload = active && normalizedPayload?.length
|
|
37
|
+
? normalizedPayload
|
|
35
38
|
: lastTooltipData?.payload;
|
|
36
39
|
const displayLabel = active ? label : lastTooltipData?.label;
|
|
37
40
|
const tooltipLabel = useMemo(() => {
|