@lotics/app-sdk 0.123.1 → 0.123.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/router.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  isEmbedded,
3
3
  postHostNotification,
4
4
  setUrlParams
5
- } from "./chunk-WM76DHGT.js";
5
+ } from "./chunk-COWN73CO.js";
6
6
 
7
7
  // src/router.tsx
8
8
  import { useEffect } from "react";
package/dist/rpc.d.ts CHANGED
@@ -6,7 +6,10 @@ import { type UrlParams, type UrlParamsPatch } from "./url_params.js";
6
6
  * postMessage and the host, holding the member's session, calls the API.
7
7
  * Standalone: the public `/v1/apps/{id}/*` endpoints are called directly, with
8
8
  * no credentials. The bridged wire must match `app_iframe_host.tsx`:
9
- * app → host: { id, op, payload }, the first also carrying `load` (`AppLoadTiming`) when the browser states any
9
+ * app → host: { id, op, payload }, the first also carrying `reportsDrawn: true` and `load` (`AppLoadTiming`) when
10
+ * the browser states any
11
+ * app → host: { type: "drawn" }, once, after the first answer when no request has been in flight for two frames:
12
+ * the app's first screen has its data
10
13
  * host → app: { id, type: "result", data } | { id, type: "error", message }
11
14
  */
12
15
  export type RpcOp = "query" | "export" | "field_options" | "workflow" | "agentRuns" | "agentRun.get" | "agentRun.cancel" | "upload" | "file.rename" | "members" | "context" | "write_model" | "prediction_miss" | "openExternal" | "openApp" | "peekApp" | "askAi" | "urlState.get" | "urlState.set" | "folder.get" | "folder.set" | "comments.list" | "comments.create" | "comments.update" | "comments.delete" | "comments.counts" | "activity.list" | "activity.undo" | "recording.start" | "recording.stop";
@@ -29,6 +29,7 @@ export type QueryAggregate = {
29
29
  day?: string;
30
30
  sum?: string;
31
31
  };
32
+ export type QueryWriteShape = { columns: Record<string, { table_id: string; field_key: string; } | { table_id: string; via: string; target_table_id: string; field_key: string; } | null>; rows?: { table_id: string; closed: boolean; sort: { field_key: string; order: "asc" | "desc"; }[]; filter?: TableRecordFilters | undefined; } | undefined; totals?: { table_id: string; by: { output: string; field_key: string; }[]; aggregates: { output: string; operation: "min" | "unique" | "date_range" | "count" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "sum" | "avg" | "median" | "max" | "range" | "earliest" | "latest" | "selected" | "checked" | "unchecked" | "percent_checked" | "percent_unchecked" | "string_agg"; field_key?: string | undefined; }[]; filter?: TableRecordFilters | undefined; } | undefined; };
32
33
  export type ReportColumn = {
33
34
  /** Each row's cell by it. */
34
35
  key: string;
@@ -76,7 +77,7 @@ export type ReportReadingHead = {
76
77
  /** The read behind it was cut: its figures stand, every total over them is left out (`null`). */
77
78
  truncated: boolean;
78
79
  };
79
- export type TableField = { key: string; name: string; description: string; type: "number"; format: "number" | "currency" | "percentage"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; notation?: { style: "decimal"; } | { style: "currency"; currency: string | { per_row: string; }; } | { style: "unit"; unit: string | { per_row: string; }; } | undefined; currency?: string | null | undefined; unit?: string | null | undefined; unit_field?: string | null | undefined; currency_field?: string | null | undefined; default_value?: number | null | undefined; default_from?: { link_field_key: string; field_key: string; options?: Record<string, string> | undefined; } | null | undefined; } | { key: string; name: string; description: string; type: "text"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; unique?: boolean | undefined; format?: "text" | "link" | "markdown" | undefined; default_value?: string | null | undefined; default_from?: { link_field_key: string; field_key: string; options?: Record<string, string> | undefined; } | null | undefined; } | { key: string; name: string; description: string; type: "date"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; format?: "date" | "datetime" | "date_range" | "datetime_range" | undefined; timezone?: string | undefined; derive_from?: "created_at" | "updated_at" | undefined; default_value?: string | null | undefined; default_from?: { link_field_key: string; field_key: string; options?: Record<string, string> | undefined; } | null | undefined; } | { key: string; name: string; description: string; type: "boolean"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; default_value?: boolean | null | undefined; default_from?: { link_field_key: string; field_key: string; options?: Record<string, string> | undefined; } | null | undefined; } | { key: string; name: string; description: string; type: "select"; options: { key: string; name: string; color: "red" | "orange" | "amber" | "yellow" | "lime" | "green" | "emerald" | "teal" | "cyan" | "sky" | "blue" | "indigo" | "violet" | "purple" | "fuchsia" | "pink" | "rose" | "slate" | "gray" | "zinc" | "neutral" | "stone"; mark?: { kind: string; name: string; } | undefined; }[]; confirm_before_update?: boolean | undefined; required?: boolean | undefined; multi?: boolean | undefined; default_value?: string[] | null | undefined; default_from?: { link_field_key: string; field_key: string; options?: Record<string, string> | undefined; } | null | undefined; } | { key: string; name: string; description: string; type: "select_member"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; multi?: boolean | undefined; default_value?: string[] | null | undefined; default_from?: { link_field_key: string; field_key: string; options?: Record<string, string> | undefined; } | null | undefined; } | { key: string; name: string; description: string; type: "select_record_link"; table_id: string; confirm_before_update?: boolean | undefined; required?: boolean | undefined; paired_field_key?: string | undefined; cardinality?: "one" | "many" | undefined; default_from?: { link_field_key: string; field_key: string; options?: Record<string, string> | undefined; } | null | undefined; } | { key: string; name: string; description: string; type: "rollup"; source_field_key: string; aggregate_option: { operation: "count"; field_key?: string | undefined; } | { operation: "unique" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique"; field_key: string; } | { operation: "min" | "unique" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "sum" | "avg" | "median" | "max" | "range"; field_key: string; } | { operation: "unique" | "date_range" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "earliest" | "latest"; field_key: string; } | { operation: "unique" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "selected"; field_key: string; } | { operation: "unique" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "selected"; field_key: string; } | { operation: "empty" | "filled" | "percent_empty" | "percent_filled" | "selected"; field_key: string; } | { operation: "checked" | "unchecked" | "percent_checked" | "percent_unchecked"; field_key: string; } | { operation: "empty" | "filled" | "percent_empty" | "percent_filled" | "selected"; field_key: string; } | { operation: "min" | "unique" | "date_range" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "sum" | "avg" | "median" | "max" | "range" | "earliest" | "latest" | "checked" | "unchecked" | "percent_checked" | "percent_unchecked"; field_key: string; } | { operation: "min" | "unique" | "date_range" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "sum" | "avg" | "median" | "max" | "range" | "earliest" | "latest"; field_key: string; }; confirm_before_update?: boolean | undefined; required?: boolean | undefined; filter?: TableRecordFiltersGroupNode | undefined; aggregate_field_type?: "number" | "boolean" | "date" | "files" | "text" | "select" | "select_member" | "select_record_link" | "rollup" | "formula" | "lookup" | "button" | "autonumber" | undefined; aggregate_field_notation?: { style: "decimal"; } | { style: "currency"; currency: string | { per_row: string; }; } | { style: "unit"; unit: string | { per_row: string; }; } | undefined; aggregate_field_format?: string | undefined; aggregate_field_currency?: string | undefined; aggregate_field_unit?: string | undefined; aggregate_field_unit_field?: string | undefined; aggregate_field_currency_field?: string | undefined; } | { key: string; name: string; description: string; type: "lookup"; source_field_key: string; lookup_field_key: string; confirm_before_update?: boolean | undefined; required?: boolean | undefined; lookup_field_type?: "number" | "boolean" | "date" | "files" | "text" | "select" | "select_member" | "select_record_link" | "rollup" | "formula" | "lookup" | "button" | "autonumber" | undefined; lookup_field_notation?: { style: "decimal"; } | { style: "currency"; currency: string | { per_row: string; }; } | { style: "unit"; unit: string | { per_row: string; }; } | undefined; lookup_field_format?: string | undefined; lookup_field_currency?: string | undefined; lookup_field_unit?: string | undefined; lookup_field_options?: { key: string; name: string; color?: string | undefined; mark?: { kind: string; name: string; } | undefined; }[] | undefined; lookup_field_autonumber?: boolean | undefined; order_by?: { field_key: string; direction: "asc" | "desc"; } | undefined; } | { key: string; name: string; description: string; type: "files"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; } | { key: string; name: string; description: string; type: "formula"; formula: { expression: string; notation?: { style: "decimal"; } | { style: "currency"; currency: string | { per_row: string; }; } | { style: "unit"; unit: string | { per_row: string; }; } | undefined; link?: true | undefined; format?: "number" | "currency" | "percentage" | "link" | undefined; currency?: string | undefined; unit?: string | undefined; unit_field?: string | undefined; currency_field?: string | undefined; options?: { key: string; name: string; color: "red" | "orange" | "amber" | "yellow" | "lime" | "green" | "emerald" | "teal" | "cyan" | "sky" | "blue" | "indigo" | "violet" | "purple" | "fuchsia" | "pink" | "rose" | "slate" | "gray" | "zinc" | "neutral" | "stone"; mark?: { kind: string; name: string; } | undefined; }[] | undefined; output_type?: "number" | "boolean" | "date" | "text" | "datetime" | "select" | undefined; volatile?: boolean | undefined; }; confirm_before_update?: boolean | undefined; required?: boolean | undefined; } | { key: string; name: string; description: string; type: "button"; text: string; confirm_before_update?: boolean | undefined; required?: boolean | undefined; workflow_id?: string | null | undefined; } | { key: string; name: string; description: string; type: "autonumber"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; prefix?: string | undefined; padding?: number | undefined; template?: string | undefined; };
80
+ export type TableField = { key: string; name: string; description: string; type: "number"; format: "number" | "currency" | "percentage"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; notation?: { style: "decimal"; } | { style: "currency"; currency: string | { per_row: string; }; } | { style: "unit"; unit: string | { per_row: string; }; } | undefined; currency?: string | null | undefined; unit?: string | null | undefined; unit_field?: string | null | undefined; currency_field?: string | null | undefined; default_value?: number | null | undefined; default_from?: { link_field_key: string; field_key: string; options?: Record<string, string> | undefined; } | null | undefined; } | { key: string; name: string; description: string; type: "text"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; unique?: boolean | undefined; format?: "text" | "link" | "markdown" | undefined; default_value?: string | null | undefined; default_from?: { link_field_key: string; field_key: string; options?: Record<string, string> | undefined; } | null | undefined; } | { key: string; name: string; description: string; type: "date"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; format?: "date" | "datetime" | "date_range" | "datetime_range" | undefined; timezone?: string | undefined; derive_from?: "created_at" | "updated_at" | undefined; default_value?: string | null | undefined; default_from?: { link_field_key: string; field_key: string; options?: Record<string, string> | undefined; } | null | undefined; } | { key: string; name: string; description: string; type: "boolean"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; default_value?: boolean | null | undefined; default_from?: { link_field_key: string; field_key: string; options?: Record<string, string> | undefined; } | null | undefined; } | { key: string; name: string; description: string; type: "select"; options: { key: string; name: string; color: "red" | "orange" | "amber" | "yellow" | "lime" | "green" | "emerald" | "teal" | "cyan" | "sky" | "blue" | "indigo" | "violet" | "purple" | "fuchsia" | "pink" | "rose" | "slate" | "gray" | "zinc" | "neutral" | "stone"; mark?: { kind: string; name: string; } | undefined; }[]; confirm_before_update?: boolean | undefined; required?: boolean | undefined; multi?: boolean | undefined; default_value?: string[] | null | undefined; default_from?: { link_field_key: string; field_key: string; options?: Record<string, string> | undefined; } | null | undefined; } | { key: string; name: string; description: string; type: "select_member"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; multi?: boolean | undefined; default_value?: string[] | null | undefined; default_from?: { link_field_key: string; field_key: string; options?: Record<string, string> | undefined; } | null | undefined; } | { key: string; name: string; description: string; type: "select_record_link"; table_id: string; confirm_before_update?: boolean | undefined; required?: boolean | undefined; paired_field_key?: string | undefined; cardinality?: "one" | "many" | undefined; default_from?: { link_field_key: string; field_key: string; options?: Record<string, string> | undefined; } | null | undefined; } | { key: string; name: string; description: string; type: "rollup"; source_field_key: string; aggregate_option: { operation: "count"; field_key?: string | undefined; } | { operation: "unique" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique"; field_key: string; } | { operation: "min" | "unique" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "sum" | "avg" | "median" | "max" | "range"; field_key: string; } | { operation: "unique" | "date_range" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "earliest" | "latest"; field_key: string; } | { operation: "unique" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "selected"; field_key: string; } | { operation: "empty" | "filled" | "percent_empty" | "percent_filled" | "selected"; field_key: string; } | { operation: "checked" | "unchecked" | "percent_checked" | "percent_unchecked"; field_key: string; } | { operation: "min" | "unique" | "date_range" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "sum" | "avg" | "median" | "max" | "range" | "earliest" | "latest" | "checked" | "unchecked" | "percent_checked" | "percent_unchecked"; field_key: string; } | { operation: "min" | "unique" | "date_range" | "empty" | "filled" | "percent_empty" | "percent_filled" | "percent_unique" | "sum" | "avg" | "median" | "max" | "range" | "earliest" | "latest"; field_key: string; }; confirm_before_update?: boolean | undefined; required?: boolean | undefined; filter?: TableRecordFiltersGroupNode | undefined; aggregate_field_type?: "number" | "boolean" | "date" | "files" | "text" | "select" | "select_member" | "select_record_link" | "rollup" | "formula" | "lookup" | "button" | "autonumber" | undefined; aggregate_field_notation?: { style: "decimal"; } | { style: "currency"; currency: string | { per_row: string; }; } | { style: "unit"; unit: string | { per_row: string; }; } | undefined; aggregate_field_format?: string | undefined; aggregate_field_currency?: string | undefined; aggregate_field_unit?: string | undefined; aggregate_field_unit_field?: string | undefined; aggregate_field_currency_field?: string | undefined; } | { key: string; name: string; description: string; type: "lookup"; source_field_key: string; lookup_field_key: string; confirm_before_update?: boolean | undefined; required?: boolean | undefined; lookup_field_type?: "number" | "boolean" | "date" | "files" | "text" | "select" | "select_member" | "select_record_link" | "rollup" | "formula" | "lookup" | "button" | "autonumber" | undefined; lookup_field_notation?: { style: "decimal"; } | { style: "currency"; currency: string | { per_row: string; }; } | { style: "unit"; unit: string | { per_row: string; }; } | undefined; lookup_field_format?: string | undefined; lookup_field_currency?: string | undefined; lookup_field_unit?: string | undefined; lookup_field_options?: { key: string; name: string; color?: string | undefined; mark?: { kind: string; name: string; } | undefined; }[] | undefined; lookup_field_autonumber?: boolean | undefined; order_by?: { field_key: string; direction: "asc" | "desc"; } | undefined; } | { key: string; name: string; description: string; type: "files"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; } | { key: string; name: string; description: string; type: "formula"; formula: { expression: string; notation?: { style: "decimal"; } | { style: "currency"; currency: string | { per_row: string; }; } | { style: "unit"; unit: string | { per_row: string; }; } | undefined; link?: true | undefined; format?: "number" | "currency" | "percentage" | "link" | undefined; currency?: string | undefined; unit?: string | undefined; unit_field?: string | undefined; currency_field?: string | undefined; options?: { key: string; name: string; color: "red" | "orange" | "amber" | "yellow" | "lime" | "green" | "emerald" | "teal" | "cyan" | "sky" | "blue" | "indigo" | "violet" | "purple" | "fuchsia" | "pink" | "rose" | "slate" | "gray" | "zinc" | "neutral" | "stone"; mark?: { kind: string; name: string; } | undefined; }[] | undefined; output_type?: "number" | "boolean" | "date" | "text" | "datetime" | "select" | undefined; volatile?: boolean | undefined; }; confirm_before_update?: boolean | undefined; required?: boolean | undefined; } | { key: string; name: string; description: string; type: "button"; text: string; confirm_before_update?: boolean | undefined; required?: boolean | undefined; workflow_id?: string | null | undefined; } | { key: string; name: string; description: string; type: "autonumber"; confirm_before_update?: boolean | undefined; required?: boolean | undefined; prefix?: string | undefined; padding?: number | undefined; template?: string | undefined; };
80
81
  export type TableRecordFilters = TableRecordFiltersConditionNode | TableRecordFiltersTraversalNode | TableRecordFiltersGroupNode;
81
82
  export type TableRecordFiltersConditionNode = { node_type: "condition"; field_key?: string | undefined; type?: string | undefined; operator: string; value?: unknown; unit_option?: string | undefined; };
82
83
  export type TableRecordFiltersGroupNode = { node_type: "group"; logic: "and" | "or"; children: (TableRecordFilters)[]; };
package/dist/written.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type BodyTable } from "./shared_types.js";
2
2
  import type { TableField } from "./shared_types.js";
3
- import { type QueryWriteShape } from "./shared_types.js";
3
+ import type { QueryWriteShape } from "./shared_types.js";
4
4
  export type Row = Record<string, unknown>;
5
5
  /** A predicted write in the shape rows store it: an option as `["opt_…"]`, a link as `[{ id, display }]`. */
6
6
  export type Staged = {
package/docs/queries.md CHANGED
@@ -398,14 +398,14 @@ as one. `record_id` is the sole exception, and declaring anything but `text` on
398
398
 
399
399
  `{ link: { source, field } }` requires a direct `from_table` parent, and `source` must be a
400
400
  `select_record_link` field on it. `field` names a field on the **link target table** —
401
- resolved **by display name first, then by key**. The target row is pinned to the declared
401
+ resolved **by display name first, then by key** (exactly: `SIZE` is not `Size`). A save refuses a
402
+ `field` the target holds under neither, naming its fields. The target row is pinned to the declared
402
403
  table, the workspace, live (non-archived, non-draft) rows, and the target table's row scope —
403
404
  a scoped-out or stale link extracts as NULL.
404
405
 
405
406
  **Warning:** only the **first** link in the cell is followed. Multi-link analytics go through
406
- `unnest` + join (§8). And because `field` resolves by name first, renaming a field on the
407
- target table can silently re-point (or break) the extraction — prefer field **keys** when the
408
- target's names are volatile.
407
+ `unnest` + join (§8). Renaming the field a `field` names by its name rewrites that `field` to
408
+ the field's key; a field renamed to a name another read uses re-points that read — prefer keys.
409
409
 
410
410
  **Cost.** Every `link` column over one `source` shares one read of the linked record per row, and
411
411
  every `link_agg` column over one `source` shares one aggregate, so ten columns through `customer` cost
@@ -487,7 +487,7 @@ derived surfaces share one implementation.
487
487
  | boolean | `boolean` | `equals` (value `true`/`false`; `false` matches NULL/missing) | same | `checked`/`unchecked`/`percent_*` aggregate (`unchecked` counts false **or** empty). |
488
488
  | select (single & multi) | `select` | `has_any_of`, `has_none_of`, `has_all_of`, `is_empty`, `is_not_empty` — values are **option keys** (`opt_*`), never labels | the **runtime** `filter` (§9) also takes option **names**, resolving each to its key — an unknown one is refused rather than matching nothing, and the value stays an array either way; a select column the query DERIVES instead of projecting from a field or a select literal carries no option set, so addressing one there is refused | Cells are arrays even for single-selects. Sorting a select column orders by its options' order (a multi-select by its first option), an empty cell last, on every page, keyset included; through a `union` whose arms project different selects, each row by its own select's order (§2 `union`); a select the query derives carries no options and orders by raw JSON. `unnest` fans option keys. |
489
489
  | select_member | `select_member` | select ops + `is_current_member`, `is_not_current_member` (field-scoped, no value; `is_not_current_member` matches every cell not holding the requester, an empty one included) | same — `is_current_member` **works at the runtime layer** too | Sorting orders by the first member's name as a reader is shown it (the one the organization gave, else their profile's; a member it does not name reads as empty text), folded like text, then by member id; an empty cell last, on every page, keyset included. `unnest` fans member ids. Anonymous public request: current-member binds the app owner (§1). |
490
- | select_record_link | `select_record_link` | membership by linked-record **id**: `has_any_of`, `has_none_of`, `has_all_of`; text over the cached **display**: `contains`, `not_contains`, `starts_with`, `ends_with`; `is_empty`, `is_not_empty` | same — id-membership works at the runtime layer (converged `[{id}]` containment) | Sorting orders by the first linked record's display, folded like text, then by its id; an empty cell last, on every page, keyset included. `unnest` fans link ids (+ display). |
490
+ | select_record_link | `select_record_link` | membership by linked-record **id**: `has_any_of`, `has_none_of`, `has_all_of`; text over the cached **display** (deprecated — filter by id, and a linked record's field through a lookup): `contains`, `not_contains`, `starts_with`, `ends_with`; `is_empty`, `is_not_empty` | same — id-membership works at the runtime layer (converged `[{id}]` containment) | Sorting orders by the first linked record's display, folded like text, then by its id; an empty cell last, on every page, keyset included. `unnest` fans link ids (+ display). |
491
491
  | files | `files` | `has_filename`, `has_mime_type` (substring), `has_file_count` (exact count), `is_empty`, `is_not_empty` | same | Presign-enriched at delivery (§1); `unnest` fans file ids. Only presence-counting aggregates. |
492
492
  | formula | its declared output type (`number`/`text`/`boolean`/`date`/`datetime`, or `select` for one stating its own options, its cell a single select's; `json` until inferred) | filtered by the output type's operators; a cell holding an error (`#ERROR: …`) holds no value — it projects as `null` whatever the output type, and matches what an empty cell matches (`is_empty`, a negation such as `not_equals`) and nothing that needs a value | same | Extracted as a real scalar, so numeric formulas feed `sum`/`avg`/sorts. A **datetime-output** formula matches day-level date filters (full-day expansion). |
493
493
  | rollup | decided by the OPERATION, never by the aggregated field: `earliest`/`latest` → `date`/`datetime` per that field's format; **every** other op → `number` | number or date operators per output type; text operators route as text | same | Datetime-format rollups match day-level date filters. `date_range` counts DAYS between the extremes, so it is a number despite its date input. A presence count (`filled`/`empty`/`percent_*`) is a number whatever it counts — including over `files`. |
package/docs/runtime.md CHANGED
@@ -156,6 +156,15 @@ outgoing message is targeted at that origin — a third frame can neither inject
156
156
  results nor observe payloads. A host error with no message rejects as
157
157
  `"RPC failed"`.
158
158
 
159
+ The host covers the app with its loading skeleton until the app's first screen
160
+ has its data. The SDK says so for you: its first request carries
161
+ `reportsDrawn: true`, and once every request is answered and none has been sent
162
+ for two animation frames it posts `{ type: "drawn" }` to the host, once per page.
163
+ A screen that issues its reads in its first render or effects is covered until
164
+ they land; one that starts a read later than two frames after the previous
165
+ answers is shown before it, and the host lifts the cover after a bounded time
166
+ following the first request.
167
+
159
168
  **Warning:** a bridged call has **no timeout**. An op the host doesn't
160
169
  implement does reject (`"Unknown RPC op: …"`), but a message the host drops
161
170
  without replying (a crashed handler, a malformed envelope) leaves the returned
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lotics/app-sdk",
3
- "version": "0.123.1",
3
+ "version": "0.123.3",
4
4
  "description": "The SDK a Lotics custom-code app reads and writes through — typed hooks over the host bridge, cell readers, mount() and AppRouter",
5
5
  "type": "module",
6
6
  "exports": {