@altertable/data-app 0.62.0 → 0.63.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 (140) hide show
  1. package/AGENTS.md +4 -34
  2. package/CONTRIBUTING.md +63 -75
  3. package/README.md +14 -38
  4. package/dist/chunks/{contract-xjv197ck.js → contract-8wdybxj7.js} +8 -8
  5. package/dist/chunks/{contract-xjv197ck.js.map → contract-8wdybxj7.js.map} +3 -3
  6. package/dist/chunks/{contract-ryyf6dme.js → contract-cxr9t12b.js} +2 -5
  7. package/dist/chunks/contract-cxr9t12b.js.map +10 -0
  8. package/dist/chunks/contract-farfe948.js.map +2 -2
  9. package/dist/chunks/contract-mev09s5v.js.map +2 -2
  10. package/dist/chunks/{contract-3bnrf5pf.js → contract-tf8c3qpv.js} +11 -3
  11. package/dist/chunks/contract-tf8c3qpv.js.map +11 -0
  12. package/dist/chunks/{contract-4vrw9zk9.js → contract-tkc552ze.js} +69 -60
  13. package/dist/chunks/contract-tkc552ze.js.map +12 -0
  14. package/dist/chunks/{contract-nt819swq.js → contract-wd8qe3mt.js} +6 -1
  15. package/dist/chunks/{contract-nt819swq.js.map → contract-wd8qe3mt.js.map} +3 -3
  16. package/dist/chunks/contract-wz59z8pq.js.map +1 -1
  17. package/dist/chunks/{contract-rrm7s5zp.js → contract-zr3jd7mr.js} +5 -5
  18. package/dist/chunks/contract-zr3jd7mr.js.map +12 -0
  19. package/dist/client/index.js +7 -8
  20. package/dist/client/index.js.map +1 -1
  21. package/dist/core/contract.js +2 -2
  22. package/dist/embed/index.js +9 -11
  23. package/dist/embed/index.js.map +2 -2
  24. package/dist/local.js +7 -31
  25. package/dist/local.js.map +5 -7
  26. package/dist/react/embed/index.js +2 -3
  27. package/dist/react/embed/index.js.map +2 -2
  28. package/dist/react/index.js +5437 -221
  29. package/dist/react/index.js.map +70 -65
  30. package/dist/server.js +7 -31
  31. package/dist/server.js.map +5 -7
  32. package/dist/types/client/data-client.d.ts +33 -0
  33. package/dist/types/client/index.d.ts +5 -41
  34. package/dist/types/client/location.d.ts +4 -6
  35. package/dist/types/client/navigation.d.ts +2 -1
  36. package/dist/types/core/bridge.d.ts +2 -8
  37. package/dist/types/core/config.d.ts +1 -1
  38. package/dist/types/core/contract.d.ts +9 -56
  39. package/dist/types/core/format.d.ts +0 -1
  40. package/dist/types/core/messages.d.ts +5 -30
  41. package/dist/types/core/navigation.d.ts +11 -0
  42. package/dist/types/core/operation-types.d.ts +76 -0
  43. package/dist/types/core/operation.d.ts +2 -8
  44. package/dist/types/core/variables.d.ts +2 -2
  45. package/dist/types/embed/host.d.ts +5 -3
  46. package/dist/types/embed/index.d.ts +0 -1
  47. package/dist/types/embed/source.d.ts +2 -9
  48. package/dist/types/react/content.d.ts +3 -3
  49. package/dist/types/react/hooks.d.ts +65 -64
  50. package/dist/types/react/index.d.ts +135 -2
  51. package/dist/types/react/injectStyles.d.ts +7 -0
  52. package/dist/types/react/shellStyles.d.ts +3 -0
  53. package/dist/types/react/styles.d.ts +8 -0
  54. package/dist/types/react/ui/AboutData.d.ts +0 -1
  55. package/dist/types/react/ui/AppFooter.d.ts +0 -1
  56. package/dist/types/react/ui/AppHeader.d.ts +0 -1
  57. package/dist/types/react/ui/AppLayout.d.ts +0 -1
  58. package/dist/types/react/ui/AppScope.d.ts +0 -1
  59. package/dist/types/react/ui/AppToolbar.d.ts +0 -1
  60. package/dist/types/react/ui/Breakdown.d.ts +0 -1
  61. package/dist/types/react/ui/Button.d.ts +0 -1
  62. package/dist/types/react/ui/Checkbox.d.ts +0 -1
  63. package/dist/types/react/ui/Combobox.d.ts +0 -1
  64. package/dist/types/react/ui/ComparisonVisual.d.ts +0 -1
  65. package/dist/types/react/ui/ContentSkeleton.d.ts +2 -5
  66. package/dist/types/react/ui/DataApp.d.ts +2 -2
  67. package/dist/types/react/ui/DataAppSkeleton.d.ts +0 -1
  68. package/dist/types/react/ui/DataBoundary.d.ts +0 -1
  69. package/dist/types/react/ui/DataSection.d.ts +2 -4
  70. package/dist/types/react/ui/DataTable.d.ts +2 -3
  71. package/dist/types/react/ui/DataViewToast.d.ts +0 -1
  72. package/dist/types/react/ui/DataWidget.d.ts +4 -14
  73. package/dist/types/react/ui/DateRangePicker.d.ts +0 -1
  74. package/dist/types/react/ui/DateTimeTooltip.d.ts +0 -1
  75. package/dist/types/react/ui/EmptyState.d.ts +2 -5
  76. package/dist/types/react/ui/GettingStarted.d.ts +0 -1
  77. package/dist/types/react/ui/GlossaryDefinition.d.ts +0 -1
  78. package/dist/types/react/ui/GlossaryExplanation.d.ts +0 -1
  79. package/dist/types/react/ui/GradientScroll.d.ts +0 -1
  80. package/dist/types/react/ui/Grid.d.ts +0 -1
  81. package/dist/types/react/ui/HelpPopover.d.ts +0 -2
  82. package/dist/types/react/ui/Kbd.d.ts +0 -1
  83. package/dist/types/react/ui/LiveControl.d.ts +0 -1
  84. package/dist/types/react/ui/MetricWidget.d.ts +0 -2
  85. package/dist/types/react/ui/PeriodSummary.d.ts +0 -1
  86. package/dist/types/react/ui/PresentStory.d.ts +0 -1
  87. package/dist/types/react/ui/QueryList.d.ts +0 -1
  88. package/dist/types/react/ui/Ranking.d.ts +0 -1
  89. package/dist/types/react/ui/RefreshControl.d.ts +0 -1
  90. package/dist/types/react/ui/RefreshRegion.d.ts +0 -1
  91. package/dist/types/react/ui/RequestHint.d.ts +0 -1
  92. package/dist/types/react/ui/SearchField.d.ts +0 -1
  93. package/dist/types/react/ui/SearchInput.d.ts +0 -1
  94. package/dist/types/react/ui/SearchMatch.d.ts +0 -1
  95. package/dist/types/react/ui/SelectableBarChart.d.ts +0 -1
  96. package/dist/types/react/ui/SelectionMark.d.ts +0 -1
  97. package/dist/types/react/ui/Sheet.d.ts +0 -1
  98. package/dist/types/react/ui/Skeleton.d.ts +0 -1
  99. package/dist/types/react/ui/Stack.d.ts +0 -1
  100. package/dist/types/react/ui/StatusPanel.d.ts +0 -1
  101. package/dist/types/react/ui/TableWidget.d.ts +2 -3
  102. package/dist/types/react/ui/Tabs.d.ts +0 -1
  103. package/dist/types/react/ui/TextContent.d.ts +4 -0
  104. package/dist/types/react/ui/TextWidget.d.ts +19 -0
  105. package/dist/types/react/ui/ThemeSelector.d.ts +0 -1
  106. package/dist/types/react/ui/Tooltip.d.ts +0 -1
  107. package/dist/types/react/ui/UpdatedAt.d.ts +0 -1
  108. package/dist/types/react/ui/VariableBar.d.ts +0 -1
  109. package/dist/types/react/ui/VisualizationWidget.d.ts +5 -13
  110. package/dist/types/react/ui/WidgetDisclosure.d.ts +0 -1
  111. package/dist/types/react/ui/WidgetViewTabs.d.ts +2 -3
  112. package/dist/types/react/ui/data-identifiers.d.ts +0 -1
  113. package/dist/types/react/ui/presentation.d.ts +19 -0
  114. package/dist/types/react/view-controls.d.ts +1 -1
  115. package/dist/types/react/view.d.ts +2 -2
  116. package/docs/app-authoring.md +60 -44
  117. package/docs/client.md +11 -19
  118. package/docs/contract.md +9 -9
  119. package/docs/embed.md +10 -29
  120. package/docs/hosted-apps.md +39 -0
  121. package/docs/local-data-apps.md +14 -0
  122. package/docs/react-embed.md +10 -19
  123. package/docs/react.md +108 -134
  124. package/docs/server-bun.md +4 -6
  125. package/docs/server.md +2 -2
  126. package/docs/worker.md +3 -5
  127. package/examples/starter-data-app/index.tsx +159 -0
  128. package/package.json +16 -10
  129. package/dist/chunks/contract-3bnrf5pf.js.map +0 -10
  130. package/dist/chunks/contract-4vrw9zk9.js.map +0 -12
  131. package/dist/chunks/contract-rrm7s5zp.js.map +0 -12
  132. package/dist/chunks/contract-ryyf6dme.js.map +0 -10
  133. package/dist/react/index.css +0 -4628
  134. package/dist/react.css.d.ts +0 -1
  135. package/dist/types/react/ui/index.d.ts +0 -131
  136. package/docs/appearance.md +0 -43
  137. package/docs/config.md +0 -21
  138. package/docs/format.md +0 -29
  139. package/docs/react-styles.md +0 -17
  140. package/docs/starter-agent-instructions.md +0 -53
@@ -2,10 +2,9 @@ import { type ComponentPropsWithRef, type ReactNode } from 'react';
2
2
  import type { WidgetEvidence } from './WidgetEvidence.js';
3
3
  import type { WidgetStatus } from './RequestHint.js';
4
4
  import { type DataTableSearch } from './DataTable.js';
5
- import type { EmptyStateProps } from './EmptyState.js';
5
+ import type { EmptyContent } from './presentation.js';
6
6
  import { type SearchHit, type SearchItemsOptions } from './searchItems.js';
7
7
  import type { DataReading } from '../../core/reading.js';
8
-
9
8
  export type TableWidgetColumn<Row> = {
10
9
  /** Stable, nonempty identity; unique within this table. */
11
10
  id: string;
@@ -28,7 +27,7 @@ type TableWidgetBaseProps<Row> = {
28
27
  evidence?: WidgetEvidence;
29
28
  search?: TableWidgetSearch<Row>;
30
29
  /** Valid result with no rows; the header remains visible. */
31
- empty: Pick<EmptyStateProps, 'title' | 'description'>;
30
+ empty: EmptyContent;
32
31
  } & ({
33
32
  /** Positive integer preview cap after search; disables pagination. */
34
33
  limit: number;
@@ -1,5 +1,4 @@
1
1
  import { Tabs, TabList, Tab, TabPanels, TabPanel } from 'react-aria-components/Tabs';
2
-
3
2
  /** React Aria tabs with keyboard and ARIA behavior; pair each Tab and TabPanel by stable id. */
4
3
  export { Tabs, TabList, Tab, TabPanels, TabPanel };
5
4
  /** Keep a page-level tab in `?view=`; reserve `?tab=` for the inspect sheet. */
@@ -0,0 +1,4 @@
1
+ import type { ComponentPropsWithRef } from 'react';
2
+ export type TextContentProps = ComponentPropsWithRef<'div'>;
3
+ /** Prose typography for page introductions, widget bodies, and inspection content. */
4
+ export declare function TextContent({ className, ...props }: TextContentProps): import("react").JSX.Element;
@@ -0,0 +1,19 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { DataReading } from '../../core/reading.js';
3
+ import { type DataWidgetProps } from './DataWidget.js';
4
+ import type { WidgetEvidence } from './WidgetEvidence.js';
5
+ type TextWidgetBaseProps = Omit<Extract<DataWidgetProps, {
6
+ reading?: never;
7
+ }>, 'reading' | 'children' | 'isEmpty' | 'empty' | 'skeleton' | 'bodyPadding'>;
8
+ /** Framed narrative context. Bind claims and scope to the same displayed reading.
9
+ * The renderer handles every ready value, including zero and empty collections. */
10
+ export type TextWidgetProps<Data = unknown> = TextWidgetBaseProps & ({
11
+ reading: DataReading<Data>;
12
+ evidence: WidgetEvidence;
13
+ children: (data: Data) => ReactNode;
14
+ } | {
15
+ reading?: never;
16
+ children: ReactNode;
17
+ });
18
+ export declare function TextWidget<Data>(props: TextWidgetProps<Data>): import("react").JSX.Element;
19
+ export {};
@@ -1,6 +1,5 @@
1
1
  import { type ComponentPropsWithRef, type ReactNode, type RefObject } from 'react';
2
2
  import type { ThemeController } from '../../core/appearance.js';
3
-
4
3
  export type ThemeSelectorProps = {
5
4
  theme: ThemeController;
6
5
  children?: ReactNode;
@@ -1,5 +1,4 @@
1
1
  import { type ComponentPropsWithRef, type ReactNode, type RefObject } from 'react';
2
-
3
2
  export type TooltipProviderProps = {
4
3
  children: ReactNode;
5
4
  delay?: number;
@@ -1,6 +1,5 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { type DateTimeTooltipProps } from './DateTimeTooltip.js';
3
-
4
3
  export type UpdatedAtProps = {
5
4
  timestamp: number;
6
5
  locale?: string;
@@ -1,5 +1,4 @@
1
1
  import type { ComponentPropsWithRef, ReactNode } from 'react';
2
-
3
2
  export type VariableBarProps = {
4
3
  children: ReactNode;
5
4
  } & Omit<ComponentPropsWithRef<'div'>, 'children'>;
@@ -1,18 +1,15 @@
1
1
  import { type ComponentPropsWithRef, type ReactNode } from 'react';
2
2
  import type { WidgetEvidence } from './WidgetEvidence.js';
3
- import { type DataWidgetProps } from './DataWidget.js';
4
- import type { EmptyStateProps } from './EmptyState.js';
5
- import type { DataReading } from '../../core/reading.js';
6
- import { type ContentSkeletonProps } from './ContentSkeleton.js';
7
-
3
+ import type { WidgetStatus } from './RequestHint.js';
4
+ import type { EmptyContent, BoundWidgetReading } from './presentation.js';
8
5
  type VisualizationWidgetBaseProps = {
9
6
  title: ReactNode;
10
7
  description?: ReactNode;
11
8
  insight?: ReactNode;
12
9
  action?: ReactNode;
13
10
  evidence?: WidgetEvidence;
14
- status?: DataWidgetProps['status'];
15
- empty?: Pick<EmptyStateProps, 'title' | 'description'>;
11
+ status?: WidgetStatus;
12
+ empty?: EmptyContent;
16
13
  } & Omit<ComponentPropsWithRef<'section'>, 'about' | 'title' | 'children'>;
17
14
  type UnboundVisualizationWidgetProps = VisualizationWidgetBaseProps & ({
18
15
  loading: true;
@@ -27,12 +24,7 @@ export type VisualizationWidgetView<Data> = {
27
24
  label: ReactNode;
28
25
  render: (data: Data) => ReactNode;
29
26
  };
30
- type BoundVisualizationWidgetBase<Data> = VisualizationWidgetBaseProps & {
31
- evidence: WidgetEvidence;
32
- reading: DataReading<Data>;
33
- isEmpty: (data: Data) => boolean;
34
- empty: Pick<EmptyStateProps, 'title' | 'description'>;
35
- skeleton?: Pick<ContentSkeletonProps, 'variant' | 'rows'>;
27
+ type BoundVisualizationWidgetBase<Data> = VisualizationWidgetBaseProps & BoundWidgetReading<Data> & {
36
28
  visual?: never;
37
29
  loading?: never;
38
30
  };
@@ -1,5 +1,4 @@
1
1
  import type { ComponentPropsWithRef, ReactNode } from 'react';
2
-
3
2
  export type WidgetDisclosureProps = {
4
3
  label: ReactNode;
5
4
  children: ReactNode;
@@ -1,11 +1,10 @@
1
1
  import type { ComponentPropsWithRef, ReactNode } from 'react';
2
- import { type EmptyStateProps } from './EmptyState.js';
3
-
2
+ import type { EmptyContent } from './presentation.js';
4
3
  export type WidgetView = {
5
4
  id: string;
6
5
  label: ReactNode;
7
6
  content: ReactNode;
8
- empty: Pick<EmptyStateProps, 'title' | 'description'>;
7
+ empty: EmptyContent;
9
8
  isEmpty: boolean;
10
9
  };
11
10
  export type WidgetViewTabsProps<Views extends readonly WidgetView[] = readonly WidgetView[]> = {
@@ -1,4 +1,3 @@
1
-
2
1
  export type TableIdentifier = {
3
2
  catalog: string;
4
3
  schema: string;
@@ -0,0 +1,19 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { DataReading } from '../../core/reading.js';
3
+ import type { WidgetEvidence } from './WidgetEvidence.js';
4
+ export type EmptyContent = {
5
+ title: ReactNode;
6
+ description?: ReactNode;
7
+ };
8
+ export type SkeletonContent = {
9
+ variant: 'metric' | 'panel' | 'ranking';
10
+ rows?: number;
11
+ };
12
+ /** A bound widget must explain empty results and link the reading to its evidence. */
13
+ export type BoundWidgetReading<Data> = {
14
+ reading: DataReading<Data>;
15
+ isEmpty: (data: Data) => boolean;
16
+ empty: EmptyContent;
17
+ evidence: WidgetEvidence;
18
+ skeleton?: SkeletonContent;
19
+ };
@@ -6,7 +6,7 @@ export declare function useViewVariables<Variables extends VariableCollection>(d
6
6
  values: AppVariableValues<Variables>;
7
7
  set: <Key extends keyof Variables & string>(name: Key, value: AppVariableValues<Variables>[Key]) => void;
8
8
  reset: <Key extends keyof Variables & string>(name: Key) => void;
9
- update: (next: Partial<AppVariableValues<Variables>>, history?: import("../core/variables.js").HistoryMode) => void;
9
+ update: (next: Partial<AppVariableValues<Variables>>, history?: import("../core/navigation.js").HistoryMode) => void;
10
10
  bind: <Key extends keyof Variables & string>(name: Key) => {
11
11
  value: AppVariableValues<Variables>[Key];
12
12
  onChange(value: AppVariableValues<Variables>[Key]): void;
@@ -1,4 +1,4 @@
1
- import type { EmptyStateProps } from './ui/EmptyState.js';
1
+ import type { EmptyContent } from './ui/presentation.js';
2
2
  import type { AppVariableValues, DateRangeVariable, VariableCollection } from '../core/variables.js';
3
3
  import type { DateRangeRequest } from '../core/contract.js';
4
4
  export type ResolvedVariables<Variables extends VariableCollection> = {
@@ -23,7 +23,7 @@ export type DataViewDefinition<Name extends string, Variables extends VariableCo
23
23
  bindings?: ViewBindings<Variables, Input>;
24
24
  /** App-owned semantics: measured zero need not mean an empty result. */
25
25
  isEmpty: (data: Data) => boolean;
26
- empty: Pick<EmptyStateProps, 'title' | 'description'>;
26
+ empty: EmptyContent;
27
27
  } & ({
28
28
  date: ViewDate<Variables, Input>;
29
29
  describeInput?: (input: Input) => string;
@@ -1,46 +1,62 @@
1
1
  # Author a data app
2
2
 
3
- An app owns its question, SQL, result parsing, business definitions, configuration,
4
- and presentation. The package supplies contracts, request handling, typed client
5
- calls, and reusable React UI.
6
-
7
- 1. Inspect the source data, time coverage, and existing definitions before choosing
8
- the exploration. Build findings from observed results and distinguish
9
- association from cause.
10
- 2. Define named, bounded [operations](contract.md) in the execution runtime. Put shared input
11
- contracts in a browser-safe module and validate outputs before returning them.
12
- 3. For HTTP apps, use a [server handler](server.md) that authorizes each request, or the
13
- [Bun adapter](server-bun.md) for local development.
14
- 4. Create a typed [client](client.md) and compose [React views](react.md). Import
15
- the operation registry with `import type` for HTTP apps, or as a value for bundle apps.
16
- 5. Define glossary and query evidence, handle empty results, and preserve the
17
- displayed input while a request refreshes or fails. A measured zero and an
18
- unavailable value must remain distinct.
19
- 6. Verify the app's findings and interactions against the original question.
20
- Inspect loading, error, stale, and empty states at desktop and phone widths.
21
-
22
- Import through `@altertable/data-app/<entry>`. Installed package files are
23
- dependencies; customize the app's own source rather than editing `node_modules`.
24
- For HTTP apps, SQL, credentials, and viewer authorization belong on the server.
25
- For bundle apps, define operations in the browser and pass them to
26
- `createDataClient({ operations })`; SQL travels through the authorized
27
- [SQL bridge](embed.md#sql-query-route). Credentials, viewer authorization, and
28
- enforced query limits remain backend-owned.
29
-
30
- ## Execution ownership
31
-
32
- The client selects one of two paths when it is created:
33
-
34
- | App model | Operation execution | Query delivery |
35
- | ---------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
36
- | HTTP app | The browser sends a name and input; the server authorizes the request and runs its registered operation. | A server-owned lakehouse adapter executes SQL. |
37
- | Bundle app | The browser directly runs the registry passed to `createDataClient({ operations })`. | `bridge.lakehouse` sends SQL to the host, which authorizes each query and supplies a backend adapter. |
38
-
39
- Both paths use the same internal operation executor for input/output validation,
40
- query names, row bounds, deadlines, evidence, and response serialization limits.
41
- The executor accepts a `Lakehouse` and never chooses a transport or authorizes a
42
- viewer. Each adapter owns its boundary: HTTP owns request parsing and responses;
43
- the bridge owns message delivery; the SQL host owns authorization and public query
44
- errors. Backend permissions and resource limits apply independently of app policy.
45
-
46
- For agent-assisted authoring, see the [starter AGENTS.md template](starter-agent-instructions.md).
3
+ Build an exploration that answers the user's question and a story that presents
4
+ its strongest findings. Both use the same queries, definitions, and evidence.
5
+
6
+ ## Inspect the data
7
+
8
+ Inspect the relevant catalogs, tables, and fields, their time coverage, and
9
+ existing definitions. Choose a question the available data can answer.
10
+
11
+ ## Build the exploration
12
+
13
+ Choose the execution path:
14
+
15
+ | App | Guide |
16
+ | ---------------------------------- | --------------------------------------- |
17
+ | Data app (hosted / remote / cloud) | [Single-file authoring](hosted-apps.md) |
18
+ | Local data app | [Local authoring](local-data-apps.md) |
19
+
20
+ Lead with a supported finding and expose the relevant fields as filter variables. Use a date filter for questions worth exploring over time,
21
+ or a fixed period snapshot for a deliberate historical analysis.
22
+ Register inspected tables and fields with `defineDataIdentifiers()` and use
23
+ `<DataIdentifier>` when naming sources. Register terms and query evidence so
24
+ readers can inspect the source of each claim.
25
+
26
+ Connect visualizations with introductions and explanations. Use `<TextWidget>`
27
+ for a narrative panel with the standard widget frame, or `<TextContent>` for
28
+ borderless prose. Bind claims to `result.select((data, input) => ...)` so their
29
+ values and scope follow the displayed results through filter changes, refresh,
30
+ and failure.
31
+
32
+ | Task | Documentation |
33
+ | ------------------------------------------ | ---------------------------------------------------------- |
34
+ | Define queries, inputs, and result parsing | [Operations](contract.md) |
35
+ | Build views, filters, and request states | [React](react.md) |
36
+ | Introduce and explain visualizations | [Narrative text](react.md#narrative-text) |
37
+ | Find formatters and presentation helpers | [App helpers](react.md#reuse-app-helpers) |
38
+ | Register source names | [Source identifiers](react.md#register-source-identifiers) |
39
+ | Choose date and field filters | [Filter variables](react.md#time-views-and-field-filters) |
40
+ | Handle refresh and stale results | [Displayed results](react.md#preserve-displayed-results) |
41
+ | Bind definitions and source evidence | [Data context](react.md#bind-evidence) |
42
+
43
+ Use the exported types for configuration, appearance, formatting, and component
44
+ options.
45
+
46
+ ## Present the findings
47
+
48
+ Compose a [story](react.md#present-data-with-stories) from the exploration's
49
+ findings. Lead with the answer, then show the evidence and comparisons that
50
+ explain it. Select the findings that matter to the audience; do not turn every
51
+ row or chart into a step.
52
+
53
+ ## Verify the app
54
+
55
+ Verify findings against the source and the user's question. Distinguish measured
56
+ zero, unavailable values, and empty results. Check filters, refresh, loading,
57
+ empty, error, and stale states, then present the story. Inspect both experiences
58
+ at phone and desktop widths in light and dark themes.
59
+
60
+ The app owns its queries, result parsing, business definitions, configuration,
61
+ and presentation. Credentials, authorization, and enforced access/query limits
62
+ stay backend-owned. Edit app-owned files; installed package files are dependencies.
package/docs/client.md CHANGED
@@ -1,9 +1,11 @@
1
1
  # Client
2
2
 
3
- Import `createDataClient` and `DataAppError` from
3
+ Import `createDataClient()` and `DataAppError` from
4
4
  `@altertable/data-app/client`. The client uses Fetch APIs and has no React or
5
5
  server dependency.
6
6
 
7
+ ## HTTP operations
8
+
7
9
  ```ts
8
10
  import { createDataClient } from '@altertable/data-app/client';
9
11
  import type { operations } from './operations';
@@ -36,6 +38,8 @@ See [contracts](contract.md), [server handlers](server.md), and
36
38
 
37
39
  ## Browser-owned operations for bundle apps
38
40
 
41
+ Start with the complete [single-file example](../examples/starter-data-app/index.tsx)
42
+ and [data app authoring guide](hosted-apps.md).
39
43
  Bundle apps pass their operation registry as a value:
40
44
 
41
45
  ```ts
@@ -49,8 +53,7 @@ const response = await client.query('connection', {});
49
53
  ```
50
54
 
51
55
  The client runs input parsing, operation logic, and output parsing in the browser.
52
- It uses the same executor as the server handler for query names, row and duration
53
- bounds, response size, and query evidence. Each query sends `{ statement, limit }`
56
+ Operation policy bounds rows, duration, and response size and records query evidence. Each query sends `{ statement, limit }`
54
57
  to the installed iframe bridge's `data:sql` route; the host needs no operation
55
58
  registry. SQL is visible in the browser, even when `exposeSql` is false; that flag
56
59
  only controls evidence in the returned response. Credentials remain backend-owned.
@@ -72,7 +75,7 @@ the host through the existing bridge cancellation protocol.
72
75
  `createDataClient({ transport })` accepts a `DataTransport`. Without an explicit
73
76
  transport, endpoint, or Fetch implementation, it discovers an installed iframe
74
77
  transport before falling back to HTTP. Explicit endpoint/Fetch options select
75
- HTTP. `createHttpTransport` provides the underlying operation delivery adapter.
78
+ HTTP. `createHttpTransport()` provides the underlying operation delivery adapter.
76
79
 
77
80
  A URL-hosted app configures trust and installs the bridge before mounting:
78
81
 
@@ -93,8 +96,8 @@ URL controls attach the optional navigation adapter to that connection. Cleanup
93
96
  the installation and disposes pending work. Bundle apps receive this installation
94
97
  from the [trusted bootstrap](embed.md#trusted-bootstrap).
95
98
 
96
- `getDataAppTransport()` returns the explicitly installed bridge. Its `request`
97
- transport supports custom typed routes:
99
+ `getDataAppTransport()` returns the explicitly installed bridge. Its `request()`
100
+ method supports custom typed routes:
98
101
 
99
102
  ```ts
100
103
  import {
@@ -129,7 +132,7 @@ navigation.publish('replace');
129
132
  navigation.dispose();
130
133
  ```
131
134
 
132
- The adapter provides `snapshot`, `subscribe`, and `update`. Opaque sandboxes keep
135
+ The adapter provides `snapshot()`, `subscribe()`, and `update()`. Opaque sandboxes keep
133
136
  search/hash in memory; URL frames preserve their URL and local-preview parent
134
137
  marker. Host Back/Forward state is applied without publishing it back.
135
138
 
@@ -137,14 +140,6 @@ marker. Host Back/Forward state is applied without publishing it back.
137
140
  and shares one adapter per document. React mounting and URL-backed controls call
138
141
  it automatically. Apps that only use data delivery do not attach navigation.
139
142
 
140
- Migration: replace `bridge.appLocation` with the navigation adapter and
141
- `bridge.location(mode)` with `navigation.publish(mode)`. The version-1 wire envelope
142
- now carries host context in `initialize.state` and subsequent `state` messages;
143
- upgrade independently deployed hosts and runtimes together.
144
- Transport state is shared per window across separately bundled entry points;
145
- install only one transport per document. Public error classes retain `instanceof`
146
- recognition across independent bootstrap and app bundles in that window.
147
-
148
143
  ## Pending request limits
149
144
 
150
145
  Each iframe bridge accepts at most 128 unresolved requests, including requests
@@ -152,10 +147,7 @@ waiting for the connection handshake. Further calls reject with `bridge_busy`
152
147
  until a pending call completes, is cancelled, or times out. This bounds the
153
148
  client's promises, timers, and queued messages.
154
149
 
155
- The host independently limits pending requests to 128: an iframe can send
156
- messages directly without using the client helper. The shared cap is a resource
157
- policy, not a requirement of the message protocol. It rejects excess requests;
158
- it does not queue them or limit the total number of calls over a session.
150
+ Hosts enforce their own request limits independently of the client.
159
151
 
160
152
  A failed local-preview verification can be retried by the next explicit data
161
153
  request. Concurrent callers share the current verification attempt; aborting
package/docs/contract.md CHANGED
@@ -7,8 +7,8 @@ should import their operation types using `import type`.
7
7
 
8
8
  ## Execute named queries
9
9
 
10
- The app supplies `calendar`, `parseActivity`, `checkInput`, `buildActivitySql`,
11
- and `parseActivityRows` in this example.
10
+ The app supplies `calendar`, `parseActivity()`, `checkInput`, `buildActivitySql()`,
11
+ and `parseActivityRows()` in this example.
12
12
 
13
13
  ```ts
14
14
  import {
@@ -30,14 +30,14 @@ const activity = defineOperation({
30
30
  });
31
31
  ```
32
32
 
33
- `query` inherits the operation's limit and cancellation signal; `{ limit }` can lower a particular query's bound. Names are checked by TypeScript and at runtime. The executor records the SQL and query ID when execution occurs, so evidence does not need a separate result field. HTTP browser modules import operation types with `import type`. Bundle apps import their browser-owned operation registry as a value and use [browser execution](client.md#browser-owned-operations-for-bundle-apps). Never bundle credentials or server adapters.
33
+ `query()` inherits the operation's limit and cancellation signal; `{ limit }` can lower a particular query's bound. Names are checked by TypeScript and at runtime. Responses include executed SQL and query IDs when disclosure is allowed. HTTP browser modules import operation types with `import type`. Bundle apps import their browser-owned operation registry as a value and use [browser execution](client.md#browser-owned-operations-for-bundle-apps). Never bundle credentials or server adapters.
34
34
 
35
35
  ## Shared date ranges
36
36
 
37
- Use `defineDateRangeContract` in a browser-safe module to share source coverage,
37
+ Use `defineDateRangeContract()` in a browser-safe module to share source coverage,
38
38
  time zone, maximum range, and comparison rules between the server parser and
39
- React variables. The server uses `calendar.parseRequest`; React uses the same
40
- contract with `dateRangeVariable`.
39
+ React variables. The server uses `calendar.parseRequest()`; React uses the same
40
+ contract with `dateRangeVariable()`.
41
41
 
42
42
  ```ts
43
43
  import { defineDateRangeContract } from '@altertable/data-app/contract';
@@ -49,7 +49,7 @@ export const calendar = defineDateRangeContract({
49
49
  });
50
50
  ```
51
51
 
52
- `parseEmptyInput`, `parseTrue`, `parseCount`, and `parseDateRangeInput` validate
52
+ `parseEmptyInput()`, `parseTrue()`, `parseCount()`, and `parseDateRangeInput()` validate
53
53
  common inputs and results. `connectionCheck()` defines a bounded connectivity
54
54
  operation. A successful connectivity check confirms access; it is not an
55
55
  analysis result.
@@ -83,14 +83,14 @@ const router = createMessageRouter(routes, {
83
83
  });
84
84
  ```
85
85
 
86
- The app defines `parseString` to validate unknown values. `router.dispatch` checks
86
+ The app defines `parseString()` to validate unknown values. `router.dispatch()` checks
87
87
  registered routes, input, output, and cancellation. `MessageRoutingError` exposes
88
88
  an intentional public code, message, and optional request ID; other handler
89
89
  errors are replaced with a generic failure.
90
90
 
91
91
  `defineDataQueryRoute(operationContracts)` validates the selected operation's
92
92
  input and output and preserves its response evidence. It infers the result type
93
- from the selected operation when used with `createMessageClient`. Share input and
93
+ from the selected operation when used with `createMessageClient()`. Share input and
94
94
  output parsers, not operation implementations containing SQL or credentials.
95
95
  `dataAppRoutes` supplies generic `data:query` and `navigation:update` contracts;
96
96
  generic hosts must delegate operation validation and authorization to their
package/docs/embed.md CHANGED
@@ -6,7 +6,7 @@ app UI stylesheet.
6
6
 
7
7
  ## Sources
8
8
 
9
- `attachDataAppBridge` in source mode owns iframe loading, sandbox policy, startup timeout, and
9
+ `attachDataAppBridge()` in source mode owns iframe loading, sandbox policy, startup timeout, and
10
10
  message delivery. The host supplies an iframe and a validated message dispatcher:
11
11
 
12
12
  ```ts
@@ -67,13 +67,13 @@ img-src data: blob:; connect-src 'none'; base-uri 'none'; form-action 'none'`.
67
67
  The initializer installs a shared transport before evaluating the app script;
68
68
  data clients discover it even when independently bundled. The bootstrap contains
69
69
  no app navigation adapter. React mounting or URL controls attach navigation in the
70
- app bundle; non-React apps use `createDataAppNavigation` from `/client`.
70
+ app bundle; non-React apps use `createDataAppNavigation()` from `/client`.
71
71
 
72
72
  ## Delivery and navigation
73
73
 
74
- `attachDataAppBridge` also supports connection mode for a host-owned iframe. Supply
74
+ `attachDataAppBridge()` also supports connection mode for a host-owned iframe. Supply
75
75
  `connection: { type: 'origin', origin }` or `{ type: 'opaque', token }`, and an
76
- `onMessage` dispatcher. Both modes use the same transport. It returns `{ dispose, setPresentation }` and owns source/origin checks, request
76
+ `onMessage` dispatcher. Both modes use the same transport. It returns `dispose()` and `setPresentation()` methods and owns source/origin checks, request
77
77
  correlation, cancellation, bounded pending requests, and reconnection. Use source mode for bundle loading and token rotation. The opaque destination requires
78
78
  wildcard delivery, but incoming messages still require the exact iframe window,
79
79
  null origin, token, document, and session to match.
@@ -97,14 +97,6 @@ The source bridge's `startupTimeoutMs` defaults to 30 seconds.
97
97
  payloads. Routed handlers must authorize every data request. Message validation
98
98
  and iframe isolation do not grant access to data or execute SQL.
99
99
 
100
- Message types follow `{scope}:{action}`: `bridge:connect`, `bridge:ready`,
101
- `bridge:initialize`, `bridge:request`, `bridge:result`, `bridge:error`,
102
- `bridge:cancel`, `bridge:disconnect`, `runtime:ready`, `runtime:error`,
103
- `script:load`, and `state:update`. Routed requests use the same convention,
104
- including `data:query` and `navigation:update`. Hosts, apps, and bootstrap scripts
105
- must use matching names; the former unscoped and dot-separated names are no
106
- longer supported.
107
-
108
100
  ## SQL query route
109
101
 
110
102
  Hosts serving browser-owned operations register `sqlQueryRoute` explicitly:
@@ -127,16 +119,16 @@ const router = createMessageRouter(
127
119
  // Supply router.dispatch as the shell's onMessage handler.
128
120
  ```
129
121
 
130
- The host supplies `authorizedLakehouseForCurrentViewer`. Its backend must enforce
122
+ The host supplies `authorizedLakehouseForCurrentViewer()`. Its backend must enforce
131
123
  viewer/dataset permissions, permitted query behavior, maximum rows, execution
132
124
  time, concurrency, and response size independently of browser policy. Route
133
- validation is not SQL authorization. `createSqlQueryHandler` calls authorization
125
+ validation is not SQL authorization. `createSqlQueryHandler()` calls authorization
134
126
  for each query, forwards cancellation, and preserves `DataSourceError` reasons
135
127
  as public `source_*` errors with request IDs. Authorization failures return
136
128
  `forbidden`; unknown query errors are hidden. Custom handlers can return deliberate
137
129
  public failures with `MessageRoutingError`.
138
130
 
139
- `SqlQueryInput` (exported from `/contract` and `/embed`) carries
131
+ `SqlQueryInput` (exported from `/contract`) carries
140
132
  `{ statement: string, limit: number }`; responses are
141
133
  `{ columns: { name: string, type?: string }[], rows: unknown[][], queryId?: string }`.
142
134
  The route rejects empty statements, unsafe or nonpositive limits, malformed
@@ -167,27 +159,16 @@ host.setPresentation({ surface: 'embedded', theme: 'light' });
167
159
 
168
160
  Both source and connection modes support the `presentation` option and
169
161
  `host.setPresentation(presentation)` method. `DataAppPresentation` is exported from
170
- `/embed` and `/client`. Use `surface: 'embedded'` when the parent provides page chrome, as in the
162
+ `/embed`. Use `surface: 'embedded'` when the parent provides page chrome, as in the
171
163
  Altertable frontend, and `'standalone'` when the app provides its own header and
172
164
  footer. `theme` must be resolved to `'light'` or
173
165
  `'dark'`; the parent decides how its system preference is resolved.
174
166
 
175
- Presentation travels over `postMessage` in the authenticated `bridge:initialize` and
167
+ Presentation travels over `postMessage()` in the authenticated `bridge:initialize` and
176
168
  `state:update` messages, alongside `search` and `hash`. Framework-neutral apps
177
169
  can narrow the unknown state returned by `bridge.snapshot()` to read its
178
- `presentation` field, and subscribe through `bridge.subscribe`. React `DataApp` consumes it automatically.
170
+ `presentation` field, and subscribe through `bridge.subscribe()`. React `<DataApp>` consumes it automatically.
179
171
  An embedded surface renders toolbar actions without the page header or footer.
180
172
  Both surfaces follow the parent's theme, including presentation mode, without
181
173
  changing saved viewer preferences. Omitting presentation preserves standalone behavior;
182
174
  `host.setPresentation(undefined)` restores it.
183
-
184
- The host attachment is an explicit `{ dispose, setPresentation }` object. Replace
185
- former cleanup calls (`dispose()`) with `host.dispose()`. React hosts manage this
186
- lifecycle automatically.
187
-
188
- ## Migration
189
-
190
- `attachDataAppShell` and `DataAppShellOptions` have been removed. Use
191
- `attachDataAppBridge` with the same source options and `DataAppBridgeOptions`.
192
- Source and connection options are mutually exclusive. The frontend and CLI own
193
- their shell UI; this package provides iframe bridges only.
@@ -0,0 +1,39 @@
1
+ # Author a data app
2
+
3
+ Follow the [shared authoring flow](app-authoring.md) using [index.tsx](../examples/starter-data-app/index.tsx) for a hosted / remote / cloud data app. Keep the app in
4
+ one file with public package imports; omit server files, HTML, credentials, and
5
+ relative or app-alias imports.
6
+
7
+ Replace the sample SQL, parsers, filters, data context, story, and configuration with
8
+ an exploration of the source data you inspected. The starter uses two SQL
9
+ `VALUES` rows, so it needs no production table.
10
+
11
+ For execution details, see [browser-owned operations](client.md#browser-owned-operations-for-bundle-apps).
12
+
13
+ ## Convert a local data app
14
+
15
+ 1. Combine the app's operations and parsers, data context, views, story,
16
+ configuration, and browser entry into one `index.tsx`, following the
17
+ [single-file starter](../examples/starter-data-app/index.tsx).
18
+ 2. Replace the HTTP client with `createDataClient({ operations })`, using the
19
+ operation registry as a value. See [browser-owned operations](client.md#browser-owned-operations-for-bundle-apps)
20
+ for execution through the host.
21
+ 3. Remove the Bun server, HTML, server adapters, credentials, and relative or
22
+ app-alias imports. Rewrite any operation that depends on server-only code
23
+ to use the operation's `query()` helper.
24
+ 4. Confirm the host can query the same catalogs, tables, and fields.
25
+ [Verify the app](app-authoring.md#verify-the-app) in the hosted runtime against
26
+ the local version's filters and findings.
27
+
28
+ ## Preview in this repository
29
+
30
+ ```fish
31
+ bun install --frozen-lockfile
32
+ bun run build
33
+ bun browser-tests/server.ts
34
+ ```
35
+
36
+ Open [the starter preview](http://127.0.0.1:27418/starter-data-app).
37
+ Its test host executes the sample SQL through the iframe bridge using SQLite.
38
+
39
+ For checks, see [Contributing](../CONTRIBUTING.md).
@@ -0,0 +1,14 @@
1
+ # Author a local data app
2
+
3
+ Start from the CLI scaffold or the [local starter](https://github.com/altertable-ai/data-app/tree/main/examples/starter-local-data-app).
4
+ Use the starter's README for setup and its AGENTS.md to find app-owned files.
5
+ Replace the connectivity screen with the exploration and story from the
6
+ [shared authoring flow](app-authoring.md).
7
+
8
+ | Task | Documentation |
9
+ | --------------------------------------- | ---------------------------------------- |
10
+ | Serve locally with Bun | [Bun server](server-bun.md) |
11
+ | Execute and authorize server operations | [Server](server.md) |
12
+ | Call operations from the browser | [HTTP client](client.md#http-operations) |
13
+
14
+ You can also [convert the local app to a hosted data app](hosted-apps.md#convert-a-local-data-app).