@definite-app/data-apps 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +686 -0
- package/LICENSE +201 -0
- package/README.md +643 -0
- package/build.mjs +459 -0
- package/examples/_refined_demo/app.json +15 -0
- package/examples/_refined_demo/data/sample.parquet +0 -0
- package/examples/_refined_demo/gen_preview_data.py +59 -0
- package/examples/_refined_demo/preview-data.json +13 -0
- package/examples/_refined_demo/src/App.tsx +188 -0
- package/examples/_refined_demo/src/main.tsx +12 -0
- package/examples/loan-portfolio/app.json +31 -0
- package/examples/loan-portfolio/data/loan_book.parquet +0 -0
- package/examples/loan-portfolio/gen_preview_data.py +454 -0
- package/examples/loan-portfolio/preview-data.json +84 -0
- package/examples/loan-portfolio/src/App.tsx +1103 -0
- package/examples/loan-portfolio/src/main.tsx +12 -0
- package/examples/revenue-explorer/app.json +23 -0
- package/examples/revenue-explorer/data/transactions.parquet +0 -0
- package/examples/revenue-explorer/gen_preview_data.py +129 -0
- package/examples/revenue-explorer/preview-data.json +49 -0
- package/examples/revenue-explorer/src/App.tsx +527 -0
- package/examples/revenue-explorer/src/main.tsx +12 -0
- package/package.json +55 -0
- package/preview.mjs +35 -0
- package/runtime/definite-runtime.tsx +5934 -0
- package/scripts/headless-smoke.mjs +196 -0
- package/templates/blank/app.json +15 -0
- package/templates/blank/src/App.tsx +41 -0
- package/templates/blank/src/main.tsx +12 -0
- package/templates/refined/app.json +15 -0
- package/templates/refined/src/App.tsx +198 -0
- package/templates/refined/src/main.tsx +12 -0
package/README.md
ADDED
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
# Definite Data Apps
|
|
2
|
+
|
|
3
|
+
Build interactive React applications that run inside [Definite](https://www.definite.app/) Docs. Data apps compile to a single HTML file with client-side DuckDB WASM, Perspective.js, and a built-in component library.
|
|
4
|
+
|
|
5
|
+
## How data flows
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
app.json Definite platform Browser DuckDB WASM App.tsx
|
|
9
|
+
(manifest) --> (server-side fetch) --> (local tables) --> (client-side SQL)
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
1. **app.json** declares every data resource the app needs. Each resource has a key, a `kind`, and a `source`.
|
|
13
|
+
2. **The Definite platform** reads the manifest and fetches data server-side (from DuckLake, Cube, GCS, etc.). The app never talks to the warehouse directly.
|
|
14
|
+
3. **The runtime** loads fetched data into a **browser-side DuckDB WASM** instance as local tables.
|
|
15
|
+
4. **App.tsx** queries those local tables via `useSqlQuery(dataset, sql, deps)`. These SQL queries run in the browser, not against the server.
|
|
16
|
+
|
|
17
|
+
Column names in your `useSqlQuery` SQL must match the aliases in your `app.json` SQL. If `app.json` has `SELECT foo AS myColumn`, the local table has a column called `myColumn`.
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
git clone https://github.com/definite-app/definite-data-apps.git
|
|
23
|
+
cd definite-data-apps
|
|
24
|
+
make setup # npm install
|
|
25
|
+
make new-app NAME=my-app # blank template (one KPI)
|
|
26
|
+
# or:
|
|
27
|
+
make new-app NAME=my-app TEMPLATE=refined # sidebar shell + drill drawer
|
|
28
|
+
# Edit examples/my-app/app.json (declare your data resources)
|
|
29
|
+
# Edit examples/my-app/src/App.tsx (build your UI)
|
|
30
|
+
make build NAME=my-app # build to dist/index.html
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Two bundled templates live under `templates/`:
|
|
34
|
+
|
|
35
|
+
| Template | When to use |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `blank` | Single-view dashboards, embedded tiles, anything that doesn't need a navigation rail. Built on `AppShell`. |
|
|
38
|
+
| `refined` | Multi-view analytics apps. Sidebar with nav + date range + 10-filter accordion, KPI cards with sparklines, drill drawer, cache popover, optional AI follow-up chat. Built on `ShellLayout` + `Sidebar`. |
|
|
39
|
+
|
|
40
|
+
To preview bundled examples with mock data:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
make preview NAME=revenue-explorer # blank-style
|
|
44
|
+
make preview NAME=loan-portfolio # refined-style reference app
|
|
45
|
+
open examples/<name>/dist/index.html
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Directory structure
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
examples/my-app/
|
|
52
|
+
app.json # Manifest: declares all data resources
|
|
53
|
+
preview-data.json # Optional: mock data for local preview
|
|
54
|
+
src/
|
|
55
|
+
main.tsx # Entry point (boilerplate, don't edit)
|
|
56
|
+
App.tsx # Your UI code (imports from "@definite/runtime")
|
|
57
|
+
dist/
|
|
58
|
+
index.html # Built artifact (generated by build.mjs)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Deploy `dist/index.html` by uploading it to Definite Drive and creating a Doc with an HTML tile:
|
|
62
|
+
|
|
63
|
+
```yaml
|
|
64
|
+
version: 1
|
|
65
|
+
schemaVersion: "2025-01"
|
|
66
|
+
kind: dashboard
|
|
67
|
+
metadata:
|
|
68
|
+
name: "My App"
|
|
69
|
+
datasets: {}
|
|
70
|
+
layout:
|
|
71
|
+
columns: 36
|
|
72
|
+
tiles:
|
|
73
|
+
- id: app
|
|
74
|
+
x: 0
|
|
75
|
+
y: 0
|
|
76
|
+
w: 36
|
|
77
|
+
h: 22
|
|
78
|
+
type: html
|
|
79
|
+
fullScreen: true
|
|
80
|
+
driveFile: "apps-v2/my-app/dist/index.html"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Manifest (app.json)
|
|
84
|
+
|
|
85
|
+
The manifest declares what data the app needs. The platform fetches it server-side and the runtime loads it into the browser.
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"version": 2,
|
|
90
|
+
"name": "Revenue Explorer",
|
|
91
|
+
"entry": "src/main.tsx",
|
|
92
|
+
"resources": {
|
|
93
|
+
"transactions": {
|
|
94
|
+
"kind": "dataset",
|
|
95
|
+
"source": {
|
|
96
|
+
"type": "sql",
|
|
97
|
+
"sql": "SELECT id AS transactionId, STRFTIME(created_at, '%Y-%m-%d') AS transactionDate, amount::DOUBLE AS amount FROM LAKE.SCHEMA.transactions LIMIT 200000"
|
|
98
|
+
},
|
|
99
|
+
"public": false
|
|
100
|
+
},
|
|
101
|
+
"branches": {
|
|
102
|
+
"kind": "json",
|
|
103
|
+
"source": {
|
|
104
|
+
"type": "sql",
|
|
105
|
+
"sql": "SELECT branch_id AS branchId, branch_name AS branchName FROM LAKE.SCHEMA.branches ORDER BY 2 LIMIT 3000"
|
|
106
|
+
},
|
|
107
|
+
"snapshot": {
|
|
108
|
+
"format": "json",
|
|
109
|
+
"drivePath": "apps-v2/my-app/snapshots/branches.json"
|
|
110
|
+
},
|
|
111
|
+
"public": true
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Resource kinds
|
|
118
|
+
|
|
119
|
+
| Kind | Hook | Use for |
|
|
120
|
+
|------|------|---------|
|
|
121
|
+
| `dataset` | `useDataset(key)` | Data loaded into browser DuckDB WASM as a local table |
|
|
122
|
+
| `json` | `useJsonResource(key)` | Small lookup lists returned as plain arrays |
|
|
123
|
+
|
|
124
|
+
### Source types
|
|
125
|
+
|
|
126
|
+
| Type | Description |
|
|
127
|
+
|------|-------------|
|
|
128
|
+
| `sql` | SQL query executed server-side against DuckLake. **Recommended for most cases.** |
|
|
129
|
+
| `duckdbFile` | A `.duckdb` file downloaded from Drive/GCS and attached locally |
|
|
130
|
+
| `cube` | Cube semantic model query. **Not recommended** (column names are Cube titles with spaces). |
|
|
131
|
+
|
|
132
|
+
### Snapshots
|
|
133
|
+
|
|
134
|
+
For public embeds, add a `snapshot` block to pre-cache data:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
"snapshot": {
|
|
138
|
+
"format": "json",
|
|
139
|
+
"drivePath": "apps-v2/my-app/snapshots/branches.json"
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Runtime hooks
|
|
144
|
+
|
|
145
|
+
### `useDataset(key, opts?)`
|
|
146
|
+
|
|
147
|
+
Loads a `kind: "dataset"` resource into browser DuckDB WASM. Returns a `DatasetHandle` with:
|
|
148
|
+
|
|
149
|
+
- `tableRef`: the table reference string for use in SQL (e.g., `memory.main.transactions`)
|
|
150
|
+
- `db`, `conn`: the DuckDB WASM database and connection
|
|
151
|
+
- `perspectiveTable`: table name for Perspective viewer
|
|
152
|
+
- `loading`, `error`: loading state
|
|
153
|
+
- `cache`: cache metadata (source, load time, TTL)
|
|
154
|
+
- `refresh()`: hard refresh (bypasses IndexedDB cache)
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
const data = useDataset("transactions");
|
|
158
|
+
|
|
159
|
+
if (data.loading) return <LoadingState message="Loading..." />;
|
|
160
|
+
if (data.error) return <ErrorState title="Error" message={data.error} />;
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### `useSqlQuery(dataset, sql, deps?)`
|
|
164
|
+
|
|
165
|
+
Runs client-side SQL against a loaded dataset's DuckDB WASM instance.
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
const result = useSqlQuery(
|
|
169
|
+
data,
|
|
170
|
+
data.tableRef
|
|
171
|
+
? `SELECT branchName, SUM(amount)::INTEGER AS total FROM ${data.tableRef} GROUP BY 1`
|
|
172
|
+
: "",
|
|
173
|
+
[],
|
|
174
|
+
);
|
|
175
|
+
// result.data = [{ branchName: "Austin", total: 2685 }, ...]
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### `useJsonResource(key, opts?)`
|
|
179
|
+
|
|
180
|
+
Loads a `kind: "json"` resource. Returns `{ data, loading, error, cache, refresh }`.
|
|
181
|
+
|
|
182
|
+
```tsx
|
|
183
|
+
const branches = useJsonResource<{ branchId: string; branchName: string }>("branches");
|
|
184
|
+
// branches.data = [{ branchId: "AUS", branchName: "Austin" }, ...]
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### `useTheme()`
|
|
188
|
+
|
|
189
|
+
Returns `{ theme, toggleTheme }` for dark/light mode support. Pass `theme` and `toggleTheme` to `AppShell`.
|
|
190
|
+
|
|
191
|
+
### `usePerspective(dataset)`
|
|
192
|
+
|
|
193
|
+
Initializes a Perspective client connected to the dataset's DuckDB instance. Returns `{ client, perspectiveTable, loading, error }` for use with `PerspectivePanel`.
|
|
194
|
+
|
|
195
|
+
## Component reference
|
|
196
|
+
|
|
197
|
+
The runtime includes a complete component library. Import from `@definite/runtime`.
|
|
198
|
+
|
|
199
|
+
### Layout
|
|
200
|
+
|
|
201
|
+
#### `AppShell`
|
|
202
|
+
|
|
203
|
+
Page wrapper with title, subtitle, theme toggle, and optional meta slot.
|
|
204
|
+
|
|
205
|
+
| Prop | Type | Description |
|
|
206
|
+
|------|------|-------------|
|
|
207
|
+
| `title` | `string` | Page title |
|
|
208
|
+
| `subtitle` | `string?` | Subtitle below title |
|
|
209
|
+
| `theme` | `PerspectiveTheme` | Current theme from `useTheme()` |
|
|
210
|
+
| `onToggleTheme` | `() => void` | Toggle function from `useTheme()` |
|
|
211
|
+
| `meta` | `ReactNode?` | Content below subtitle (e.g., `ResourceCacheBadge`) |
|
|
212
|
+
| `children` | `ReactNode` | Page content |
|
|
213
|
+
|
|
214
|
+
#### `Card`
|
|
215
|
+
|
|
216
|
+
Container with optional title and right-aligned header content.
|
|
217
|
+
|
|
218
|
+
| Prop | Type | Description |
|
|
219
|
+
|------|------|-------------|
|
|
220
|
+
| `title` | `string?` | Card header title |
|
|
221
|
+
| `headerRight` | `ReactNode?` | Right-aligned header content |
|
|
222
|
+
| `noPadding` | `boolean?` | Remove inner padding (for tables, charts) |
|
|
223
|
+
| `children` | `ReactNode` | Card body |
|
|
224
|
+
|
|
225
|
+
#### `TabGroup`
|
|
226
|
+
|
|
227
|
+
Tab bar with accent underline.
|
|
228
|
+
|
|
229
|
+
| Prop | Type | Description |
|
|
230
|
+
|------|------|-------------|
|
|
231
|
+
| `tabs` | `string[]` | Tab labels. **Must be a plain string array, NOT `{key, label}` objects.** |
|
|
232
|
+
| `activeTab` | `string` | Currently active tab |
|
|
233
|
+
| `onTabChange` | `(tab: string) => void` | Tab change handler |
|
|
234
|
+
| `children` | `ReactNode?` | Content below tabs |
|
|
235
|
+
|
|
236
|
+
### Data display
|
|
237
|
+
|
|
238
|
+
#### `KpiCard`
|
|
239
|
+
|
|
240
|
+
Metric card with hover lift, accent top-line, and shimmer loading state.
|
|
241
|
+
|
|
242
|
+
| Prop | Type | Description |
|
|
243
|
+
|------|------|-------------|
|
|
244
|
+
| `title` | `string` | Metric label |
|
|
245
|
+
| `value` | `unknown` | Metric value |
|
|
246
|
+
| `format` | `"number" \| "currency" \| "percent"` | **Required.** `number`: commas, no decimals. `currency`: USD. `percent`: appends "%" with 1 decimal. |
|
|
247
|
+
| `loading` | `boolean?` | Show shimmer placeholder |
|
|
248
|
+
| `detail` | `ReactNode?` | Content below value (badges, comparison deltas) |
|
|
249
|
+
|
|
250
|
+
Pre-formatted strings (e.g., `"53.6s"`) pass through as-is. NaN/Infinity display as a dash.
|
|
251
|
+
|
|
252
|
+
#### `DataTable`
|
|
253
|
+
|
|
254
|
+
Simple table with row hover.
|
|
255
|
+
|
|
256
|
+
| Prop | Type | Description |
|
|
257
|
+
|------|------|-------------|
|
|
258
|
+
| `columns` | `{ key: string; label: string }[]` | Column definitions |
|
|
259
|
+
| `rows` | `Record<string, unknown>[]` | Row data |
|
|
260
|
+
| `emptyState` | `string?` | Empty state message |
|
|
261
|
+
|
|
262
|
+
#### `ReportTable`
|
|
263
|
+
|
|
264
|
+
Rich management report table with grouped column headers, colored bands, section dividers, subtotal/total rows, and per-cell conditional styling.
|
|
265
|
+
|
|
266
|
+
| Prop | Type | Description |
|
|
267
|
+
|------|------|-------------|
|
|
268
|
+
| `headerGroups` | `{ label, colSpan?, color?, subHeaders[] }[]` | Grouped headers with optional color bands |
|
|
269
|
+
| `rows` | `{ type?, indent?, cells }[]` | Rows with type: `data`, `section`, `subtotal`, or `total` |
|
|
270
|
+
| `emptyState` | `string?` | Empty state message |
|
|
271
|
+
|
|
272
|
+
#### `Badge`
|
|
273
|
+
|
|
274
|
+
Status indicator with colored dot.
|
|
275
|
+
|
|
276
|
+
| Prop | Type | Description |
|
|
277
|
+
|------|------|-------------|
|
|
278
|
+
| `variant` | `"default" \| "success" \| "warning" \| "error" \| "info"` | Color scheme |
|
|
279
|
+
| `dot` | `boolean?` | Show/hide the dot (default: true) |
|
|
280
|
+
| `children` | `ReactNode` | Badge text |
|
|
281
|
+
|
|
282
|
+
### Charts
|
|
283
|
+
|
|
284
|
+
#### `EChart`
|
|
285
|
+
|
|
286
|
+
Apache ECharts wrapper. Handles init/dispose lifecycle, theme switching, and resize.
|
|
287
|
+
|
|
288
|
+
| Prop | Type | Description |
|
|
289
|
+
|------|------|-------------|
|
|
290
|
+
| `option` | `Record<string, unknown>` | Full ECharts option spec |
|
|
291
|
+
| `height` | `number?` | Chart height in pixels |
|
|
292
|
+
| `theme` | `PerspectiveTheme?` | Theme for auto-switching |
|
|
293
|
+
| `onClick` | `(params) => void` | Click handler for drill-down |
|
|
294
|
+
|
|
295
|
+
**Critical**: EChart serializes options through `JSON.stringify`. **Functions are silently stripped.** Do not use `formatter`, `valueFormatter`, or callbacks. Use ECharts string templates instead (e.g., `"{value}%"`).
|
|
296
|
+
|
|
297
|
+
#### `PerspectivePanel`
|
|
298
|
+
|
|
299
|
+
Wrapper for [Perspective.js](https://perspective.finos.org/) viewer. Supports Datagrid, Y Bar, Y Line, Y Area, X Bar, Y Scatter, Heatmap, Treemap, and Sunburst.
|
|
300
|
+
|
|
301
|
+
| Prop | Type | Description |
|
|
302
|
+
|------|------|-------------|
|
|
303
|
+
| `client` | `any` | Perspective client from `usePerspective()` |
|
|
304
|
+
| `table` | `string` | Table name from dataset's `perspectiveTable` |
|
|
305
|
+
| `theme` | `PerspectiveTheme` | Current theme |
|
|
306
|
+
| `config` | `Record<string, unknown>?` | Perspective viewer config (plugin, columns, group_by, etc.) |
|
|
307
|
+
| `onSelect` | `(row) => void` | Click handler for drill-down (`perspective-select` event) |
|
|
308
|
+
|
|
309
|
+
Example:
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
const perspective = usePerspective(data);
|
|
313
|
+
|
|
314
|
+
<PerspectivePanel
|
|
315
|
+
client={perspective.client}
|
|
316
|
+
table={data.perspectiveTable}
|
|
317
|
+
theme={theme}
|
|
318
|
+
config={{
|
|
319
|
+
plugin: "Y Bar",
|
|
320
|
+
columns: ["amount"],
|
|
321
|
+
group_by: ["branchName"],
|
|
322
|
+
aggregates: { amount: "sum" },
|
|
323
|
+
sort: [["amount", "desc"]],
|
|
324
|
+
}}
|
|
325
|
+
onSelect={(row) => {
|
|
326
|
+
if (!row) return;
|
|
327
|
+
setFilter(prev => prev === row.branchName ? "" : String(row.branchName));
|
|
328
|
+
}}
|
|
329
|
+
/>
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Inputs
|
|
333
|
+
|
|
334
|
+
#### `Select`
|
|
335
|
+
|
|
336
|
+
Single-select dropdown.
|
|
337
|
+
|
|
338
|
+
| Prop | Type | Description |
|
|
339
|
+
|------|------|-------------|
|
|
340
|
+
| `options` | `{ value: string; label: string }[]` | Options |
|
|
341
|
+
| `value` | `string` | Selected value |
|
|
342
|
+
| `onChange` | `(value: string) => void` | Change handler |
|
|
343
|
+
| `placeholder` | `string?` | Placeholder text |
|
|
344
|
+
| `label` | `ReactNode?` | Label above dropdown |
|
|
345
|
+
|
|
346
|
+
#### `MultiSelect<T>`
|
|
347
|
+
|
|
348
|
+
Searchable checkbox dropdown.
|
|
349
|
+
|
|
350
|
+
| Prop | Type | Description |
|
|
351
|
+
|------|------|-------------|
|
|
352
|
+
| `options` | `T[]` | All options |
|
|
353
|
+
| `selected` | `T[]` | Selected items |
|
|
354
|
+
| `onChange` | `(selected: T[]) => void` | Change handler |
|
|
355
|
+
| `labelKey` | `string` | Key for display label |
|
|
356
|
+
| `valueKey` | `string` | Key for value |
|
|
357
|
+
| `label` | `ReactNode?` | Label above dropdown |
|
|
358
|
+
| `placeholder` | `string?` | Placeholder text |
|
|
359
|
+
|
|
360
|
+
#### `FilterPills`
|
|
361
|
+
|
|
362
|
+
Single-select horizontal toggle group.
|
|
363
|
+
|
|
364
|
+
| Prop | Type | Description |
|
|
365
|
+
|------|------|-------------|
|
|
366
|
+
| `options` | `{ value: string; label: string }[]` | Options |
|
|
367
|
+
| `value` | `string` | Selected value |
|
|
368
|
+
| `onChange` | `(value: string) => void` | Change handler |
|
|
369
|
+
| `label` | `ReactNode?` | Label above pills |
|
|
370
|
+
|
|
371
|
+
#### `TextInput`
|
|
372
|
+
|
|
373
|
+
Text input with focus ring and optional icon.
|
|
374
|
+
|
|
375
|
+
| Prop | Type | Description |
|
|
376
|
+
|------|------|-------------|
|
|
377
|
+
| `value` | `string` | Current value |
|
|
378
|
+
| `onChange` | `(value: string) => void` | Change handler |
|
|
379
|
+
| `placeholder` | `string?` | Placeholder |
|
|
380
|
+
| `label` | `ReactNode?` | Label |
|
|
381
|
+
| `icon` | `ReactNode?` | Icon on left |
|
|
382
|
+
|
|
383
|
+
#### `DateInput`
|
|
384
|
+
|
|
385
|
+
Native date picker with design system styling.
|
|
386
|
+
|
|
387
|
+
| Prop | Type | Description |
|
|
388
|
+
|------|------|-------------|
|
|
389
|
+
| `value` | `string` | Date string (YYYY-MM-DD) |
|
|
390
|
+
| `onChange` | `(value: string) => void` | Change handler |
|
|
391
|
+
| `label` | `ReactNode?` | Label |
|
|
392
|
+
| `max` | `string?` | Max date |
|
|
393
|
+
| `min` | `string?` | Min date |
|
|
394
|
+
|
|
395
|
+
### Feedback
|
|
396
|
+
|
|
397
|
+
#### `LoadingState`
|
|
398
|
+
|
|
399
|
+
Full-page pulsing dots animation.
|
|
400
|
+
|
|
401
|
+
| Prop | Type | Description |
|
|
402
|
+
|------|------|-------------|
|
|
403
|
+
| `message` | `string?` | Loading message (default: "Loading...") |
|
|
404
|
+
|
|
405
|
+
#### `ErrorState`
|
|
406
|
+
|
|
407
|
+
Full-page error with red left bar.
|
|
408
|
+
|
|
409
|
+
| Prop | Type | Description |
|
|
410
|
+
|------|------|-------------|
|
|
411
|
+
| `title` | `string` | Error title |
|
|
412
|
+
| `message` | `string` | Error details |
|
|
413
|
+
|
|
414
|
+
#### `Tooltip`
|
|
415
|
+
|
|
416
|
+
Hover tooltip with arbitrary content.
|
|
417
|
+
|
|
418
|
+
| Prop | Type | Description |
|
|
419
|
+
|------|------|-------------|
|
|
420
|
+
| `content` | `ReactNode` | Tooltip content |
|
|
421
|
+
| `children` | `ReactNode` | Trigger element |
|
|
422
|
+
| `position` | `"top" \| "bottom"` | Position (default: top) |
|
|
423
|
+
| `maxWidth` | `number?` | Max width in pixels (default: 240) |
|
|
424
|
+
|
|
425
|
+
#### `ResourceCacheBadge`
|
|
426
|
+
|
|
427
|
+
Cache metadata popover showing row count, source, load time, TTL, and "Clear cache & reload" button.
|
|
428
|
+
|
|
429
|
+
| Prop | Type | Description |
|
|
430
|
+
|------|------|-------------|
|
|
431
|
+
| `rows` | `number?` | Row count to display |
|
|
432
|
+
| `cache` | `ResourceCacheDetails` | Cache metadata from `useDataset()` |
|
|
433
|
+
| `onClearAndReload` | `() => Promise<void>` | Clear + reload handler (use dataset's `refresh()`) |
|
|
434
|
+
|
|
435
|
+
## Refined SaaS shell primitives
|
|
436
|
+
|
|
437
|
+
A second track of components built for multi-view analytics apps. They're driven by a palette object (not CSS vars), so apps can pass a brand `accent` at the root and every surface (KPI top-line, active nav, filter chip, loading dot) picks it up. Pair with the `refined` template.
|
|
438
|
+
|
|
439
|
+
```tsx
|
|
440
|
+
import {
|
|
441
|
+
buildPalette, PaletteProvider, usePalette,
|
|
442
|
+
ShellLayout, Sidebar, SaasKpiCard,
|
|
443
|
+
DrillProvider, useDrill, CachePopover,
|
|
444
|
+
callFiFast, buildDrillPrompt,
|
|
445
|
+
} from "@definite/runtime";
|
|
446
|
+
|
|
447
|
+
const palette = buildPalette(theme, { accent: "#FF006E" });
|
|
448
|
+
|
|
449
|
+
<PaletteProvider value={palette}>
|
|
450
|
+
<DrillProvider
|
|
451
|
+
aiChat={{
|
|
452
|
+
onAsk: (q, entity) => callFiFast({ prompt: buildDrillPrompt(q, entity) }),
|
|
453
|
+
}}
|
|
454
|
+
>
|
|
455
|
+
<ShellLayout
|
|
456
|
+
palette={palette}
|
|
457
|
+
sidebar={<Sidebar logo={...} navItems={...} activeView={...} ... />}
|
|
458
|
+
title="Overview"
|
|
459
|
+
headerRight={<CachePopover cache={data.cache} onRefresh={data.refresh} ... />}
|
|
460
|
+
>
|
|
461
|
+
<SaasKpiCard title="Total rows" value={count} onClick={() => drill.open({...})} />
|
|
462
|
+
</ShellLayout>
|
|
463
|
+
</DrillProvider>
|
|
464
|
+
</PaletteProvider>
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### Palette
|
|
468
|
+
|
|
469
|
+
| Export | Purpose |
|
|
470
|
+
|--------|---------|
|
|
471
|
+
| `buildPalette(theme, { accent? })` | Returns a `SaasPalette` of semantic colors + fonts. `accent` override derives `accentSoft` automatically. |
|
|
472
|
+
| `PaletteProvider` / `usePalette()` | Context for descendant primitives. |
|
|
473
|
+
|
|
474
|
+
### Shell
|
|
475
|
+
|
|
476
|
+
| Export | Purpose |
|
|
477
|
+
|--------|---------|
|
|
478
|
+
| `ShellLayout` | Outer flex container; renders the sidebar slot, breadcrumb, title, and a `headerRight` slot (typically `CachePopover` + an Export button). Wraps children in `PaletteProvider`. |
|
|
479
|
+
| `Sidebar` | Logo, nav, `dateRangeSlot`, `FilterAccordion` (if `filterGroups` provided), theme toggle, footer slot. |
|
|
480
|
+
| `FilterAccordion` | Collapsible filter groups with per-group counts, global + per-group search, selected chips. |
|
|
481
|
+
| `Breadcrumb` | Simple `/`-separated trail. |
|
|
482
|
+
|
|
483
|
+
### Data display
|
|
484
|
+
|
|
485
|
+
| Export | Purpose |
|
|
486
|
+
|--------|---------|
|
|
487
|
+
| `SaasKpiCard` | Accent top-line + sparkline + delta pill + loading shimmer. Props: `title`, `value`, `delta?`, `up?`, `sub?`, `spark?`, `accent?`, `loading?`, `onClick?`. |
|
|
488
|
+
| `Sparkline` | 32-px SVG polyline + last-point dot. Props: `values`, `color?`, `width?`, `height?`. |
|
|
489
|
+
| `SkeletonShimmer` | Palette-driven loading shimmer. Props: `width?`, `height?`, `radius?`. |
|
|
490
|
+
| `CachePopover` | Click-to-inspect cache pill with "Clear cache & reload". Same cache object as `useDataset().cache`. |
|
|
491
|
+
|
|
492
|
+
### Drill drawer
|
|
493
|
+
|
|
494
|
+
`DrillProvider` mounts a slide-over drawer. Any descendant calls `useDrill().open(entity)` to show it. The drawer renders computed stats, breakdown bars, SQL, and an optional AI follow-up chat.
|
|
495
|
+
|
|
496
|
+
```tsx
|
|
497
|
+
const drill = useDrill();
|
|
498
|
+
drill.open({
|
|
499
|
+
kind: "kpi", // "kpi" | "row" | "chart"
|
|
500
|
+
id: "total_outstanding",
|
|
501
|
+
title: "Total outstanding",
|
|
502
|
+
value: "$50.8M",
|
|
503
|
+
breadcrumb: "Overview",
|
|
504
|
+
stats: [["Active", "2,511"], ["Avg", "$33K"]],
|
|
505
|
+
breakdown: [{ label: "2026-04", value: 1_780_000 }, ...],
|
|
506
|
+
sql: "SELECT SUM(balance) FROM loans WHERE ...",
|
|
507
|
+
narrative: "Total principal balance across active contracts.",
|
|
508
|
+
});
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
Wire AI chat by passing `aiChat` to `DrillProvider`:
|
|
512
|
+
|
|
513
|
+
```tsx
|
|
514
|
+
<DrillProvider
|
|
515
|
+
aiChat={{
|
|
516
|
+
onAsk: (userMessage, entity) => callFiFast({
|
|
517
|
+
prompt: buildDrillPrompt(userMessage, entity),
|
|
518
|
+
}),
|
|
519
|
+
placeholder: "Ask a follow-up…",
|
|
520
|
+
}}
|
|
521
|
+
>
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### AI integration
|
|
525
|
+
|
|
526
|
+
| Export | Purpose |
|
|
527
|
+
|--------|---------|
|
|
528
|
+
| `callFiFast({ prompt, system?, authToken?, ... })` | One-shot POST to `/v4/fi-fast`. Extracts `response.content.parts[0].text`. Same-origin cookies by default; pass `authToken` for Bearer auth. |
|
|
529
|
+
| `buildDrillPrompt(userMessage, entity)` | Default prompt builder that grounds the model in the drill entity's stats + breakdown. Use as-is or wrap. |
|
|
530
|
+
|
|
531
|
+
### When to use `ShellLayout` vs `AppShell`
|
|
532
|
+
|
|
533
|
+
| Use case | Component |
|
|
534
|
+
|----------|-----------|
|
|
535
|
+
| Multi-view standalone analytics app with nav rail | `ShellLayout` + `Sidebar` (refined template) |
|
|
536
|
+
| Embedded Doc tile, single-view dashboard, email-receipt-style view | `AppShell` (blank template) |
|
|
537
|
+
|
|
538
|
+
Both remain supported and will coexist.
|
|
539
|
+
|
|
540
|
+
## Best practices
|
|
541
|
+
|
|
542
|
+
### Always use SQL resources
|
|
543
|
+
|
|
544
|
+
Use `type: "sql"` for dataset resources. SQL gives you full control over column names via `AS` aliases. Avoid `type: "cube"` because Cube responses use long title-based column names (e.g., `"Credit Projects FICO Band"`) that require double-quoting everywhere.
|
|
545
|
+
|
|
546
|
+
### Use camelCase aliases
|
|
547
|
+
|
|
548
|
+
Alias all columns to camelCase in your `app.json` SQL:
|
|
549
|
+
|
|
550
|
+
```sql
|
|
551
|
+
SELECT
|
|
552
|
+
installer_account_name AS installerAccountName,
|
|
553
|
+
fico::INTEGER AS fico,
|
|
554
|
+
STRFTIME(application_date, '%Y-%m-%d') AS applicationDate,
|
|
555
|
+
approval_cnt::INTEGER AS approvalCnt
|
|
556
|
+
FROM LAKE.credit.projects
|
|
557
|
+
LIMIT 500000
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
### Keep client-side SQL simple
|
|
561
|
+
|
|
562
|
+
| Server-side (app.json SQL) | Client-side (useSqlQuery) |
|
|
563
|
+
|---|---|
|
|
564
|
+
| Complex joins, subqueries, CTEs | Simple GROUP BY + aggregation |
|
|
565
|
+
| CASE WHEN with compound booleans | SUM/COUNT of pre-computed columns |
|
|
566
|
+
| NOT IN, LIKE, regex filters | Simple WHERE on date/string equality |
|
|
567
|
+
| Type casts (BIGINT, DOUBLE) | `::INTEGER` on SUM results |
|
|
568
|
+
| Flag computation | Date range filters |
|
|
569
|
+
|
|
570
|
+
### Always cast SUM to INTEGER
|
|
571
|
+
|
|
572
|
+
DuckDB WASM may return HUGEINT from SUM, which JavaScript cannot handle cleanly:
|
|
573
|
+
|
|
574
|
+
```tsx
|
|
575
|
+
// Good
|
|
576
|
+
`SELECT SUM(amount)::INTEGER AS total FROM ${data.tableRef}`
|
|
577
|
+
|
|
578
|
+
// Bad: may return BigInt that breaks rendering
|
|
579
|
+
`SELECT SUM(amount) AS total FROM ${data.tableRef}`
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### Pre-compute flags server-side
|
|
583
|
+
|
|
584
|
+
When you need conditional counts, compute the flags in `app.json` SQL, then aggregate client-side:
|
|
585
|
+
|
|
586
|
+
```json
|
|
587
|
+
"sql": "SELECT ..., CASE WHEN agent_time > 0 AND skill NOT IN ('123','456') THEN 1 ELSE 0 END AS isHandled FROM ..."
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
```tsx
|
|
591
|
+
const kpis = useSqlQuery(data, data.tableRef ? `
|
|
592
|
+
SELECT SUM(isHandled)::INTEGER AS handled FROM ${data.tableRef}
|
|
593
|
+
` : "", []);
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
### Convert dates with STRFTIME
|
|
597
|
+
|
|
598
|
+
Always convert DuckDB dates/timestamps to VARCHAR in app.json SQL:
|
|
599
|
+
|
|
600
|
+
```sql
|
|
601
|
+
STRFTIME(created_at, '%Y-%m-%d') AS createdDate
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
## DuckDB WASM limitations
|
|
605
|
+
|
|
606
|
+
DuckDB WASM 1.29.0 has known issues with complex expressions. Compound `CASE WHEN` with `AND`/`OR` chains, `NOT IN` with many values, or `LIKE` patterns may silently return 0 for all rows in the browser, even though the same query works correctly server-side.
|
|
607
|
+
|
|
608
|
+
**Rule**: if you're writing a `CASE WHEN` with more than one condition in `useSqlQuery`, move it to `app.json` instead.
|
|
609
|
+
|
|
610
|
+
## Caching
|
|
611
|
+
|
|
612
|
+
The runtime caches successful data loads in IndexedDB with a 24-hour TTL. Cache keys include the drive file path, resource key, mode, and manifest definition, so rebuilt apps invalidate naturally.
|
|
613
|
+
|
|
614
|
+
Use `ResourceCacheBadge` in your `AppShell` meta slot to show cache status:
|
|
615
|
+
|
|
616
|
+
```tsx
|
|
617
|
+
<AppShell
|
|
618
|
+
title="My App"
|
|
619
|
+
meta={
|
|
620
|
+
<ResourceCacheBadge
|
|
621
|
+
rows={result.data?.length}
|
|
622
|
+
cache={data.cache}
|
|
623
|
+
onClearAndReload={async () => { await data.refresh(); }}
|
|
624
|
+
/>
|
|
625
|
+
}
|
|
626
|
+
>
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
Call `refresh()` on `useDataset()` or `useJsonResource()` for a hard refresh that bypasses IndexedDB.
|
|
630
|
+
|
|
631
|
+
## Version pins
|
|
632
|
+
|
|
633
|
+
These versions are pinned because the Arrow ingestion path is version-sensitive:
|
|
634
|
+
|
|
635
|
+
| Library | Version |
|
|
636
|
+
|---------|---------|
|
|
637
|
+
| DuckDB WASM | 1.29.0 |
|
|
638
|
+
| Apache Arrow | 17.0.0 |
|
|
639
|
+
| Perspective | 4.3.0 |
|
|
640
|
+
|
|
641
|
+
## License
|
|
642
|
+
|
|
643
|
+
MIT
|