@altertable/data-app 0.62.0 → 0.64.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 (150) hide show
  1. package/AGENTS.md +4 -34
  2. package/CONTRIBUTING.md +79 -87
  3. package/README.md +14 -38
  4. package/dist/chunks/{contract-ryyf6dme.js → contract-cxr9t12b.js} +2 -5
  5. package/dist/chunks/contract-cxr9t12b.js.map +10 -0
  6. package/dist/chunks/contract-mev09s5v.js.map +2 -2
  7. package/dist/chunks/{contract-xjv197ck.js → contract-mwe7gnmh.js} +8 -8
  8. package/dist/chunks/{contract-xjv197ck.js.map → contract-mwe7gnmh.js.map} +3 -3
  9. package/dist/chunks/{contract-3bnrf5pf.js → contract-tf8c3qpv.js} +11 -3
  10. package/dist/chunks/contract-tf8c3qpv.js.map +11 -0
  11. package/dist/chunks/{contract-farfe948.js → contract-txjy0en2.js} +31 -31
  12. package/dist/chunks/contract-txjy0en2.js.map +10 -0
  13. package/dist/chunks/contract-wz59z8pq.js.map +2 -2
  14. package/dist/chunks/{contract-4vrw9zk9.js → contract-xtxza1fy.js} +69 -60
  15. package/dist/chunks/contract-xtxza1fy.js.map +12 -0
  16. package/dist/chunks/{contract-nt819swq.js → contract-yxbjea23.js} +13 -6
  17. package/dist/chunks/contract-yxbjea23.js.map +12 -0
  18. package/dist/chunks/{contract-rrm7s5zp.js → contract-zr3jd7mr.js} +5 -5
  19. package/dist/chunks/contract-zr3jd7mr.js.map +12 -0
  20. package/dist/client/index.js +7 -8
  21. package/dist/client/index.js.map +1 -1
  22. package/dist/core/appearance.js +4 -2
  23. package/dist/core/appearance.js.map +1 -1
  24. package/dist/core/contract.js +2 -2
  25. package/dist/embed/index.js +9 -11
  26. package/dist/embed/index.js.map +2 -2
  27. package/dist/local.js +7 -31
  28. package/dist/local.js.map +5 -7
  29. package/dist/react/embed/index.js +2 -3
  30. package/dist/react/embed/index.js.map +2 -2
  31. package/dist/react/index.js +5446 -230
  32. package/dist/react/index.js.map +71 -66
  33. package/dist/server.js +7 -31
  34. package/dist/server.js.map +5 -7
  35. package/dist/types/client/data-client.d.ts +33 -0
  36. package/dist/types/client/index.d.ts +5 -41
  37. package/dist/types/client/location.d.ts +4 -6
  38. package/dist/types/client/navigation.d.ts +2 -1
  39. package/dist/types/core/appearance.d.ts +12 -1
  40. package/dist/types/core/bridge.d.ts +2 -8
  41. package/dist/types/core/config.d.ts +3 -2
  42. package/dist/types/core/contract.d.ts +9 -56
  43. package/dist/types/core/format.d.ts +0 -1
  44. package/dist/types/core/messages.d.ts +5 -30
  45. package/dist/types/core/navigation.d.ts +11 -0
  46. package/dist/types/core/operation-types.d.ts +76 -0
  47. package/dist/types/core/operation.d.ts +2 -8
  48. package/dist/types/core/variables.d.ts +2 -2
  49. package/dist/types/embed/host.d.ts +5 -3
  50. package/dist/types/embed/index.d.ts +0 -1
  51. package/dist/types/embed/source.d.ts +2 -9
  52. package/dist/types/react/content.d.ts +3 -3
  53. package/dist/types/react/hooks.d.ts +65 -64
  54. package/dist/types/react/index.d.ts +135 -2
  55. package/dist/types/react/injectStyles.d.ts +7 -0
  56. package/dist/types/react/shellStyles.d.ts +3 -0
  57. package/dist/types/react/styles.d.ts +8 -0
  58. package/dist/types/react/ui/AboutData.d.ts +0 -1
  59. package/dist/types/react/ui/AppFooter.d.ts +0 -1
  60. package/dist/types/react/ui/AppHeader.d.ts +0 -1
  61. package/dist/types/react/ui/AppLayout.d.ts +0 -1
  62. package/dist/types/react/ui/AppScope.d.ts +0 -1
  63. package/dist/types/react/ui/AppToolbar.d.ts +0 -1
  64. package/dist/types/react/ui/Breakdown.d.ts +0 -1
  65. package/dist/types/react/ui/Button.d.ts +0 -1
  66. package/dist/types/react/ui/Checkbox.d.ts +0 -1
  67. package/dist/types/react/ui/Combobox.d.ts +0 -1
  68. package/dist/types/react/ui/ComparisonVisual.d.ts +0 -1
  69. package/dist/types/react/ui/ContentSkeleton.d.ts +2 -5
  70. package/dist/types/react/ui/DataApp.d.ts +2 -2
  71. package/dist/types/react/ui/DataAppSkeleton.d.ts +0 -1
  72. package/dist/types/react/ui/DataBoundary.d.ts +0 -1
  73. package/dist/types/react/ui/DataSection.d.ts +2 -4
  74. package/dist/types/react/ui/DataTable.d.ts +2 -3
  75. package/dist/types/react/ui/DataViewToast.d.ts +0 -1
  76. package/dist/types/react/ui/DataWidget.d.ts +4 -14
  77. package/dist/types/react/ui/DateRangePicker.d.ts +0 -1
  78. package/dist/types/react/ui/DateTimeTooltip.d.ts +0 -1
  79. package/dist/types/react/ui/EmptyState.d.ts +2 -5
  80. package/dist/types/react/ui/GettingStarted.d.ts +0 -1
  81. package/dist/types/react/ui/GlossaryDefinition.d.ts +0 -1
  82. package/dist/types/react/ui/GlossaryExplanation.d.ts +0 -1
  83. package/dist/types/react/ui/GradientScroll.d.ts +0 -1
  84. package/dist/types/react/ui/Grid.d.ts +0 -1
  85. package/dist/types/react/ui/HelpPopover.d.ts +0 -2
  86. package/dist/types/react/ui/Kbd.d.ts +0 -1
  87. package/dist/types/react/ui/LiveControl.d.ts +0 -1
  88. package/dist/types/react/ui/MetricWidget.d.ts +0 -2
  89. package/dist/types/react/ui/PeriodSummary.d.ts +0 -1
  90. package/dist/types/react/ui/PresentStory.d.ts +0 -1
  91. package/dist/types/react/ui/QueryList.d.ts +0 -1
  92. package/dist/types/react/ui/Ranking.d.ts +0 -1
  93. package/dist/types/react/ui/RefreshControl.d.ts +0 -1
  94. package/dist/types/react/ui/RefreshRegion.d.ts +0 -1
  95. package/dist/types/react/ui/RequestHint.d.ts +0 -1
  96. package/dist/types/react/ui/SearchField.d.ts +0 -1
  97. package/dist/types/react/ui/SearchInput.d.ts +0 -1
  98. package/dist/types/react/ui/SearchMatch.d.ts +0 -1
  99. package/dist/types/react/ui/SelectableBarChart.d.ts +0 -1
  100. package/dist/types/react/ui/SelectionMark.d.ts +0 -1
  101. package/dist/types/react/ui/Sheet.d.ts +0 -1
  102. package/dist/types/react/ui/Skeleton.d.ts +0 -1
  103. package/dist/types/react/ui/Stack.d.ts +0 -1
  104. package/dist/types/react/ui/StatusPanel.d.ts +0 -1
  105. package/dist/types/react/ui/TableWidget.d.ts +2 -3
  106. package/dist/types/react/ui/Tabs.d.ts +0 -1
  107. package/dist/types/react/ui/TextContent.d.ts +4 -0
  108. package/dist/types/react/ui/TextWidget.d.ts +19 -0
  109. package/dist/types/react/ui/ThemeSelector.d.ts +0 -1
  110. package/dist/types/react/ui/Tooltip.d.ts +0 -1
  111. package/dist/types/react/ui/UpdatedAt.d.ts +0 -1
  112. package/dist/types/react/ui/VariableBar.d.ts +0 -1
  113. package/dist/types/react/ui/VisualizationWidget.d.ts +5 -13
  114. package/dist/types/react/ui/WidgetDisclosure.d.ts +0 -1
  115. package/dist/types/react/ui/WidgetViewTabs.d.ts +2 -3
  116. package/dist/types/react/ui/data-identifiers.d.ts +0 -1
  117. package/dist/types/react/ui/presentation.d.ts +19 -0
  118. package/dist/types/react/ui/useAppAppearance.d.ts +2 -2
  119. package/dist/types/react/view-controls.d.ts +1 -1
  120. package/dist/types/react/view.d.ts +2 -2
  121. package/dist/worker.js +1 -0
  122. package/docs/app-authoring.md +61 -44
  123. package/docs/client.md +11 -19
  124. package/docs/contract.md +9 -9
  125. package/docs/embed.md +10 -29
  126. package/docs/hosted-apps.md +39 -0
  127. package/docs/local-data-apps.md +14 -0
  128. package/docs/react-embed.md +10 -19
  129. package/docs/react.md +108 -134
  130. package/docs/server-bun.md +4 -6
  131. package/docs/server.md +2 -2
  132. package/docs/worker.md +3 -5
  133. package/examples/starter-data-app/index.tsx +159 -0
  134. package/package.json +16 -10
  135. package/dist/chunks/contract-3bnrf5pf.js.map +0 -10
  136. package/dist/chunks/contract-4vrw9zk9.js.map +0 -12
  137. package/dist/chunks/contract-8q35dcyh.js +0 -9
  138. package/dist/chunks/contract-8q35dcyh.js.map +0 -10
  139. package/dist/chunks/contract-farfe948.js.map +0 -10
  140. package/dist/chunks/contract-nt819swq.js.map +0 -11
  141. package/dist/chunks/contract-rrm7s5zp.js.map +0 -12
  142. package/dist/chunks/contract-ryyf6dme.js.map +0 -10
  143. package/dist/react/index.css +0 -4628
  144. package/dist/react.css.d.ts +0 -1
  145. package/dist/types/react/ui/index.d.ts +0 -131
  146. package/docs/appearance.md +0 -43
  147. package/docs/config.md +0 -21
  148. package/docs/format.md +0 -29
  149. package/docs/react-styles.md +0 -17
  150. package/docs/starter-agent-instructions.md +0 -53
@@ -1,10 +1,10 @@
1
1
  # React embedding
2
2
 
3
- Import `DataAppBridge` from `@altertable/data-app/react/embed`. This entry depends
3
+ Import `<DataAppBridge>` from `@altertable/data-app/react/embed`. This entry depends
4
4
  on React and the embedding engine, and does not load the app's widgets, React
5
5
  Query, or CSS.
6
6
 
7
- The bridge owns iframe setup and `postMessage` communication. The consuming
7
+ The bridge owns iframe setup and `postMessage()` communication. The consuming
8
8
  frontend or CLI owns its shell: fetching a bundle, subscriptions, layout, loading
9
9
  and error UI, and retry controls. Both hosts use the same bridge and transport.
10
10
 
@@ -63,7 +63,7 @@ including `className`, `style`, and `hidden`. The bridge controls `src`, `srcDoc
63
63
 
64
64
  ## Host-owned iframe
65
65
 
66
- Use the same `DataAppBridge` with `iframe` and `connection` when the host already
66
+ Use the same `<DataAppBridge>` with `iframe` and `connection` when the host already
67
67
  owns a loaded iframe and its security policy:
68
68
 
69
69
  ```tsx
@@ -91,27 +91,18 @@ mode renders nothing and handles delivery only. Use source mode for bundle
91
91
  loading, sandbox policy, token rotation, and startup timeout. The two prop modes
92
92
  are mutually exclusive.
93
93
 
94
- ## Migration from the package shell
94
+ ## Loading an embedded app
95
95
 
96
- `DataAppShell` and `DataAppShellProps` have been removed. Replace the import with
97
- `DataAppBridge` and keep `source`, `title`, and message/status callbacks. Move
98
- `loading` and `renderError` into the consuming shell, use `iframeProps.hidden` to
99
- control visibility, and change the bridge's key for retries. Frontend bundle
100
- fetching and subscriptions and CLI local-server forwarding remain host concerns.
101
-
102
- ## Loading placeholder
103
-
104
- For a shared placeholder while building or connecting, render `DataAppSkeleton`
105
- from `@altertable/data-app/react` in the consuming shell. Import
106
- `@altertable/data-app/react/styles.css` once in that host's browser entry. See
107
- [React](react.md#loading-an-embedded-app) for usage. The bridge does not render
108
- loading UI itself.
96
+ Use `<DataAppSkeleton>` from `/react` while the host builds or starts an app.
97
+ Call `injectDataAppShellStyles()` from `/react` before rendering the placeholder.
98
+ The host owns when to show it and supplies any surrounding header or footer.
99
+ `/react/embed` itself remains independent of UI components and styles.
109
100
 
110
101
  ## Parent-owned presentation
111
102
 
112
103
  Pass `presentation={{ surface: 'embedded', theme: resolvedTheme }}`
113
- to `DataAppBridge` in either source or connection mode. Use `'standalone'` when the app should render its own page chrome.
104
+ to `<DataAppBridge>` in either source or connection mode. Use `'standalone'` when the app should render its own page chrome.
114
105
  Prop updates publish trusted state without reloading the iframe or reconnecting
115
106
  the session. Resolve system preference in the parent to `'light'` or `'dark'`.
116
- Inside an embedded surface, `DataApp` retains toolbar actions and hides its header
107
+ Inside an embedded surface, `<DataApp>` retains toolbar actions and hides its header
117
108
  and footer. See [parent presentation](embed.md#parent-presentation).
package/docs/react.md CHANGED
@@ -1,75 +1,26 @@
1
1
  # React
2
2
 
3
3
  Import hooks, components, and UI helpers from `@altertable/data-app/react`.
4
- Import `@altertable/data-app/react/styles.css` once in the browser entry.
5
4
  React 19.2 or newer and React DOM 19.2 or newer are peer dependencies.
6
5
 
6
+ ## Mount the app
7
+
7
8
  `mountDataApp({ config, component })` mounts into `#root`, sets the document
8
9
  title and language, attaches navigation to an available iframe transport, and
9
- installs `DataAppProvider`. When mounting through another
10
- framework, wrap the app in `DataAppProvider` yourself.
11
-
12
- ## Find UI by task
13
-
14
- All of these APIs are exported from `/react`. Each component's stylesheet lives beside its implementation.
15
-
16
- | Task | Start here | Related APIs |
17
- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
18
- | Primary request and page shell | [DataApp](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/DataApp.tsx) | AppLayout, AppHeader, AppToolbar, AppFooter, AppScope, ThemeToggle |
19
- | Initial connection check | [GettingStarted](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/GettingStarted.tsx) | Pair with `connectionCheck()` from `/contract` |
20
- | Arrange content | [Grid](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/Grid.tsx), [Stack](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/Stack.tsx) | DataWidget |
21
- | Show a key number | [MetricWidget](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/MetricWidget.tsx) | ComparisonVisual |
22
- | Show charts and collections | [VisualizationWidget](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/VisualizationWidget.tsx), [TableWidget](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/TableWidget.tsx) | DataTable, Ranking, Breakdown, chartColor |
23
- | Handle a request's loading, error, and stale data | [DataSection](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/DataSection.tsx) | DataBoundary, DataViewToast, EmptyState, StatusPanel, Skeleton |
24
- | Show freshness and refresh | [UpdatedAt](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/UpdatedAt.tsx), [AppToolbar](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/AppToolbar.tsx) | RefreshRegion, LiveControl |
25
- | Bind filters to the URL | [variables](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/variables.ts), [DateRangePicker](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/DateRangePicker.tsx) | Combobox, PeriodSummary, Tabs, useViewTab |
26
- | Search a loaded collection | [searchItems](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/searchItems.ts), [SearchMatch](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/SearchMatch.tsx) | SearchField |
27
- | Explain context, glossary, and queries | [AboutData](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/AboutData.tsx), [DataContext](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/data-context.ts) | [GlossaryDefinition](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/GlossaryDefinition.tsx), GlossaryExplanation, [defineDataIdentifiers](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/data-identifiers.tsx) |
28
- | Present loaded findings | [PresentStory](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/PresentStory.tsx) | StoryFinding |
29
- | Build custom controls and overlays | [Button](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/Button.tsx), [Sheet](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/Sheet.tsx) | IconButton, Tooltip, HelpPopover, Kbd |
30
-
31
- ## Loading an embedded app
32
-
33
- `DataAppSkeleton` is a host-side placeholder while an embedded app is building or
34
- starting. It composes the shared metric, visualization, and data widgets with
35
- skeleton content: three metric cards, a chart, and table rows. Optional `header`
36
- and `footer` React nodes let the host supply its own layout; neither renders by
37
- default.
38
- It adapts to container width and inherits the package's appearance variables and
39
- reduced-motion skeleton animation.
10
+ installs `<DataAppProvider>`. When mounting through another
11
+ framework, wrap the app in `<DataAppProvider>` yourself.
40
12
 
41
13
  ```tsx
42
- import { DataAppSkeleton } from '@altertable/data-app/react';
43
- import '@altertable/data-app/react/styles.css';
44
-
45
- <DataAppSkeleton
46
- aria-label="Loading activity report"
47
- className="app-loading"
48
- header={<ReportHeaderSkeleton />}
49
- footer={<ReportFooterSkeleton />}
50
- />;
51
- ```
52
-
53
- The container announces a loading status; its widget placeholders are hidden from
54
- assistive technology. Supplied header and footer nodes remain accessible and can
55
- use the exported `Skeleton` component for their own placeholders. Standard output props, including `style` and `ref`, can be used
56
- for layout. The consuming shell controls when to display it. The `/react/embed`
57
- entry remains independent of UI components and the stylesheet.
58
-
59
- ## Bound views and widgets
14
+ import { injectDataAppStyles, mountDataApp } from '@altertable/data-app/react';
60
15
 
61
- | Definition | Runtime owns |
62
- | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
63
- | `defineOperation` | Input/output validation, check inputs, query limits and cancellation; `query(name, sql)` accepts registered names and records executed evidence. |
64
- | `defineDataView` | URL variables, operation input, emptiness and the primary date binding. `useView` connects the result and controls to `DataApp`. |
65
- | `DataApp` | Header, variable bar, refresh state, stale-result notice and dimming, default inspection empty states. |
66
- | `view.content` | One loading/ready layout. `result.select` never evaluates loading data; `result.metric` binds comparisons to the displayed input. |
67
- | `context.metric` | Label, numeric format, glossary evidence and optional direction of improvement. |
68
- | `WidgetViewTabs` | Valid, unique selection IDs and a required empty state per tab. |
69
-
70
- SQL and business definitions belong to the app. Hosted adapters authorize every request; local development can use the CLI proxy. SQL disclosure also requires server permission.
16
+ injectDataAppStyles();
17
+ mountDataApp({ config, component: App });
18
+ ```
71
19
 
72
- A measured zero and unavailable data have different meanings. Metric readings use `null` for an unavailable previous value. The app defines whether a result is empty. `Breakdown` shows parts of a total; `Ranking` scales against its largest value. Percent formats accept ratios.
20
+ Importing the package does not inject styles. For server rendering, call the
21
+ injector on the client before mounting or hydrating. With a restrictive CSP,
22
+ pass a permitted nonce; the first call sets it for the document. No CSS loader
23
+ or separate stylesheet asset is needed.
73
24
 
74
25
  ## Bind a view
75
26
 
@@ -86,7 +37,7 @@ import {
86
37
  } from '@altertable/data-app/react';
87
38
  import type { operations } from '#app/operations.ts';
88
39
  import { calendar } from '#app/contracts.ts';
89
- import { dataContext, actions } from '#app/data-context.tsx';
40
+ import { dataContext, trackedIdentities } from '#app/data-context.tsx';
90
41
  import config from '#config';
91
42
 
92
43
  const period = dateRangeVariable({
@@ -112,7 +63,7 @@ const featureEvidence = dataContext.evidence({
112
63
  const content = activityView.content(result => (
113
64
  <Grid columns={2}>
114
65
  <MetricWidget
115
- metric={actions}
66
+ metric={trackedIdentities}
116
67
  reading={result.metric(data => ({
117
68
  current: data.count,
118
69
  previous: data.previousCount,
@@ -145,33 +96,62 @@ function App() {
145
96
 
146
97
  The `date` binding identifies the controlling variable and extracts its range from the operation input. Nested inputs use, for example, `input: (input) => input.period`. The runtime rejects mappings that silently change the selected range or comparison. Non-date views supply `describeInput`; date views can override it when other inputs also need describing.
147
98
 
148
- The shared calendar lives in a browser-safe module:
99
+ Share the [date range contract](contract.md#shared-date-ranges) between the
100
+ operation parser and the view.
149
101
 
150
- ```ts
151
- import { defineDateRangeContract } from '@altertable/data-app/contract';
152
- export const calendar = defineDateRangeContract({
153
- minDate: '2026-01-01',
154
- maxRangeDays: 90,
155
- timeZone: 'UTC',
156
- });
157
- // Server operation: input: calendar.parseRequest
158
- ```
102
+ `useView()` generates controls for date, text and fixed-option select variables; custom controls use `result.variables.bind(name)`. `input` chooses which variables reach the operation, so local search can stay local. Hooks belong in the enclosing component.
159
103
 
160
- `useView` generates controls for date, text and fixed-option select variables; custom controls use `result.variables.bind(name)`. `input` chooses which variables reach the operation, so local search can stay local. The callback in `view.content` receives the displayed result, including its original input during refreshes and failures. Hooks belong in the enclosing component.
104
+ For large tables, use query-backed pagination with a stable sort and total
105
+ count. Client pagination and search cover only the rows already returned.
161
106
 
162
- `TableWidget` also accepts `reading={result.select((data) => data.rows)}` and optional `skeletonRows`. Columns and empty states are declared once for both loading and ready layouts. Bounded tables default to 10 rows per page with a bottom footer shared with inspection. Set `pagination={{ pageSize: 8 }}` to change the page size or `pagination={false}` to show all rows. Search runs before pagination; the footer counts only the supplied rows. `limit` remains a separate, mutually exclusive display cap. Large catalogs need query-backed pagination with a stable sort and total count.
107
+ Bound `<VisualizationWidget>` and `<TableWidget>` components require `evidence` from `context.evidence(...)`. A bound `<MetricWidget>` gets evidence from its metric definition. Evidence must name at least one glossary entry or query. Static widgets may omit it.
163
108
 
164
- Bound `VisualizationWidget` and `TableWidget` calls require `evidence` from `context.evidence(...)`. A bound `MetricWidget` gets evidence from its metric definition. Evidence must name at least one glossary entry or query. Static widgets may omit it.
109
+ `<MetricWidget>` and `<ComparisonVisual>` both accept the same `metric` and `reading`. The comparison is enabled by the displayed result's range. The definition supplies formatting and evidence; a reading cannot override those or provide a second value. `favorableDirection` is optional; changes are neutral until the author defines whether up or down is favorable.
165
110
 
166
- `MetricWidget` and `ComparisonVisual` both accept the same `metric` and `reading`. The comparison is enabled by the displayed result's range. The definition supplies formatting and evidence; a reading cannot override those or provide a second value. `favorableDirection` is optional; changes are neutral until the author defines whether up or down is favorable.
111
+ Use the [app helpers](#reuse-app-helpers) for metric formats and values in tables,
112
+ charts, and custom views.
167
113
 
168
- `defineDataContent` remains available for manually managed requests. Its optional `{ date: (input) => rangeRequest }` binds comparison readings. `DataSection` handles independent requests. Low-level widgets, tabs and layout components remain available for custom interfaces.
114
+ `defineDataContent()` remains available for manually managed requests. Its optional `{ date: (input) => rangeRequest }` binds comparison readings. `<DataSection>` handles independent requests. Low-level widgets, tabs and layout components remain available for custom interfaces.
169
115
 
170
- ## Bind evidence
116
+ ## Narrative text
117
+
118
+ Use `<TextContent>` for prose within a page or custom layout, and `<TextWidget>`
119
+ when the explanation belongs in a titled panel alongside other widgets.
120
+
121
+ Give text a purpose: frame the question, explain how to interpret a comparison,
122
+ qualify a finding, or suggest what to explore next. Choose the content for the
123
+ reader's question and the decisions the exploration supports.
124
+
125
+ For data-dependent text, use `reading={result.select((data, input) => ...)}` and
126
+ provide `evidence`. Derive both the explanation and its scope from those displayed
127
+ values so it stays consistent with the visualizations while filters change.
128
+ Static instructions can use ordinary children; local filters should feed the same
129
+ filtered data to the text and its related visualization.
130
+
131
+ ## Reuse app helpers
132
+
133
+ Use the shared helpers for common app tasks. Follow the entry points below to
134
+ find their exports, types, and usage constraints.
135
+
136
+ | Task | Entry point |
137
+ | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
138
+ | Format numbers, counts, percentages, currency, date ranges, and plural labels (`pluralize`) | [Format helpers](https://github.com/altertable-ai/data-app/blob/main/src/core/format.ts) |
139
+ | Choose chart colors | [Chart colors](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/chartColor.ts) |
140
+ | Search items and highlight matches | [Search helpers](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/searchItems.ts) |
141
+ | Read and synchronize URL query state | [URL state helpers](https://github.com/altertable-ai/data-app/blob/main/src/react/ui/search.ts) |
142
+ | Render timestamps, freshness, table shares, periods, and metric comparisons | [React exports](https://github.com/altertable-ai/data-app/blob/main/src/react/index.ts) |
143
+ | Configure appearance and theme preferences | [Appearance helpers](https://github.com/altertable-ai/data-app/blob/main/src/core/appearance.ts) |
144
+ | Build the app's scoped document title | [Configuration helpers](https://github.com/altertable-ai/data-app/blob/main/src/core/config.ts) |
145
+
146
+ ## Register source identifiers
147
+
148
+ Use `defineDataIdentifiers()` from `/react` to register exact catalog, schema,
149
+ table, and field names. Use `<DataIdentifier>` in descriptions and glossary
150
+ entries to render those source references consistently.
171
151
 
172
152
  ```tsx
173
- const queries = defineQueryNames({ activity: 'feature-activity' });
174
- // In the server operation: queryNames: queries
153
+ import { defineDataIdentifiers } from '@altertable/data-app/react';
154
+
175
155
  const identifiers = defineDataIdentifiers({
176
156
  tables: {
177
157
  events: {
@@ -183,6 +163,20 @@ const identifiers = defineDataIdentifiers({
183
163
  columns: { identity: { table: 'events', name: 'identity_uuid' } },
184
164
  });
185
165
  const { DataIdentifier } = identifiers;
166
+
167
+ <DataIdentifier id="tables.events" />;
168
+ <DataIdentifier id="columns.events.identity" />;
169
+ ```
170
+
171
+ Pass `identifiers.definitions` to `createDataContext()` so the context carries the
172
+ same source registry. Source identifiers name physical data; glossary entries
173
+ explain its business meaning, and query evidence records how it was queried.
174
+
175
+ ## Bind evidence
176
+
177
+ ```tsx
178
+ const queries = defineQueryNames({ activity: 'feature-activity' });
179
+ // In the server operation: queryNames: queries
186
180
  const context = createDataContext(queries)({
187
181
  identifiers: identifiers.definitions,
188
182
  description: (
@@ -207,7 +201,7 @@ const evidence = context.evidence({
207
201
  glossaryIds: ['identities'],
208
202
  queryNames: [queries.activity],
209
203
  });
210
- const actions = context.metric({
204
+ const trackedIdentities = context.metric({
211
205
  id: 'actions',
212
206
  glossaryId: 'identities',
213
207
  label: 'Tracked identities',
@@ -221,67 +215,27 @@ const finding = context.finding({
221
215
  });
222
216
  ```
223
217
 
224
- Import `defineQueryNames` from `/contract` and the context/identifier factories from `/react`. Use the same registry in `defineOperation({ queryNames: queries, ... })`. Unknown glossary/query references fail type checks and registry validation; the server also validates returned query names. Physical source identity stays in the identifier registry for future linking.
218
+ Import `defineQueryNames()` from `/contract` and the context/identifier factories from `/react`. Use the same registry in `defineOperation({ queryNames: queries, ... })`. Unknown glossary/query references fail type checks and registry validation; the server also validates returned query names.
225
219
 
226
- ## Authoring constraints
220
+ ## Time views and field filters
227
221
 
228
- - Date views declare `date: { variable: "period", input: (input) => input }`, or supply `describeInput` for a non-date view.
229
- - Numeric metrics use `value={count} format={{ kind: "count" }}`. Custom formatted JSX or strings use `content={...}` instead of `value`.
230
- - Supply `empty` to secondary `DataSection` requests or pass a bound `useView` result. A primary `DataApp` accepts it either from `useView` or as an explicit prop.
231
- - For alternate views of the same bound result, pass `views={[{ id, label, render }]}` and `viewLabel` to `VisualizationWidget`. Its required `isEmpty` and `empty` apply to the whole result; the widget owns selection. Use `WidgetViewTabs` directly only when views have independent empty states.
232
- - Variable URL keys cannot use `view`, `about`, `tab`, `present`, or `step`.
233
-
234
- ## Time views and dimension filters
235
-
236
- `createDataHooks(client).defineTimeView` owns the `period` variable, calendar
222
+ `createDataHooks(client).defineTimeView()` owns the `period` variable, calendar
237
223
  controls, and displayed-period label. Declare `time: { contract, defaultValue }`,
238
224
  an operation, `isEmpty`, and `empty`. With no additional variables, its default
239
225
  input is the calendar request. With additional variables, it is `{ period, ...variables }`.
240
226
  Supply an `input` mapper for a different operation shape and `bindings` to extract
241
- nested period or dimension inputs. Mappings must preserve the selected values.
227
+ nested period or field-filter inputs. Mappings must preserve the selected values.
242
228
 
243
- `dimensionFilter` requires exactly one option source: fixed `options` or a `facet`.
244
- Use `defineFacetFilter` to bind a facet operation and its typed input. The generated
245
- `DimensionPicker` preserves cached options during refresh and failure, offers
229
+ `dimensionFilter()` from `/contract` requires exactly one option source: fixed `options` or a `facet`.
230
+ Use `defineFacetFilter()` to bind a facet operation and its typed input. The generated
231
+ `<DimensionPicker>` preserves cached options during refresh and failure, offers
246
232
  missing values separately, and retains selected values absent from a result with
247
- zero counts. `SelectableBarChart` can share controlled selection with the picker.
248
-
249
- ## Findings and inspection
250
-
251
- `DataWidget` composes a body, toolbar feedback, and footer. Widgets and their
252
- inspection sheets render the same visual and controls. Keep interactive state
253
- above both mounts when authoring custom children.
254
-
255
- Widget headings open inspection when evidence is available; a disclosure arrow
256
- appears beside the heading on hover or keyboard focus. The top-end toolbar is
257
- reserved for actions and request status. Empty widgets omit their footer, charts
258
- omit interaction instructions without data, and table pagination appears only
259
- when there is more than one page. Local `DataBoundary` and `DataSection` inline
260
- notices sit above retained section content; use widget status for widget feedback
261
- and page controls or notices for page feedback.
262
-
263
- Combobox search text aligns with option labels. Empty search inputs blur on
264
- Escape; a second Escape dismisses an open picker. Picker failures show a centered
265
- message with the retry action below it.
233
+ zero counts. `<SelectableBarChart>` can share controlled selection with the picker.
266
234
 
267
- `DataApp.story` receives the displayed snapshot, including its original input
268
- during refresh or failure. Return one to four `StoryFinding` values with unique
269
- IDs and registered evidence. `PresentStory` presents those findings directly;
270
- `context.finding` validates their evidence against the context registry.
235
+ ## Preserve displayed results
271
236
 
272
- ### Migration from the earlier runtime
273
-
274
- - `DataWidget` replaces the internal `DataPanel` shell and `StorySection` layout.
275
- - `PresentStory` replaces `PlayStory`; provide `findings` with explicit `evidence`.
276
- - `context.finding` replaces `context.storyStep`.
277
- - Table pagination is enabled by default; use `pagination={false}` for complete tables.
278
-
279
- ## Component gallery
280
-
281
- Contributors can preview `/gallery` on the browser fixture server with
282
- `bun browser-tests/server.ts`. The gallery covers control, widget, request,
283
- inspection, and narrow-layout defaults. `bun run test:browser` verifies desktop
284
- and phone interactions. Fixtures are excluded from the published package.
237
+ The callback in `view.content()` receives the displayed result and its original
238
+ input during refreshes and failures. Use that input when labeling the data.
285
239
 
286
240
  Previous data and evidence are retained only when the input changes within the
287
241
  same operation. Switching to another operation shows its own cached response or
@@ -292,8 +246,28 @@ from the same client share queries; separate clients do not share results, stale
292
246
  data, or cancellation even under one provider. Keep client instances stable
293
247
  across renders to preserve their cache.
294
248
 
249
+ ## Findings and inspection
250
+
251
+ `<DataWidget>` composes a body, toolbar feedback, and footer. Widgets and their
252
+ inspection sheets render the same visual and controls. Keep interactive state
253
+ above both mounts when authoring custom children.
254
+
295
255
  When a trusted parent supplies [parent presentation](embed.md#parent-presentation),
296
- `DataApp` follows its resolved theme and suppresses local theme controls,
256
+ `<DataApp>` follows its resolved theme and suppresses local theme controls,
297
257
  including in presentations. On an embedded surface (`surface: 'embedded'`),
298
258
  only toolbar actions remain above the app body; the title, scope, description, and
299
259
  footer are omitted. Variables and request states remain available.
260
+
261
+ ## Present data with stories
262
+
263
+ Stories turn the exploration's findings into a presentation. Set the `story` prop on `<DataApp>`
264
+ to enable **Present story** in the toolbar; use `<PresentStory>` directly for a
265
+ custom shell. Start from the [data app starter](../examples/starter-data-app/index.tsx).
266
+
267
+ Return one to four findings with a headline, a visual, and registered source
268
+ evidence. Use `context.finding()` to bind the evidence.
269
+
270
+ The callback receives the displayed data and its original input, including
271
+ during refresh or failure. Derive the story from that snapshot so it agrees with
272
+ the visible exploration. Initial loading, empty results, and initial errors have
273
+ no story to present.
@@ -1,6 +1,9 @@
1
1
  # Local Bun server
2
2
 
3
- Import `serveLocalApp` and `localLakehouse` from
3
+ Use this Bun server for local data apps. Start from the CLI scaffold or
4
+ [local data app starter](https://github.com/altertable-ai/data-app/tree/main/examples/starter-local-data-app).
5
+
6
+ Import `serveLocalApp()` and `localLakehouse()` from
4
7
  `@altertable/data-app/server/bun`. This entry requires Bun; install `@types/bun`
5
8
  when typechecking a Bun app.
6
9
 
@@ -24,8 +27,3 @@ Missing credentials fail the query. Keep these variables out of browser code.
24
27
  Local serving permits SQL disclosure and does not authenticate hosted viewers.
25
28
  Use [the portable server handler](server.md) with per-request authorization when
26
29
  hosting an app for other people.
27
-
28
- The local adapter validates NDJSON metadata, optional string or typed column
29
- headers, and array rows before returning a result. Query IDs must be strings.
30
- Rows must match the column count when a header is present; headerless array rows
31
- remain supported with empty column metadata. Empty results remain valid.
package/docs/server.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Server
2
2
 
3
- Import `createDataHandler` and the `RequestAccess` type from
3
+ Import `createDataHandler()` and the `RequestAccess` type from
4
4
  `@altertable/data-app/server`. The handler accepts a Web `Request` and returns a
5
5
  `Promise<Response>`. This entry can be imported in Node and Bun without loading
6
6
  React or the Bun local-development adapter.
@@ -21,7 +21,7 @@ export const handleDataRequest = createDataHandler(
21
21
  );
22
22
  ```
23
23
 
24
- The app supplies `authenticate` and `lakehouseFor`, then routes `/api/data/*`
24
+ The app supplies `authenticate()` and `lakehouseFor()`, then routes `/api/data/*`
25
25
  requests to the handler. Authorize every viewer and operation, and scope the
26
26
  returned lakehouse to the viewer's permitted data. Origin and Fetch Metadata
27
27
  checks reject cross-site browser requests; they do not authenticate viewers.
package/docs/worker.md CHANGED
@@ -41,10 +41,8 @@ return 403. Missing or invalid origin configuration returns 503; an absent
41
41
  selection requires an exact default origin. Referrers and messages never supply
42
42
  trust.
43
43
 
44
- The package owns the unstyled HTML (`#root` followed by an inline classic script),
45
- attribute and script escaping, restrictive CSP with configured `frame-ancestors`,
46
- `no-referrer`, and `nosniff`. App scripts load through the authenticated
47
- `altertable:data-app` bridge and retain opaque host state. Apps own styling and
44
+ The Worker enforces a restrictive CSP with configured `frame-ancestors`,
45
+ `no-referrer`, and `nosniff`. App scripts load through the authenticated bridge. Apps own styling and
48
46
  navigation. Bundle apps execute their operation registry in the browser and send
49
47
  SQL through the [host query route](embed.md#sql-query-route); the Worker does not
50
48
  resolve operation names or execute queries. Data requests require backend authorization.
@@ -53,5 +51,5 @@ If Terraform reads a file, copy the resolved `/worker` asset unchanged to that
53
51
  file during deployment preparation. Configure `DOMAIN_NAME`, `PARENT_ORIGINS`,
54
52
  domains, routes, and deployment settings in Terraform. Upgrade the host package
55
53
  and Worker together for protocol changes.
56
- Custom hosts that bundle their own bootstrap can use `startDataAppBootstrap` from
54
+ Custom hosts that bundle their own bootstrap can use `startDataAppBootstrap()` from
57
55
  [/embed](embed.md#trusted-bootstrap).
@@ -0,0 +1,159 @@
1
+ import { createDataClient } from '@altertable/data-app/client';
2
+ import type { DataAppConfig } from '@altertable/data-app/config';
3
+ import {
4
+ defineOperation,
5
+ defineQueryNames,
6
+ parseCount,
7
+ } from '@altertable/data-app/contract';
8
+ import {
9
+ createDataContext,
10
+ createDataHooks,
11
+ DataApp,
12
+ injectDataAppStyles,
13
+ mountDataApp,
14
+ MetricWidget,
15
+ textVariable,
16
+ } from '@altertable/data-app/react';
17
+
18
+ const queryNames = defineQueryNames({
19
+ sampleCountsByGroup: 'sample-counts-by-group',
20
+ });
21
+ function parseSampleCountFilter(value: unknown) {
22
+ if (
23
+ !value ||
24
+ typeof value !== 'object' ||
25
+ !('groupName' in value) ||
26
+ typeof value.groupName !== 'string' ||
27
+ value.groupName.length > 40
28
+ )
29
+ throw new Error('Expected a group name of at most 40 characters.');
30
+ return { groupName: value.groupName };
31
+ }
32
+ function parseSampleCounts(
33
+ value: unknown
34
+ ): { groupName: string; sampleCount: number }[] {
35
+ if (!Array.isArray(value))
36
+ throw new Error('Expected sample counts by group.');
37
+ return value.map(row => {
38
+ if (!row || typeof row !== 'object' || typeof row.groupName !== 'string')
39
+ throw new Error('Expected a group name.');
40
+ return {
41
+ groupName: row.groupName,
42
+ sampleCount: parseCount(row.sampleCount),
43
+ };
44
+ });
45
+ }
46
+ const operations = {
47
+ sampleCountsByGroup: defineOperation({
48
+ queryNames,
49
+ input: parseSampleCountFilter,
50
+ output: parseSampleCounts,
51
+ checks: [
52
+ { groupName: '' },
53
+ { groupName: 'Alpha' },
54
+ { groupName: 'missing' },
55
+ ],
56
+ policy: { maxQueryRows: 10, maxDurationMs: 15000, exposeSql: true },
57
+ async run({ query }, { groupName }) {
58
+ // Portable sample data, not a production table. Escape the validated SQL literal.
59
+ const escapedGroupName = groupName.replaceAll("'", "''");
60
+ const queryResult = await query(
61
+ queryNames.sampleCountsByGroup,
62
+ `
63
+ WITH sample_counts(group_name, sample_count) AS (VALUES ('Alpha', 3), ('Beta', 0))
64
+ SELECT group_name, sample_count FROM sample_counts
65
+ WHERE '${escapedGroupName}' = '' OR group_name = '${escapedGroupName}'
66
+ ORDER BY group_name LIMIT 10`
67
+ );
68
+ return parseSampleCounts(
69
+ queryResult.rows.map(([groupName, sampleCount]) => ({
70
+ groupName,
71
+ sampleCount,
72
+ }))
73
+ );
74
+ },
75
+ }),
76
+ };
77
+ const appConfig: DataAppConfig = {
78
+ title: 'Sample counts',
79
+ scope: { organization: 'demo', environment: 'sample' },
80
+ appearance: { theme: 'system' },
81
+ };
82
+ const sampleDataContext = createDataContext(queryNames)({
83
+ description:
84
+ 'Two SQL VALUES rows demonstrate the host query path. Replace them with inspected source data before publishing findings.',
85
+ glossary: {
86
+ sampleCount: {
87
+ term: 'Sample count',
88
+ definition: 'A fixture value: Alpha is 3 and Beta is a measured zero.',
89
+ queryNames: [queryNames.sampleCountsByGroup],
90
+ },
91
+ },
92
+ });
93
+ const { defineDataView, useView } = createDataHooks(
94
+ createDataClient({ operations })
95
+ );
96
+ const sampleCountsView = defineDataView({
97
+ operation: 'sampleCountsByGroup',
98
+ variables: {
99
+ groupName: textVariable({ key: 'group', label: 'Group', defaultValue: '' }),
100
+ },
101
+ input: ({ groupName }) => ({ groupName }),
102
+ describeInput: ({ groupName }) =>
103
+ groupName ? `group ${groupName}` : 'all groups',
104
+ isEmpty: sampleCounts => sampleCounts.length === 0,
105
+ empty: {
106
+ title: 'No matching groups',
107
+ description: 'Try Alpha, Beta, or clear the group filter.',
108
+ },
109
+ });
110
+ function App() {
111
+ const sampleCountsRequest = useView(sampleCountsView);
112
+ return (
113
+ <DataApp
114
+ 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
+ }
141
+ >
142
+ {(sampleCounts, displayedInput) => (
143
+ <section aria-label="Sample results">
144
+ <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
+ )}
155
+ </DataApp>
156
+ );
157
+ }
158
+ injectDataAppStyles();
159
+ mountDataApp({ config: appConfig, component: App });