@llmnative/react 2.2.2 → 2.4.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.
@@ -16,6 +16,8 @@ export interface I18nDict {
16
16
  goHome: string;
17
17
  /** The button that opens the menu on a small screen (the header of the scaffold). */
18
18
  openNavigation: string;
19
+ /** CR-129 — a read the database refused or lost. */
20
+ loadError: string;
19
21
  };
20
22
  auth: {
21
23
  signIn: string;
@@ -79,6 +79,12 @@ export interface ModalYesNoProps {
79
79
  /** Async handler for the No button. Return `true` to close. */
80
80
  onNo?: (e: React.MouseEvent<HTMLElement>) => Promise<boolean>;
81
81
  onClose?: () => void;
82
+ /** The words of the confirming button: the action, e.g. "Revoca" (default "Yes" of the dictionary). */
83
+ yesLabel?: string;
84
+ /** The words of the other button, e.g. "Annulla" (default "No" of the dictionary). */
85
+ noLabel?: string;
86
+ /** The action cannot be undone: the confirming button is red (`danger`) and the other one comes first. */
87
+ danger?: boolean;
82
88
  }
83
89
  /** Props for a simple acknowledgment modal with an OK button. */
84
90
  export interface ModalOkProps {
@@ -87,6 +93,6 @@ export interface ModalOkProps {
87
93
  onClose?: () => void;
88
94
  }
89
95
  declare const Modal: (props: ModalProps) => React.JSX.Element;
90
- export declare const ModalYesNo: ({ children, title, onYes, onNo, onClose }: ModalYesNoProps) => React.JSX.Element;
96
+ export declare const ModalYesNo: ({ children, title, onYes, onNo, onClose, yesLabel, noLabel, danger }: ModalYesNoProps) => React.JSX.Element;
91
97
  export declare const ModalOk: ({ children, title, onClose }: ModalOkProps) => React.JSX.Element;
92
98
  export default Modal;
@@ -1,5 +1,5 @@
1
1
  import React from "react";
2
2
  import { type RecordProps } from "../../../providers/data/DataProvider";
3
3
  import { type GridCoreProps } from "./types";
4
- declare function GridCore<TRecord extends RecordProps>({ records, recordId, sourcePath, columns, actions, form, editDeepLink, header, footer, view, views, sticky, wrapperClassName, cardClassName, bodyClassName, loading, title, before, after, sortable, pagination, selection, onRowClick, reorderable, onReorder, groupBy, searchable, filters, onSave, onDelete, onComplete, audit, onLoad, }: GridCoreProps<TRecord>): React.JSX.Element;
4
+ declare function GridCore<TRecord extends RecordProps>({ records, recordId, sourcePath, columns, actions, form, editDeepLink, header, footer, view, views, sticky, wrapperClassName, cardClassName, bodyClassName, loading, title, before, after, sortable, pagination, selection, onRowClick, reorderable, onReorder, groupBy, searchable, filters, onSave, onDelete, onComplete, audit, onLoad, loadError, }: GridCoreProps<TRecord>): React.JSX.Element;
5
5
  export default GridCore;
@@ -411,6 +411,8 @@ export type GridCoreProps<TRecord extends RecordProps> = GridBaseProps<TRecord>
411
411
  records: TRecord[];
412
412
  recordId: GridRecordKey<TRecord>;
413
413
  sourcePath?: string;
414
+ /** CR-129 — the reading of `path` failed: this message replaces the records (and their empty state). */
415
+ loadError?: React.ReactNode;
414
416
  };
415
417
  export type GridArrayProps<TRecord extends RecordProps> = GridBaseProps<TRecord> & {
416
418
  records: TRecord[];
@@ -1,9 +1,13 @@
1
- import { type DatabaseOptions, type RecordProps } from "../../../providers/data/DataProvider";
1
+ import { type DatabaseOptions, type DataProviderError, type RecordProps } from "../../../providers/data/DataProvider";
2
2
  type UseGridDBRecordsArgs = {
3
3
  path?: string;
4
4
  where?: DatabaseOptions["where"];
5
5
  order?: DatabaseOptions["order"];
6
6
  fieldMap?: DatabaseOptions["fieldMap"];
7
7
  };
8
- declare function useGridDBRecords<TRecord extends RecordProps>({ path, where, order, fieldMap, }: UseGridDBRecordsArgs): TRecord[];
8
+ /** The records of a `Grid` with `path`, live, and why they did not arrive (CR-129). */
9
+ declare function useGridDBRecords<TRecord extends RecordProps>({ path, where, order, fieldMap, }: UseGridDBRecordsArgs): {
10
+ records: TRecord[];
11
+ error: DataProviderError | null;
12
+ };
9
13
  export default useGridDBRecords;
@@ -15,7 +15,11 @@ export type { AppThemeProviderConfig, BuiltInThemeId, CardTheme, ColorScale, Gal
15
15
  export { AuthButton, useAccessToken, getAccessToken } from './auth';
16
16
  export * from './components/auth';
17
17
  export { useGlobalVars, getGlobalVars, setGlobalVars, removeGlobalVars } from './Global';
18
- export type { DataProviderAdapter, FieldValue, RecordProps, RecordArray, DatabaseOptions, DBConfig, ReadOptions, WhereClause, OrderClause, Condition } from './providers/data/DataProvider';
18
+ export type { DataProviderAdapter, FieldValue, RecordProps, RecordArray, DatabaseOptions, DBConfig, ReadOptions, WhereClause, OrderClause, Condition, DataProviderErrorCode } from './providers/data/DataProvider';
19
+ export { DataProviderError } from './providers/data/DataProvider';
20
+ export { toDataProviderError } from './providers/data/query';
21
+ export { useDataSubscription } from './providers/data/useDataSubscription';
22
+ export type { DataSubscriptionOptions, DataSubscriptionState } from './providers/data/useDataSubscription';
19
23
  export type { OrderConfig, OrderDirection } from './libs/order';
20
24
  export { Order } from './libs/order';
21
25
  export { PromptUtils } from './libs/promptUtils';
@@ -28,6 +28,10 @@ export type Condition = {
28
28
  gte?: RangeValue;
29
29
  nin?: ListValue;
30
30
  in?: ListValue;
31
+ /** CR-129 — the field is a list that holds this value (Firestore `array-contains`, Supabase `contains`). */
32
+ contains?: ScalarValue;
33
+ /** CR-129 — the field is a list that holds at least one of these values (Firestore `array-contains-any`, Supabase `overlaps`). */
34
+ containsAny?: ListValue;
31
35
  };
32
36
  export type WhereClause = {
33
37
  [field: string]: Condition | OperatorValue;
@@ -35,9 +39,32 @@ export type WhereClause = {
35
39
  export type OrderClause = {
36
40
  [field: string]: "asc" | "desc";
37
41
  };
42
+ /**
43
+ * CR-129 — why a read failed, the same on every provider: the rules refused it (`permission-denied`), there
44
+ * is no session (`unauthenticated`), the query cannot run as written, e.g. a missing index or an operator the
45
+ * database refuses (`invalid-query`), the database cannot be reached (`unavailable`), or anything else.
46
+ */
47
+ export type DataProviderErrorCode = 'permission-denied' | 'unauthenticated' | 'invalid-query' | 'unavailable' | 'unknown';
48
+ /** CR-129 — a failed read, normalized from the error of the database (kept in `cause`). */
49
+ export declare class DataProviderError extends Error {
50
+ readonly code: DataProviderErrorCode;
51
+ readonly path?: string;
52
+ readonly cause?: unknown;
53
+ constructor(code: DataProviderErrorCode, message: string, options?: {
54
+ path?: string;
55
+ cause?: unknown;
56
+ });
57
+ }
38
58
  export interface DatabaseOptions {
39
59
  where?: WhereClause;
40
60
  order?: OrderClause;
61
+ /** CR-129 — at most this many records, after `where` and `order`. */
62
+ limit?: number;
63
+ /**
64
+ * CR-129 — called when the database refuses or loses the subscription. Without it the provider logs the
65
+ * error as before; with it the page can say why the data did not arrive.
66
+ */
67
+ onError?: (error: DataProviderError) => void;
41
68
  fieldMap?: Record<string, string>;
42
69
  onLoad?: (data: RecordObject) => RecordObject;
43
70
  }
@@ -47,6 +74,8 @@ export interface DBConfig extends DatabaseOptions {
47
74
  export interface ReadOptions {
48
75
  where?: WhereClause;
49
76
  order?: OrderClause;
77
+ /** CR-129 — at most this many records, after `where` and `order`. */
78
+ limit?: number;
50
79
  toArray?: boolean;
51
80
  exception?: boolean;
52
81
  }
@@ -10,13 +10,13 @@ export declare class FirebaseDataProvider implements DataProviderAdapter {
10
10
  getConfigurationState(): ProviderConfigurationState;
11
11
  isConfigured(): boolean;
12
12
  readShallow: (path: string, exception?: boolean) => Promise<string[]>;
13
- read: (path: string, { where, order, toArray, exception }?: ReadOptions) => Promise<any>;
13
+ read: (path: string, { where, order, limit, toArray, exception }?: ReadOptions) => Promise<any>;
14
14
  update: (path: string, data: object, exception?: boolean) => Promise<void>;
15
15
  set: (path: string, data: object, exception?: boolean) => Promise<void>;
16
16
  setChunks: (path: string, data: object, { chunkSize, purge, onProgress }?: SetChunksOptions) => Promise<void>;
17
17
  remove: (path: string, exception?: boolean) => Promise<void>;
18
18
  count: (path: string) => Promise<number>;
19
- subscribe: <T extends RecordProps = RecordProps>(path: string | undefined, setRecords: (records: T[]) => void, { where, order, fieldMap, onLoad }?: DatabaseOptions) => (() => void);
19
+ subscribe: <T extends RecordProps = RecordProps>(path: string | undefined, setRecords: (records: T[]) => void, options?: DatabaseOptions) => (() => void);
20
20
  }
21
21
  declare const _default: FirebaseDataProvider;
22
22
  export default _default;
@@ -23,7 +23,7 @@ export declare class FirestoreDataProvider implements DataProviderAdapter {
23
23
  dispose: () => Promise<void>;
24
24
  getConfigurationState(): ProviderConfigurationState;
25
25
  isConfigured(): boolean;
26
- read: (path: string, { where, order, toArray, exception }?: ReadOptions) => Promise<any>;
26
+ read: (path: string, { where, order, limit, toArray, exception }?: ReadOptions) => Promise<any>;
27
27
  set: (path: string, data: object, exception?: boolean) => Promise<void>;
28
28
  update: (path: string, data: object, exception?: boolean) => Promise<void>;
29
29
  remove: (path: string, exception?: boolean) => Promise<void>;
@@ -39,7 +39,7 @@ export declare class FirestoreDataProvider implements DataProviderAdapter {
39
39
  * Firestore inequality filters on a field require the first orderBy() to be
40
40
  * on that same field. Pass `order` accordingly to avoid SDK errors.
41
41
  */
42
- subscribe: <T extends RecordProps = RecordProps>(path: string | undefined, setRecords: (records: T[]) => void, { where, order, fieldMap, onLoad }?: DatabaseOptions) => (() => void);
42
+ subscribe: <T extends RecordProps = RecordProps>(path: string | undefined, setRecords: (records: T[]) => void, options?: DatabaseOptions) => (() => void);
43
43
  /**
44
44
  * Returns the list of document IDs in a collection without loading document
45
45
  * data - the Firestore equivalent of RTDB's ?shallow=true REST endpoint.
@@ -0,0 +1,14 @@
1
+ import { DataProviderError, type Condition, type DatabaseOptions, type OperatorValue } from './DataProvider';
2
+ /** A `where` value as a `Condition`: a list is `in`, a scalar is `eq`. */
3
+ export declare function toCondition(raw: Condition | OperatorValue): Condition;
4
+ /** Whether `value` (the field of a record) satisfies a condition of `where`. Unknown operators never match. */
5
+ export declare function matchCondition(value: unknown, raw: Condition | OperatorValue): boolean;
6
+ /** The first `limit` items (all of them without a valid limit). */
7
+ export declare function applyLimit<T>(items: T[], limit?: number): T[];
8
+ /** Any error of a database as a `DataProviderError` (an existing one is returned as it is). */
9
+ export declare function toDataProviderError(error: unknown, path?: string): DataProviderError;
10
+ /**
11
+ * Tells the page that a subscription failed: `options.onError` when it is given, else the console as the
12
+ * providers did before CR-129. Returns the normalized error.
13
+ */
14
+ export declare function reportSubscribeError(options: Pick<DatabaseOptions, 'onError'> | undefined, error: unknown, path?: string): DataProviderError;
@@ -0,0 +1,20 @@
1
+ import type { DataProviderError, DatabaseOptions, RecordArray } from './DataProvider';
2
+ export type DataSubscriptionOptions = Pick<DatabaseOptions, 'where' | 'order' | 'limit' | 'fieldMap'> & {
3
+ /** Read nothing (a page still waiting for an id): `loading` false, no records. */
4
+ skip?: boolean;
5
+ /** The name of a provider registered in `providers` (the default one without it). */
6
+ provider?: string;
7
+ };
8
+ export type DataSubscriptionState = {
9
+ records: RecordArray;
10
+ loading: boolean;
11
+ /** Why the data did not arrive (rules, session, query, network), the same on every provider. */
12
+ error: DataProviderError | null;
13
+ };
14
+ /**
15
+ * CR-129 — the records at `path`, live, from the data provider: `{ records, loading, error }`. `loading` is
16
+ * true until the first answer; a subscription the database refuses gives `error` instead of a list that never
17
+ * arrives. `where`, `order` and `limit` are compared by value, so a new object with the same content does not
18
+ * subscribe again.
19
+ */
20
+ export declare function useDataSubscription(path: string | null | undefined, options?: DataSubscriptionOptions): DataSubscriptionState;
package/llms-full.txt CHANGED
@@ -218,9 +218,9 @@ await oauth.getOrganizations(accessToken) // [{ login, avatarUrl, role: 'admin
218
218
 
219
219
  ```ts
220
220
  // DataProviderAdapter
221
- read(path, options?) / set(path, data) / update(path, data) / remove(path)
222
- subscribe(path, callback: (records: RecordArray) => void): () => void
223
- count?(path, where?): Promise<number>
221
+ read(path, { where?, order?, limit?, toArray?, exception? }) / set(path, data, exception?) / update(path, data, exception?) / remove(path, exception?)
222
+ subscribe(path, callback: (records: RecordArray) => void, { where?, order?, limit?, fieldMap?, onLoad?, onError? }): () => void
223
+ count?(path): Promise<number>
224
224
 
225
225
  // StorageProviderAdapter
226
226
  upload(file, path): Promise<string|undefined>
@@ -317,6 +317,35 @@ const creds = useCredentialsProvider()
317
317
  const token = await creds.getToken('https://www.googleapis.com/auth/solar')
318
318
  ```
319
319
 
320
+ ### Letture: filtri, `limit`, errori (CR-129)
321
+
322
+ Gli stessi operatori e lo stesso contratto su ogni provider (`firestoreDb`, `supabaseDb`, `dbRealtime`, `mock`):
323
+ l'adapter li traduce nei comandi del database, o li applica nel browser dove il database non li ha.
324
+
325
+ | `where` | Significato | Firestore | Supabase | Realtime DB / Mock |
326
+ |---|---|---|---|---|
327
+ | `{ f: 'x' }` / `{ f: { eq } }` | uguale | `==` | `eq` | server sul campo di `order`, altrimenti browser |
328
+ | `lt` `lte` `gt` `gte` | confronto | `<` … `>=` | `lt` … `gte` | idem |
329
+ | `{ f: ['a','b'] }` / `in`, `nin` | uno dei valori / nessuno | `in` (≤30), `not-in` (≤10) | `in`, `not.in` | browser |
330
+ | `contains: v` | il campo lista contiene `v` | `array-contains` | `contains` | browser |
331
+ | `containsAny: [..]` | contiene almeno uno | `array-contains-any` (≤30) | `overlaps` | browser |
332
+
333
+ `limit: n` (in `read` e `subscribe`) tiene i primi `n` dopo `where` e `order`: Firestore `limit()`, Supabase
334
+ `.limit()`, Realtime Database `limitToFirst`/`limitToLast` (nel browser se restano filtri da applicare lì).
335
+
336
+ Un'iscrizione che il database rifiuta (regole, sessione, indice mancante, rete) arriva a `onError` come
337
+ `DataProviderError` con `code`: `permission-denied` | `unauthenticated` | `invalid-query` | `unavailable` |
338
+ `unknown` (l'errore originale in `cause`). Senza `onError` il provider la scrive in console come prima.
339
+ Un percorso di record (`/users/u1`) segue quel record su ogni provider.
340
+
341
+ ```ts
342
+ const { records, loading, error } = useDataSubscription('/registro', { order: { at: 'desc' }, limit: 50 })
343
+ // error?.code === 'permission-denied' → la pagina dice che non può leggere, invece di restare in caricamento
344
+ db.subscribe('/posts', setRecords, { where: { tags: { contains: 'news' } }, limit: 20, onError: (e) => show(e.code) })
345
+ ```
346
+
347
+ `Grid` con `path` mostra `common.loadError` al posto delle righe quando la lettura fallisce.
348
+
320
349
  ### Provider custom
321
350
  ```ts
322
351
  import { DataProviderAdapter, RecordArray } from '@llmnative/react'
@@ -743,6 +772,8 @@ Quando `commands` e presente in `mode="run"`, `Prompt` monta internamente `Conte
743
772
  {content}
744
773
  </Modal>
745
774
  <ModalYesNo title="Sicuro?" onClose={() => {}} onYes={async () => true}>Eliminare il record?</ModalYesNo>
775
+ // Un'azione che non si annulla: nominala e mettila in rosso (CR-128); "Annulla" viene per primo.
776
+ <ModalYesNo title="Revocare l'invito?" danger yesLabel="Revoca" noLabel="Annulla" onYes={revoke} onNo={async () => true}>La persona non potrà più usare il link.</ModalYesNo>
746
777
  <ModalOk title="Completato!" onClose={() => {}}>Salvato.</ModalOk>
747
778
  ```
748
779
  `size`: `'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'fullscreen'`
@@ -1471,7 +1502,10 @@ type FieldValue = string | number | boolean | null | undefined | Record<string
1471
1502
  type RecordArray = RecordProps[]
1472
1503
 
1473
1504
  type OrderConfig = { field: string; dir: 'asc' | 'desc' }
1474
- type WhereClause = { field: string; op: '==' | '!=' | '<' | '<=' | '>' | '>=' | 'in' | 'not-in' | 'array-contains' | 'array-contains-any'; value: unknown }
1505
+ type Condition = { eq?, lt?, lte?, gt?, gte?, in?: list, nin?: list, contains?: scalar, containsAny?: list }
1506
+ type WhereClause = { [field: string]: Condition | scalar | list } // scalar = eq, list = in
1507
+ type OrderClause = { [field: string]: 'asc' | 'desc' }
1508
+ class DataProviderError extends Error { code: 'permission-denied' | 'unauthenticated' | 'invalid-query' | 'unavailable' | 'unknown'; path?: string; cause?: unknown }
1475
1509
 
1476
1510
  interface UIProps {
1477
1511
  before?: React.ReactNode
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llmnative/react",
3
- "version": "2.2.2",
3
+ "version": "2.4.0",
4
4
  "description": "LLM-native React framework for deterministic UI generation. Data-driven by default, schema-driven optional. Ultra-low token consumption — built for AI agents, not just humans.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
package/themes/cyber.ts CHANGED
@@ -226,7 +226,7 @@ export const components: Theme = {
226
226
  wrapperClassName: '',
227
227
  className: '',
228
228
  icon: 'loader-2',
229
- title: 'Loading..',
229
+ title: '',
230
230
  description: '',
231
231
  },
232
232
  ActionButton: {
package/themes/default.ts CHANGED
@@ -242,7 +242,7 @@ export const components: Theme = {
242
242
  wrapperClassName: '',
243
243
  className: '',
244
244
  icon: 'loader-2',
245
- title: 'Loading..',
245
+ title: '',
246
246
  description: '',
247
247
  },
248
248
  ActionButton: {
package/themes/flat.ts CHANGED
@@ -226,7 +226,7 @@ export const components: Theme = {
226
226
  wrapperClassName: '',
227
227
  className: '',
228
228
  icon: 'loader-2',
229
- title: 'Loading..',
229
+ title: '',
230
230
  description: '',
231
231
  },
232
232
  ActionButton: {