@altertable/data-app 0.64.0 → 0.66.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/AGENTS.md +40 -0
  2. package/CONTRIBUTING.md +37 -3
  3. package/README.md +2 -2
  4. package/dist/chunks/{contract-mwe7gnmh.js → contract-13j5zb3c.js} +110 -216
  5. package/dist/chunks/contract-13j5zb3c.js.map +13 -0
  6. package/dist/chunks/contract-19ckn3n7.js +78 -0
  7. package/dist/chunks/contract-19ckn3n7.js.map +10 -0
  8. package/dist/chunks/contract-awa2d5b1.js +272 -0
  9. package/dist/chunks/contract-awa2d5b1.js.map +12 -0
  10. package/dist/chunks/{contract-xtxza1fy.js → contract-e11m4y7f.js} +35 -71
  11. package/dist/chunks/contract-e11m4y7f.js.map +12 -0
  12. package/dist/chunks/{contract-zr3jd7mr.js → contract-h8a6f559.js} +100 -75
  13. package/dist/chunks/contract-h8a6f559.js.map +12 -0
  14. package/dist/chunks/contract-havnjmdr.js +13011 -0
  15. package/dist/chunks/contract-havnjmdr.js.map +99 -0
  16. package/dist/chunks/{contract-txjy0en2.js → contract-m6n8ctfc.js} +64 -72
  17. package/dist/chunks/contract-m6n8ctfc.js.map +10 -0
  18. package/dist/chunks/contract-pk603qj7.js +146 -0
  19. package/dist/chunks/contract-pk603qj7.js.map +10 -0
  20. package/dist/chunks/{contract-yxbjea23.js → contract-rfbakka4.js} +179 -2
  21. package/dist/chunks/contract-rfbakka4.js.map +13 -0
  22. package/dist/client/index.js +11 -6
  23. package/dist/client/index.js.map +1 -1
  24. package/dist/core/appearance.js +1 -1
  25. package/dist/core/contract.js +13 -3
  26. package/dist/core/contract.js.map +1 -1
  27. package/dist/embed/index.js +25 -13
  28. package/dist/embed/index.js.map +3 -3
  29. package/dist/local.js +1 -1
  30. package/dist/local.js.map +2 -2
  31. package/dist/react/embed/index.js +5 -2
  32. package/dist/react/embed/index.js.map +3 -3
  33. package/dist/react/index.js +2532 -11273
  34. package/dist/react/index.js.map +16 -88
  35. package/dist/react/ui/index.js +1614 -0
  36. package/dist/react/ui/index.js.map +22 -0
  37. package/dist/server.js +1 -1
  38. package/dist/server.js.map +2 -2
  39. package/dist/types/client/annotations.d.ts +23 -0
  40. package/dist/types/client/iframe.d.ts +2 -0
  41. package/dist/types/client/index.d.ts +2 -0
  42. package/dist/types/client/logger.d.ts +6 -0
  43. package/dist/types/core/annotations.d.ts +92 -0
  44. package/dist/types/core/appearance.d.ts +2 -2
  45. package/dist/types/core/bridge-endpoint.d.ts +21 -0
  46. package/dist/types/core/bridge.d.ts +137 -16
  47. package/dist/types/core/contract.d.ts +2 -0
  48. package/dist/types/core/logger.d.ts +12 -0
  49. package/dist/types/core/presentation.d.ts +2 -0
  50. package/dist/types/core/variables.d.ts +2 -5
  51. package/dist/types/embed/host.d.ts +6 -2
  52. package/dist/types/embed/index.d.ts +1 -0
  53. package/dist/types/embed/source.d.ts +1 -1
  54. package/dist/types/react/annotations/AnnotationBar.d.ts +24 -0
  55. package/dist/types/react/annotations/AnnotationControls.d.ts +10 -0
  56. package/dist/types/react/annotations/AnnotationEditor.d.ts +15 -0
  57. package/dist/types/react/annotations/AnnotationMarkers.d.ts +19 -0
  58. package/dist/types/react/annotations/AnnotationSelectionLayer.d.ts +12 -0
  59. package/dist/types/react/annotations/AnnotationTarget.d.ts +9 -0
  60. package/dist/types/react/annotations/AnnotationTooltip.d.ts +9 -0
  61. package/dist/types/react/annotations/AnnotationTrigger.d.ts +10 -0
  62. package/dist/types/react/annotations/annotation-editor-state.d.ts +56 -0
  63. package/dist/types/react/annotations/annotation-screenshot.d.ts +4 -0
  64. package/dist/types/react/annotations/annotation-targets.d.ts +33 -0
  65. package/dist/types/react/annotations/getAnnotationProps.d.ts +15 -0
  66. package/dist/types/react/annotations/styles.d.ts +3 -0
  67. package/dist/types/react/annotations/useAnnotationGeometry.d.ts +22 -0
  68. package/dist/types/react/annotations/useDataAppAnnotations.d.ts +19 -0
  69. package/dist/types/react/bindings.d.ts +110 -0
  70. package/dist/types/react/content.d.ts +24 -11
  71. package/dist/types/react/embed/bridge.d.ts +1 -1
  72. package/dist/types/react/hooks.d.ts +29 -787
  73. package/dist/types/react/index.d.ts +30 -104
  74. package/dist/types/react/source-owner.d.ts +2 -0
  75. package/dist/types/react/style-contract.d.ts +60 -0
  76. package/dist/types/react/style-validation.d.ts +2 -0
  77. package/dist/types/react/ui/AboutData.d.ts +9 -1
  78. package/dist/types/react/ui/AppHeader.d.ts +1 -2
  79. package/dist/types/react/ui/AppLayout.d.ts +3 -9
  80. package/dist/types/react/ui/AppToolbar.d.ts +6 -7
  81. package/dist/types/react/ui/AreaChart.d.ts +7 -0
  82. package/dist/types/react/ui/BarChart.d.ts +6 -0
  83. package/dist/types/react/ui/{ComparisonVisual.d.ts → Comparison.d.ts} +3 -3
  84. package/dist/types/react/ui/ContentSkeleton.d.ts +2 -0
  85. package/dist/types/react/ui/DataApp.d.ts +17 -53
  86. package/dist/types/react/ui/DataAppFrame.d.ts +42 -0
  87. package/dist/types/react/ui/DataBoundary.d.ts +4 -5
  88. package/dist/types/react/ui/DataSection.d.ts +6 -18
  89. package/dist/types/react/ui/DataSectionBoundary.d.ts +30 -0
  90. package/dist/types/react/ui/DataValue.d.ts +9 -0
  91. package/dist/types/react/ui/DataWidget.d.ts +2 -1
  92. package/dist/types/react/ui/DateTimeTooltip.d.ts +1 -1
  93. package/dist/types/react/ui/ExportControl.d.ts +4 -0
  94. package/dist/types/react/ui/HelpPopover.d.ts +3 -4
  95. package/dist/types/react/ui/InspectionContext.d.ts +2 -0
  96. package/dist/types/react/ui/InspectionProvider.d.ts +29 -0
  97. package/dist/types/react/ui/LineChart.d.ts +7 -0
  98. package/dist/types/react/ui/MetricWidget.d.ts +4 -3
  99. package/dist/types/react/ui/PieChart.d.ts +8 -0
  100. package/dist/types/react/ui/PresentStory.d.ts +2 -5
  101. package/dist/types/react/ui/RefreshControl.d.ts +1 -2
  102. package/dist/types/react/ui/ScatterChart.d.ts +17 -0
  103. package/dist/types/react/ui/SearchField.d.ts +1 -2
  104. package/dist/types/react/ui/SearchInput.d.ts +3 -1
  105. package/dist/types/react/ui/Sheet.d.ts +2 -1
  106. package/dist/types/react/ui/Skeleton.d.ts +5 -2
  107. package/dist/types/react/ui/StaticDataApp.d.ts +4 -0
  108. package/dist/types/react/ui/TableWidget.d.ts +19 -16
  109. package/dist/types/react/ui/Tabs.d.ts +6 -2
  110. package/dist/types/react/ui/TextWidget.d.ts +2 -1
  111. package/dist/types/react/ui/Toast.d.ts +8 -0
  112. package/dist/types/react/ui/Tooltip.d.ts +4 -3
  113. package/dist/types/react/ui/TrendChart.d.ts +5 -0
  114. package/dist/types/react/ui/UpdatedAt.d.ts +1 -1
  115. package/dist/types/react/ui/VisualizationWidget.d.ts +12 -13
  116. package/dist/types/react/ui/WidgetViewTabs.d.ts +1 -1
  117. package/dist/types/react/ui/chart-data.d.ts +25 -0
  118. package/dist/types/react/ui/csv-export.d.ts +19 -0
  119. package/dist/types/react/ui/data-context.d.ts +18 -7
  120. package/dist/types/react/ui/icons.d.ts +2 -0
  121. package/dist/types/react/ui/index.d.ts +74 -0
  122. package/dist/types/react/ui/metric.d.ts +1 -1
  123. package/dist/types/react/ui/presentation.d.ts +5 -1
  124. package/dist/types/react/ui/shortcuts.d.ts +7 -1
  125. package/dist/types/react/view-runtime.d.ts +29 -0
  126. package/dist/types/react/view.d.ts +21 -4
  127. package/dist/types/react/widgets.d.ts +79 -0
  128. package/dist/worker.js +381 -79
  129. package/docs/app-authoring.md +64 -30
  130. package/docs/data-context.md +89 -0
  131. package/docs/embed.md +1 -1
  132. package/docs/formatting-and-appearance.md +52 -0
  133. package/docs/hosted-apps.md +2 -15
  134. package/docs/layout.md +33 -0
  135. package/docs/local-data-apps.md +1 -1
  136. package/docs/react-embed.md +2 -2
  137. package/docs/react.md +20 -254
  138. package/docs/stories-and-export.md +52 -0
  139. package/docs/styling.md +55 -0
  140. package/docs/ui-quality.md +20 -0
  141. package/docs/ui.md +41 -0
  142. package/docs/variables.md +98 -0
  143. package/docs/views.md +127 -0
  144. package/docs/widgets.md +142 -0
  145. package/examples/starter-data-app/index.tsx +89 -44
  146. package/package.json +13 -4
  147. package/dist/chunks/contract-cxr9t12b.js +0 -18
  148. package/dist/chunks/contract-cxr9t12b.js.map +0 -10
  149. package/dist/chunks/contract-mwe7gnmh.js.map +0 -13
  150. package/dist/chunks/contract-txjy0en2.js.map +0 -10
  151. package/dist/chunks/contract-xtxza1fy.js.map +0 -12
  152. package/dist/chunks/contract-yxbjea23.js.map +0 -12
  153. package/dist/chunks/contract-zr3jd7mr.js.map +0 -12
  154. package/dist/types/react/ui/RefreshRegion.d.ts +0 -9
  155. package/dist/types/react/ui/SelectableBarChart.d.ts +0 -15
  156. package/docs/releasing.md +0 -79
  157. /package/dist/types/react/ui/{comparison.d.ts → metric-comparison.d.ts} +0 -0
package/docs/ui.md ADDED
@@ -0,0 +1,41 @@
1
+ # Direct UI composition
2
+
3
+ Import from `@altertable/data-app/react/ui` for setup/static screens, custom
4
+ controls, or a deliberately custom shell. Standard data apps use declared views
5
+ and bound widgets from `/react`.
6
+
7
+ `<DataApp>` here renders a static screen and accepts optional direct CSV data.
8
+ Direct widget forms accept local values or rows. `<DataWidget>` supplies a generic
9
+ frame for deliberately custom content. Use `<GettingStarted>` for
10
+ connection setup. Fetched data belongs to a declared view; avoid inventing
11
+ request states to populate a shell.
12
+
13
+ Direct `<DateRangePicker>`, `<DimensionPicker>`, and `useAppVariables()` support
14
+ custom control ownership. The caller supplies values and change handlers.
15
+ Custom tables use `<DataTable>` and its cell helpers; controls and overlays
16
+ include `<SearchField>`, `<Combobox>`, `<Tabs>`, `<Sheet>`, and `<HelpPopover>`.
17
+
18
+ Standalone `<AboutData>`, glossary components, and `<PresentStory>` support
19
+ custom inspection and presentation. Supply registered context and evidence, and
20
+ derive findings from the displayed data. Standard widgets and `<DataApp>`
21
+ already own these experiences.
22
+
23
+ For framework mounting, `<DataAppProvider>` supplies shared requests and one inspection sheet. Wrap custom shells in it.
24
+ Call `injectDataAppStyles()` before mounting either entry; imports do not install
25
+ styles.
26
+
27
+ ## Charts
28
+
29
+ Choose a chart using the [visualization guide](widgets.md#choose-a-visualization).
30
+ Import `<BarChart>`, `<LineChart>`, `<AreaChart>`, `<PieChart>`, or `<ScatterChart>` from
31
+ `/react/ui`. Compose the visual inside `<VisualizationWidget dataset={dataset} source={result}>`
32
+ so rows, loading state, evidence, and inspection come from that dataset.
33
+ Pass ordered items with unique, nonblank IDs and finite numbers. Bar and pie
34
+ values must be nonnegative. Use `formatValue` for domain formatting.
35
+
36
+ Line and area charts show equally spaced samples; include missing periods in the
37
+ input. These charts space items evenly, so they do not represent irregular time
38
+ intervals. Distinguish a missing observation from measured zero when preparing
39
+ the samples. Pie slices represent mutually exclusive parts of one total; shares
40
+ use the sum of supplied items, so include Other when showing a subset of the
41
+ whole. Scatter points represent independent X/Y observations.
@@ -0,0 +1,98 @@
1
+ # Variables and filters
2
+
3
+ ## Time views and field filters
4
+
5
+ `createDataHooks(client).defineTimeView()` owns the `period` variable, calendar
6
+ controls, and displayed-period label. Declare `time: { contract, defaultValue }`,
7
+ an operation, `isEmpty` (a predicate), and `emptyFallback` (a title and optional
8
+ description for an empty result). With no additional variables, its default
9
+ input is the calendar request. With additional variables, it is `{ period, ...variables }`.
10
+ Supply an `input` mapper for a different operation shape and `bindings` to extract
11
+ nested period or field-filter inputs. Mappings must preserve the selected values.
12
+
13
+ `dimensionFilter()` from `/contract` requires exactly one option source: fixed `options` or a `facet`.
14
+ Use `defineFacetFilter()` to bind a facet operation and its typed input. `<DimensionPicker>` offers missing values separately and keeps selected values
15
+ available when they have zero matches.
16
+
17
+ ## Declare controls once
18
+
19
+ Use `textVariable()`, `selectVariable()`, and `dateRangeVariable()` in a view's
20
+ `variables`. `<DataApp view={view}>` generates their controls and keeps values in the URL.
21
+ Keep local search out of the operation input when it only filters loaded rows.
22
+
23
+ The [date range contract](contract.md#shared-date-ranges) owns source coverage,
24
+ maximum range, time zone, and comparison rules. `defineTimeView()` is the compact
25
+ path for a view whose primary input is a period. Use `defineDataView()` for fixed
26
+ snapshots or other input shapes.
27
+
28
+ ```ts
29
+ const activity = defineTimeView({
30
+ dataContext,
31
+ operation: 'activity',
32
+ time: { contract: calendar, defaultValue: { kind: 'preset', id: 'last-7' } },
33
+ variables: { region: textVariable({ key: 'region' }) },
34
+ input: values => ({
35
+ filters: { period: values.period, region: values.region },
36
+ }),
37
+ bindings: {
38
+ period: input => input.filters.period,
39
+ region: input => input.filters.region,
40
+ },
41
+ isEmpty: data => data.rows.length === 0,
42
+ emptyFallback: { title: 'No activity' },
43
+ });
44
+ ```
45
+
46
+ The operation, calendar, and row shape are app-owned. Bindings must extract
47
+ selected values unchanged.
48
+
49
+ ## URL state and custom controls
50
+
51
+ Relative date presets remain relative. Explicit dates use `start`, `end`, and
52
+ `compare` for the `period` variable; other date variables derive these keys from
53
+ their own key. Invalid URL values fall back to validated defaults. Do not use
54
+ reserved inspection, presentation, or navigation keys for app variables.
55
+
56
+ Choose push history for meaningful selections and replace history for typing.
57
+ Back/Forward restores the same values and controls. Use generated controls for
58
+ standard data apps. Direct pickers and standalone URL state belong to
59
+ [direct UI composition](ui.md).
60
+
61
+ ## Fixed options and query-backed facets
62
+
63
+ Declare fixed choices with `dimensionFilter()` from `/contract`:
64
+
65
+ ```ts
66
+ const region = dimensionFilter({
67
+ key: 'region',
68
+ label: 'Region',
69
+ valueType: 'string',
70
+ selection: 'multiple',
71
+ options: [{ value: 'Europe', label: 'Europe' }],
72
+ });
73
+ ```
74
+
75
+ For query-backed choices use the hooks factory's `defineFacetFilter()`. The
76
+ facet operation returns parsed dimension options. Its input mapper reads current
77
+ resolved variables, including other filters:
78
+
79
+ ```ts
80
+ const { defineFacetFilter, defineTimeView } = createDataHooks(client);
81
+ const region = defineFacetFilter({
82
+ key: 'region',
83
+ label: 'Region',
84
+ valueType: 'string',
85
+ selection: 'multiple',
86
+ facet: {
87
+ operation: 'regions',
88
+ input: values => ({ period: values.period as DateRangeRequest }),
89
+ },
90
+ });
91
+ ```
92
+
93
+ The client registry defines the `regions` operation and its typed period input;
94
+ import `DateRangeRequest` from `/contract`. Put `region` in the view's `variables`
95
+ alongside its period. Use `parseFacetOptions()` to validate facet results, and
96
+ `dimensionPredicate()` to build a bounded SQL filter from parsed selections.
97
+ For the operation's input parser, use `parseDimensionSelection(value, region)` from `/contract`.
98
+ A dimension without an explicit selection represents all members.
package/docs/views.md ADDED
@@ -0,0 +1,127 @@
1
+ # Views and displayed results
2
+
3
+ ## Bind a view
4
+
5
+ ```tsx
6
+ import { createDataClient } from '@altertable/data-app/client';
7
+ import {
8
+ createDataHooks,
9
+ DataApp,
10
+ DataSection,
11
+ } from '@altertable/data-app/react';
12
+ import type { operations } from '#app/operations.ts';
13
+
14
+ const client = createDataClient<typeof operations>();
15
+ const { defineDataView } = createDataHooks(client);
16
+ const activityView = defineDataView({
17
+ dataContext,
18
+ operation: 'activity',
19
+ describeInput: () => 'all activity',
20
+ isEmpty: data => data.rows.length === 0,
21
+ emptyFallback: { title: 'No activity' },
22
+ });
23
+ const content = activityView.content(result => (
24
+ <ActivityWidgets source={result} />
25
+ ));
26
+
27
+ function App() {
28
+ return (
29
+ <DataApp
30
+ config={config}
31
+ view={activityView}
32
+ story={story}
33
+ datasets={[activityDataset]}
34
+ >
35
+ <DataSection content={content} />
36
+ </DataApp>
37
+ );
38
+ }
39
+ ```
40
+
41
+ Omit `variables` when there are no controls. Omit `input` when the resolved
42
+ variables match the operation input; nested or different inputs need a mapper.
43
+ The operation and `ActivityWidgets` are app-owned. The bound widgets derive
44
+ loading content from their source; see [widgets](widgets.md) and the
45
+ [complete starter](../examples/starter-data-app/index.tsx). Define
46
+ [filters](variables.md), [context and evidence](data-context.md), and
47
+ [story and datasets](stories-and-export.md) for the app's question.
48
+
49
+ Render static text immediately; skeletonize only dynamic content. Keep section
50
+ introductions outside request boundaries so they remain visible on empty and
51
+ error states.
52
+
53
+ Use `<DataSection>` for each independently fetched subtree and `view.content()`
54
+ to share its loading and ready layout. Refreshes retain displayed content.
55
+
56
+ Use `<DataValue>` for a dynamic value within static prose:
57
+
58
+ ```tsx
59
+ <p>
60
+ Orders in the last 7 days: <DataValue metric={weeklyOrders} source={result} />
61
+ </p>
62
+ ```
63
+
64
+ Bind the whole sentence with `<TextWidget>` when its wording depends on the result.
65
+ Use `<DataValue scope={result.scope} />` for the displayed scope label.
66
+
67
+ Use the same [date range contract](contract.md#shared-date-ranges) for the
68
+ operation and its view. For nested inputs, bind the range with
69
+ `input: input => input.period`.
70
+
71
+ Use `result.scope` inside `view.content()` as a reading for scope text. In stories and exports,
72
+ `view.scope(snapshot)` returns the same label from the displayed input, using
73
+ `describeInput` or the view's date label.
74
+
75
+ Declare standard controls in the view's [variables](variables.md). Keep
76
+ local-only filters out of the operation's `input`.
77
+
78
+ For large tables, use query-backed pagination with a stable sort and total
79
+ count. Client pagination and search cover only the rows already returned.
80
+
81
+ Declare evidence references on `view.dataset()` for charts and tables. `<MetricWidget>` uses
82
+ its metric definition for the label, format, and evidence; keep view-specific
83
+ descriptions on the widget.
84
+
85
+ `<MetricWidget>` and `<Comparison>` share a metric reading. Comparisons
86
+ follow the displayed result's range.
87
+
88
+ Use the [format helpers](formatting-and-appearance.md) for metric formats and values in tables,
89
+ charts, and custom views.
90
+
91
+ ## Preserve displayed results
92
+
93
+ The callback in `view.content()` receives a displayed source with `loading`,
94
+ `data`, `input`, and `scope`. Bind widgets to that source. Its input remains the
95
+ one that produced the visible data during refreshes and failures.
96
+
97
+ Keep the client stable across renders; create it outside the component.
98
+
99
+ ## Request ownership
100
+
101
+ `<DataApp>` owns the toolbar, controls, inspection, and page feedback for its
102
+ primary `view`. It always renders children, so introductions and static
103
+ context remain visible while a section loads. It requires both
104
+ [story and CSV export](stories-and-export.md) for a data request.
105
+
106
+ Place `<DataSection content={content}>` around each independently fetched subtree.
107
+ Declare its shared loading and ready layout with `view.content()`. The section
108
+ inherits empty copy from its view, with an optional local override, and handles
109
+ initial errors and retries. Primary content shares the app's displayed result;
110
+ independent content uses its own context and executed queries.
111
+
112
+ Keep related datasets in one view so export and story share a coherent snapshot.
113
+
114
+ | State | Display |
115
+ | --------------- | --------------------------------------------------------- |
116
+ | Initial loading | The authored loading fallback; selectors do not run |
117
+ | Initial error | A source-aware error with retry when recovery is possible |
118
+ | Empty result | The authored empty fallback |
119
+ | Ready | Data and the input that produced it |
120
+ | Updating | The prior result and its original input, fully readable |
121
+ | Failed refresh | The prior result and its evidence, with recovery feedback |
122
+
123
+ `isEmpty` is app-owned. A measured zero is valid data unless the analysis explicitly
124
+ says otherwise. Use the displayed input in content, export, and story callbacks for scope labels.
125
+
126
+ `<DataApp>` owns request execution, refresh, and cancellation. Keep raw request
127
+ state out of app code.
@@ -0,0 +1,142 @@
1
+ # Widgets and narrative
2
+
3
+ ## Choose a widget
4
+
5
+ Metrics, tables, and visualizations from `/react` consume view bindings and a source. For static displays, use
6
+ [direct UI composition](ui.md).
7
+ Manage requests in the [view and section](views.md); show skeletons for loading
8
+ readings without inventing values. Use `isEmpty` for the predicate and
9
+ `emptyFallback` for empty-result copy.
10
+
11
+ | Need | Component |
12
+ | ------------------------------------------------ | ----------------------- |
13
+ | Metric with formatting, comparison, and evidence | `<MetricWidget>` |
14
+ | Custom chart or alternate chart views | `<VisualizationWidget>` |
15
+ | Defined columns, local search, and pagination | `<TableWidget>` |
16
+ | Narrative panel | `<TextWidget>` |
17
+ | Borderless explanatory prose | `<TextContent>` |
18
+ | One data-dependent phrase within static prose | `<DataValue>` |
19
+
20
+ Use [layout](layout.md) for `<Stack>`, `<Grid>`, and responsive `<GridItem>` spans.
21
+ `<Skeleton>` supports custom fallback content; `/react/ui` supplies
22
+ `<DataAppSkeleton>` for iframe startup.
23
+
24
+ ## Choose a visualization
25
+
26
+ Choose the component by the reader's question. Compose dataset visuals inside
27
+ `<VisualizationWidget>`; use `<MetricWidget>` for a headline value. Built-in
28
+ charts come from `/react/ui`; `<Ranking>`, `<Breakdown>`, and `<Comparison>`
29
+ are also available from `/react`.
30
+
31
+ | Question or need | Component | Guidance |
32
+ | --------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------ |
33
+ | How do categories compare? | `<BarChart>` | Compare nonnegative category values; order by value for rank or by a meaningful category order. |
34
+ | Which items lead? | `<Ranking>` | Use a compact ordered list with values and optional detail. Tracks scale to the largest visible item, not a total. |
35
+ | How does a measure change over time? | `<LineChart>` | Use ordered, equally spaced samples to emphasize the trend. |
36
+ | How large is the measure over time? | `<AreaChart>` | Use the same samples as a line chart when filled magnitude relative to zero helps answer the question. |
37
+ | What makes up the whole? | `<PieChart>` | Use a few mutually exclusive parts of one total; include the remainder as Other. |
38
+ | What share of an observed total does each part represent? | `<Breakdown>` | Use a compact display of values and shares with an explicit total, including when only some parts are shown. |
39
+ | How are two numeric measures related? | `<ScatterChart>` | Use independent X/Y observations to explore relationships, clusters, and outliers. |
40
+ | How did one metric change between periods? | `<Comparison>` | Use the metric reading and its displayed comparison period. |
41
+ | What are the exact values or row details? | `<TableWidget>` | Use a bound table for lookup, search, and precise comparisons. |
42
+
43
+ Prefer bars or a ranking when readers need to compare similarly sized categories;
44
+ use a pie for a simple composition question. Prefer a line when the trend is
45
+ enough; use area when the fill adds meaning. Keep exact values available in a
46
+ table when they matter. Follow the [chart input constraints](ui.md#charts).
47
+
48
+ Use widget `views` when a chart and another representation answer the same
49
+ question from the same dataset.
50
+
51
+ ## Declare datasets and metrics
52
+
53
+ Bind reusable selections to the view. Declare evidence references directly;
54
+ the view validates them against its own context. A dataset's columns supply both formatted
55
+ table cells and raw CSV values. Widgets derive their readings, evidence, and empty fallback from the binding.
56
+
57
+ ```tsx
58
+ const countries = activityView.dataset({
59
+ name: 'Countries',
60
+ select: data => data.countries,
61
+ rowKey: row => row.country,
62
+ evidence: { id: 'countries', glossaryIds: ['identities'] },
63
+ columns: {
64
+ country: { value: row => row.country },
65
+ count: { value: row => row.count, format: { kind: 'count' } },
66
+ },
67
+ });
68
+ const total = activityView.metric(
69
+ {
70
+ id: 'tracked-identities',
71
+ glossaryId: 'identities',
72
+ format: { kind: 'count' },
73
+ },
74
+ data => ({
75
+ current: data.count,
76
+ previous: data.previousCount,
77
+ })
78
+ );
79
+ const content = activityView.content(result => (
80
+ <Grid columns={2}>
81
+ <MetricWidget metric={total} source={result} />
82
+ <TableWidget dataset={countries} source={result} />
83
+ <VisualizationWidget dataset={countries} source={result}>
84
+ {rows => <CountryChart rows={rows} />}
85
+ </VisualizationWidget>
86
+ </Grid>
87
+ ));
88
+ ```
89
+
90
+ Column keys supply unique IDs and readable default labels. Use `label` for a
91
+ specific display/export header. Declare selectors, raw value accessors, and
92
+ row-key callbacks explicitly; row keys must be stable and unique.
93
+ Metric comparisons require a
94
+ view date binding. `read()` returns a loading-aware value for custom prose.
95
+ Pass the binding and `source={snapshot}` to reuse a widget in a story. Use the
96
+ source supplied by that binding's own view; reconstructed or foreign sources
97
+ are rejected before selection.
98
+ Selectors do not run during loading. Table search, pagination, descriptions, and
99
+ actions remain local choices. For custom cells, use `format: row => ...`; CSV
100
+ still uses the raw `value` accessor. Datasets default to “No results”; supply
101
+ `emptyFallback` for specific copy. See [export](stories-and-export.md).
102
+
103
+ ## Narrative text
104
+
105
+ Use `<TextContent>` for prose within a page or custom layout, and `<TextWidget>`
106
+ when the explanation belongs in a titled panel alongside other widgets.
107
+
108
+ Give text a purpose: frame the question, explain how to interpret a comparison,
109
+ qualify a finding, or suggest what to explore next. Choose the content for the
110
+ reader's question.
111
+
112
+ For data-dependent text, pass `metric` or `dataset` and `source` to `<TextWidget>`.
113
+ It derives its title and evidence. A metric formats its current value by default;
114
+ use a child renderer for authored prose. Dataset renderers receive displayed rows,
115
+ including an empty selection, so they can explain zero activity.
116
+
117
+ Use `<DataValue metric={total} source={result} />` for a formatted value inside
118
+ static prose, or pass a dataset and row renderer. Its default loading fallback is
119
+ an inline skeleton. Use `<DataValue scope={result.scope} />` for a scope label.
120
+ All selectors use the displayed source; loading selectors do not run.
121
+ Use `<TextContent>` for static instructions; local filters should feed the same
122
+ filtered data to the text and its related visualization.
123
+
124
+ ## Custom visuals and controls
125
+
126
+ `<Ranking>`, `<Breakdown>`, and `<Comparison>`
127
+ compose inside authored widgets. Use [built-in charts](ui.md#charts) from `/react/ui`
128
+ inside `<VisualizationWidget dataset={dataset} source={result}>` for displayed rows.
129
+ The dataset owns evidence and empty copy; an empty row selection shows that fallback. A visualization's `views` declaration keeps
130
+ alternate-view selection shared with its inspection sheet. Give views stable IDs
131
+ and keep interactive state above the widget, since inspection may render it again.
132
+
133
+ `<TableWidget>` searches and paginates only the supplied rows. Use query-backed
134
+ pagination with a stable sort and total count for larger datasets. The `limit`
135
+ mode shows a bounded preview without local pagination. Custom tables can use
136
+ `<DataTable>`, `<DataTableEmptyRow>`, `<DataTableTimestamp>`, and `<DataTableShare>`.
137
+
138
+ For custom controls, tables, and overlays, see [direct UI composition](ui.md).
139
+ For embedded theme and chrome, see [parent presentation](embed.md#parent-presentation).
140
+
141
+ `<TooltipProvider>` shares hover timing outside `<DataApp>`; the app already
142
+ provides it.
@@ -1,3 +1,4 @@
1
+ import { formatMetric } from '@altertable/data-app/format';
1
2
  import { createDataClient } from '@altertable/data-app/client';
2
3
  import type { DataAppConfig } from '@altertable/data-app/config';
3
4
  import {
@@ -9,6 +10,12 @@ import {
9
10
  createDataContext,
10
11
  createDataHooks,
11
12
  DataApp,
13
+ DataSection,
14
+ DataValue,
15
+ TableWidget,
16
+ Grid,
17
+ Stack,
18
+ TextContent,
12
19
  injectDataAppStyles,
13
20
  mountDataApp,
14
21
  MetricWidget,
@@ -90,68 +97,106 @@ const sampleDataContext = createDataContext(queryNames)({
90
97
  },
91
98
  },
92
99
  });
93
- const { defineDataView, useView } = createDataHooks(
94
- createDataClient({ operations })
95
- );
100
+ const { defineDataView } = createDataHooks(createDataClient({ operations }));
96
101
  const sampleCountsView = defineDataView({
102
+ dataContext: sampleDataContext,
97
103
  operation: 'sampleCountsByGroup',
98
104
  variables: {
99
105
  groupName: textVariable({ key: 'group', label: 'Group', defaultValue: '' }),
100
106
  },
101
- input: ({ groupName }) => ({ groupName }),
102
107
  describeInput: ({ groupName }) =>
103
108
  groupName ? `group ${groupName}` : 'all groups',
104
109
  isEmpty: sampleCounts => sampleCounts.length === 0,
105
- empty: {
110
+ emptyFallback: {
106
111
  title: 'No matching groups',
107
112
  description: 'Try Alpha, Beta, or clear the group filter.',
108
113
  },
109
114
  });
115
+ const sampleCounts = sampleCountsView.dataset({
116
+ name: 'Sample counts',
117
+ select: rows => rows,
118
+ rowKey: row => row.groupName,
119
+ columns: {
120
+ groupName: { label: 'Group', value: row => row.groupName },
121
+ sampleCount: { value: row => row.sampleCount, format: { kind: 'count' } },
122
+ },
123
+ evidence: {
124
+ id: 'counts-by-group',
125
+ glossaryIds: ['sampleCount'],
126
+ },
127
+ });
128
+ const totalSamples = sampleCountsView.metric(
129
+ {
130
+ id: 'total-samples',
131
+ glossaryId: 'sampleCount',
132
+ label: 'Total samples',
133
+ format: { kind: 'count' },
134
+ },
135
+ rows => ({ current: rows.reduce((sum, row) => sum + row.sampleCount, 0) })
136
+ );
137
+ const sampleContent = sampleCountsView.content(result => (
138
+ <Stack aria-label="Sample results">
139
+ <TextContent>
140
+ <p>
141
+ Showing <DataValue scope={result.scope} />
142
+ </p>
143
+ </TextContent>
144
+ <Grid columns={2}>
145
+ <MetricWidget
146
+ metric={totalSamples}
147
+ source={result}
148
+ description="Sum of the fixture counts in the selected groups."
149
+ />
150
+ <TableWidget
151
+ dataset={sampleCounts}
152
+ source={result}
153
+ title="Counts by group"
154
+ description="Alpha and Beta demonstrate measured values, including zero."
155
+ pagination={false}
156
+ />
157
+ </Grid>
158
+ </Stack>
159
+ ));
110
160
  function App() {
111
- const sampleCountsRequest = useView(sampleCountsView);
112
161
  return (
113
162
  <DataApp
114
163
  config={appConfig}
115
- dataContext={sampleDataContext}
116
- request={sampleCountsRequest}
117
- story={
118
- sampleCountsRequest.snapshot?.data.length
119
- ? ({ data: sampleCounts, input: displayedInput }) =>
120
- sampleCounts.map(({ groupName, sampleCount }) =>
121
- sampleDataContext.finding({
122
- id: `sample-count-${groupName}`,
123
- headline: `${groupName} has ${sampleCount} samples`,
124
- context: `Demonstration values for ${displayedInput.groupName || 'all groups'}.`,
125
- visual: (
126
- <MetricWidget
127
- label="Sample count"
128
- value={sampleCount}
129
- format={{ kind: 'count' }}
130
- />
131
- ),
132
- visualKind: 'metric',
133
- evidence: {
134
- id: `sample-count-${groupName}`,
135
- glossaryIds: ['sampleCount'],
136
- },
137
- })
138
- )
139
- : undefined
140
- }
164
+ view={sampleCountsView}
165
+ datasets={[sampleCounts]}
166
+ story={snapshot => {
167
+ const total = totalSamples.read(snapshot);
168
+ const scope = sampleCountsView.scope(snapshot);
169
+ return [
170
+ {
171
+ id: 'total-samples',
172
+ headline: `Total samples: ${formatMetric(total.value.current, totalSamples.definition.format)}`,
173
+ context: `Demonstration values for ${scope}.`,
174
+ visual: <MetricWidget metric={totalSamples} source={snapshot} />,
175
+ visualKind: 'metric',
176
+ evidence: totalSamples,
177
+ },
178
+ {
179
+ id: 'counts-by-group',
180
+ headline: 'Counts by group',
181
+ context: `Demonstration values for ${scope}.`,
182
+ visual: (
183
+ <TableWidget
184
+ dataset={sampleCounts}
185
+ source={snapshot}
186
+ pagination={false}
187
+ />
188
+ ),
189
+ evidence: sampleCounts,
190
+ },
191
+ ];
192
+ }}
141
193
  >
142
- {(sampleCounts, displayedInput) => (
143
- <section aria-label="Sample results">
194
+ <Stack>
195
+ <TextContent>
144
196
  <h2>Sample counts</h2>
145
- <p>Showing {displayedInput.groupName || 'all groups'}</p>
146
- <ul>
147
- {sampleCounts.map(({ groupName, sampleCount }) => (
148
- <li key={groupName}>
149
- {groupName}: {sampleCount}
150
- </li>
151
- ))}
152
- </ul>
153
- </section>
154
- )}
197
+ </TextContent>
198
+ <DataSection content={sampleContent} />
199
+ </Stack>
155
200
  </DataApp>
156
201
  );
157
202
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@altertable/data-app",
3
- "version": "0.64.0",
3
+ "version": "0.66.0",
4
4
  "description": "Contracts, transport, and React UI for Altertable data apps",
5
5
  "keywords": [
6
6
  "altertable",
@@ -78,6 +78,10 @@
78
78
  "./react/embed": {
79
79
  "types": "./dist/types/react/embed/index.d.ts",
80
80
  "import": "./dist/react/embed/index.js"
81
+ },
82
+ "./react/ui": {
83
+ "types": "./dist/types/react/ui/index.d.ts",
84
+ "import": "./dist/react/ui/index.js"
81
85
  }
82
86
  },
83
87
  "publishConfig": {
@@ -86,12 +90,13 @@
86
90
  },
87
91
  "scripts": {
88
92
  "build": "bun run scripts/build.ts && tsc -p tsconfig.build.json && bun run scripts/build-declarations.ts",
93
+ "dev": "bun run scripts/dev.ts",
89
94
  "typecheck": "bun install --frozen-lockfile && tsc --noEmit",
90
- "lint": "oxlint --type-aware src tests scripts browser-tests examples/starter-data-app",
95
+ "lint": "oxlint --type-aware src tests scripts browser-tests dev examples/starter-data-app",
91
96
  "format": "oxfmt .",
92
97
  "format:check": "oxfmt --check .",
93
98
  "test": "bun test ./tests",
94
- "check": "bun run build && bun run typecheck && bun run lint && bun run lint:md && bun run check:links && bun run format:check && bun run test && bun run test:package && bun run test:starter",
99
+ "check": "bun run build && bun run typecheck && bun run lint && bun run check:styles && bun run lint:md && bun run check:links && bun run format:check && bun run test && bun run test:package && bun run test:starter",
95
100
  "test:package": "bun run scripts/check-package.ts",
96
101
  "check:workflows": "bash scripts/check-workflows.sh",
97
102
  "test:browser": "playwright test --config browser-tests/playwright.config.ts",
@@ -99,13 +104,16 @@
99
104
  "test:starter": "bun run scripts/check-starter.ts",
100
105
  "lint:md": "markdownlint-cli2",
101
106
  "check:links": "remark . --ext md --rc-path .remarkrc.json --frail --no-stdout",
102
- "check:links:external": "remark . --ext md --rc-path .remarkrc-external.json --frail --no-stdout"
107
+ "check:links:external": "remark . --ext md --rc-path .remarkrc-external.json --frail --no-stdout",
108
+ "check:styles": "bun run scripts/check-app-styles.ts"
103
109
  },
104
110
  "dependencies": {
105
111
  "@floating-ui/react": "0.27.20",
106
112
  "@internationalized/date": "3.12.4",
107
113
  "@tanstack/react-query": "5.104.0",
114
+ "fflate": "0.8.3",
108
115
  "fuzzysort": "4.0.2",
116
+ "html-to-image": "1.11.13",
109
117
  "lucide-react": "1.48.0",
110
118
  "react-aria-components": "1.21.1"
111
119
  },
@@ -124,6 +132,7 @@
124
132
  "remark-gfm": "4.0.1",
125
133
  "remark-lint-no-dead-urls": "2.0.1",
126
134
  "remark-validate-links": "13.1.0",
135
+ "testcontainers": "12.2.0",
127
136
  "typescript": "7.0.2"
128
137
  },
129
138
  "peerDependencies": {
@@ -1,18 +0,0 @@
1
- // src/core/bridge.ts
2
- var BRIDGE = "altertable:data-app";
3
- var PARENT_PARAM = "__altertable_parent";
4
- var MAX_PENDING = 128;
5
- var REQUEST_TIMEOUT_MS = 60000;
6
- function isBridgeMessage(value) {
7
- if (!value || typeof value !== "object")
8
- return false;
9
- const message = value;
10
- return message.channel === BRIDGE && message.version === 1 && typeof message.type === "string" && typeof message.documentId === "string" && message.documentId.length > 0 && message.documentId.length <= 128;
11
- }
12
- function validId(value) {
13
- return typeof value === "string" && /^[a-zA-Z0-9_-]{1,128}$/.test(value);
14
- }
15
-
16
- export { BRIDGE, PARENT_PARAM, MAX_PENDING, REQUEST_TIMEOUT_MS, isBridgeMessage, validId };
17
-
18
- //# debugId=416EADDA2C8ACEA064756E2164756E21