@llmnative/react 1.20.0 → 1.22.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/dist/index.css +1 -1
- package/dist/index.js +15 -15
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1453 -1324
- package/dist/index.mjs.map +1 -1
- package/dist/types/src/I18n.d.ts +6 -0
- package/dist/types/src/Theme.d.ts +3 -0
- package/dist/types/src/components/blocks/SideNav.d.ts +7 -1
- package/dist/types/src/components/ui/fields/Select.d.ts +11 -2
- package/llms-full.txt +1182 -1148
- package/package.json +1 -1
- package/themes/cyber.ts +3 -0
- package/themes/default.ts +3 -0
- package/themes/flat.ts +3 -0
package/llms-full.txt
CHANGED
|
@@ -1,1148 +1,1182 @@
|
|
|
1
|
-
# @llmnative/react — Riferimento completo
|
|
2
|
-
|
|
3
|
-
Framework React AI-first per generazione deterministica di UI. Data-driven di default, schema-driven opzionale.
|
|
4
|
-
Versione 1.0.0 — Apache-2.0
|
|
5
|
-
|
|
6
|
-
```bash
|
|
7
|
-
npm install @llmnative/react
|
|
8
|
-
import '@llmnative/react/dist/index.css'
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## Regole fondamentali
|
|
14
|
-
|
|
15
|
-
- Tutti gli import da `'@llmnative/react'` — mai da sottodirectory
|
|
16
|
-
- I provider sono iniettati via `<App providers={...}>` — mai importati direttamente nei componenti
|
|
17
|
-
- Usare `useDataProvider()`, `useAuthProvider()`, ecc. nei componenti, non istanze dirette
|
|
18
|
-
- TypeScript strict: no `any` — usare `unknown`, `Record<string, unknown>`, tipi specifici
|
|
19
|
-
- Naming props: `view` (presentazione contenuto), `appearance` (guscio visuale), `layout` (disposizione spaziale), `mode` (comportamento operativo), `variant` (tono semantico), `position` (ancoraggio), `size` (scala)
|
|
20
|
-
|
|
21
|
-
---
|
|
22
|
-
|
|
23
|
-
## App — Entry point
|
|
24
|
-
|
|
25
|
-
```tsx
|
|
26
|
-
<App
|
|
27
|
-
providers={{
|
|
28
|
-
firebase: { // FirebaseConfig — tutte le chiavi flat
|
|
29
|
-
apiKey, authDomain, databaseURL, projectId,
|
|
30
|
-
storageBucket, messagingSenderId, appId,
|
|
31
|
-
},
|
|
32
|
-
supabase: { url, anonKey },
|
|
33
|
-
google: { clientId, scope }, // flat — NON { oAuth2: { clientId } }
|
|
34
|
-
dropbox: { clientId }, // DropboxConfig
|
|
35
|
-
mock: { data: mockData },
|
|
36
|
-
ai: { // AIConfig — NON una prop separata di <App>
|
|
37
|
-
openaiApiKey, geminiApiKey, anthropicApiKey,
|
|
38
|
-
deepSeekApiKey, mistralApiKey, openRouterApiKey,
|
|
39
|
-
groqApiKey,
|
|
40
|
-
cloudflare: { apiToken, accountId }, // Cloudflare Workers AI
|
|
41
|
-
},
|
|
42
|
-
proxy: { enable: true, route: '/api/proxy' },
|
|
43
|
-
custom: { data: { myAdapter: new MyDataProvider() } },
|
|
44
|
-
services: {
|
|
45
|
-
data: 'dbRealtime', // DataDriverName
|
|
46
|
-
storage: 'firestorage', // StorageDriverName | null
|
|
47
|
-
auth: 'googleAuth', // AuthDriverName | null
|
|
48
|
-
email: 'gmail', // EmailDriverName | null
|
|
49
|
-
ai: 'openai', // AIDriverName | null
|
|
50
|
-
credentials: 'googleServiceAccount',
|
|
51
|
-
},
|
|
52
|
-
}}
|
|
53
|
-
iconProvider="lucide" // 'lucide' | 'phosphor' | AppIconProviderConfig
|
|
54
|
-
themeProvider="default" // 'default' | 'flat' | 'cyber' | AppThemeProviderConfig
|
|
55
|
-
menuConfig={menuConfig}
|
|
56
|
-
importPage={(path) => import(path)}
|
|
57
|
-
LayoutDefault={MyLayout} // layout wrapper custom (opzionale)
|
|
58
|
-
appName="MyApp" // nome app (opzionale)
|
|
59
|
-
i18n={{ locale: 'it', translations: { ... } }}
|
|
60
|
-
contextProviders={MyContextProvider}
|
|
61
|
-
errorReportUrl="/api/errors"
|
|
62
|
-
debug={import.meta.env.DEV}
|
|
63
|
-
/>
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
### AppThemeProviderConfig
|
|
67
|
-
```ts
|
|
68
|
-
{
|
|
69
|
-
mode?: 'light' | 'dark' | 'system';
|
|
70
|
-
theme: string;
|
|
71
|
-
themes?: Record<string, {
|
|
72
|
-
preset?: { colors?: ColorScale; radius?: number; font?: string };
|
|
73
|
-
motion?: MotionRegistry;
|
|
74
|
-
components?: Record<string, Partial<ThemeComponent>>;
|
|
75
|
-
}>;
|
|
76
|
-
themeOverride?: Record<string, Partial<ThemeComponent>>;
|
|
77
|
-
}
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
Built-in themes: `default`, `flat`, `cyber`.
|
|
81
|
-
|
|
82
|
-
---
|
|
83
|
-
|
|
84
|
-
## Provider System
|
|
85
|
-
|
|
86
|
-
### Matrix completa
|
|
87
|
-
|
|
88
|
-
| Servizio | Interfaccia | Driver | Config key | Obbligatorio |
|
|
89
|
-
|---|---|---|---|---|
|
|
90
|
-
| data | `DataProviderAdapter` | `mock`, `dbRealtime`, `firestoreDb`, `supabaseDb` | `providers.mock/firebase/supabase` | sì (fallback: mock) |
|
|
91
|
-
| storage | `StorageProviderAdapter` | `firestorage`, `supabaseStorage` | `providers.firebase/supabase` | no |
|
|
92
|
-
| auth | `AuthProviderAdapter` | `googleAuth`, `firebaseAuth`, `supabaseAuth`, `dropboxAuth` | `providers.google/firebase/supabase/dropbox` | sì (fallback: googleAuth) |
|
|
93
|
-
| email | `EmailProviderAdapter` | `gmail` | `providers.google` | no |
|
|
94
|
-
| ai | `AIProviderAdapter` | `openai`, `gemini`, `anthropic`, `deepseek`, `mistral`, `openrouter`, `opencode`, `cloudflare`, `groq` | `Config.tsx AIConfig` | no |
|
|
95
|
-
| credentials | `CredentialsAdapter` | `googleServiceAccount` | `providers.google.serviceAccount` | no |
|
|
96
|
-
|
|
97
|
-
### Hooks
|
|
98
|
-
|
|
99
|
-
```ts
|
|
100
|
-
const data = useDataProvider() // DataProviderAdapter | null
|
|
101
|
-
const storage = useStorageProvider() // StorageProviderAdapter | null
|
|
102
|
-
const auth = useAuthProvider() // AuthProviderAdapter | null
|
|
103
|
-
const email = useEmailProvider() // EmailProviderAdapter | null
|
|
104
|
-
const ai = useAIProvider() // AIProviderAdapter | null
|
|
105
|
-
const creds = useCredentialsProvider() // CredentialsAdapter | null
|
|
106
|
-
|
|
107
|
-
// Named provider (multi-provider setup)
|
|
108
|
-
const supabase = useDataProvider('supabase')
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### Interfacce principali
|
|
112
|
-
|
|
113
|
-
```ts
|
|
114
|
-
// DataProviderAdapter
|
|
115
|
-
read(path, options?) / set(path, data) / update(path, data) / remove(path)
|
|
116
|
-
subscribe(path, callback: (records: RecordArray) => void): () => void
|
|
117
|
-
count?(path, where?): Promise<number>
|
|
118
|
-
|
|
119
|
-
// StorageProviderAdapter
|
|
120
|
-
upload(file, path): Promise<string|undefined>
|
|
121
|
-
getURL(path) / download(path): Promise<Blob|undefined> / delete(path): Promise<boolean>
|
|
122
|
-
|
|
123
|
-
// AuthProviderAdapter
|
|
124
|
-
signIn(intent?, options?) / signOut() / getUser(): UserProfile|null
|
|
125
|
-
onAuthChange(callback) / getAccessToken(): Promise<string|null>
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
### Auth sign-in methods
|
|
129
|
-
|
|
130
|
-
| method | Firebase | Supabase | Google |
|
|
131
|
-
|---|---|---|---|
|
|
132
|
-
| `password` | ✅ | ✅ | — |
|
|
133
|
-
| `anonymous` | ✅ | ✅ | — |
|
|
134
|
-
| `magic_link` | — | ✅ | — |
|
|
135
|
-
| `oauth` (github, apple…) | ✅ | ✅ | — |
|
|
136
|
-
| GIS + scoped token | — | — | ✅ |
|
|
137
|
-
|
|
138
|
-
```ts
|
|
139
|
-
await auth.signIn({ method: 'password', email: '...', password: '...' })
|
|
140
|
-
await auth.signIn({ method: 'oauth', provider: 'github' })
|
|
141
|
-
// Google: token per API scope
|
|
142
|
-
const token = await auth.getAccessToken('https://www.googleapis.com/auth/gmail.send')
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
### CredentialsAdapter (Google Service Account)
|
|
146
|
-
```tsx
|
|
147
|
-
// Chiama Google APIs lato browser senza Node.js
|
|
148
|
-
<App providers={{ google: { serviceAccount: { clientEmail, privateKey } }, services: { credentials: 'googleServiceAccount' } }} />
|
|
149
|
-
|
|
150
|
-
const creds = useCredentialsProvider()
|
|
151
|
-
const token = await creds.getToken('https://www.googleapis.com/auth/solar')
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
### Provider custom
|
|
155
|
-
```ts
|
|
156
|
-
import { DataProviderAdapter, RecordArray } from '@llmnative/react'
|
|
157
|
-
|
|
158
|
-
export class RestDataProvider implements DataProviderAdapter {
|
|
159
|
-
async read(path: string): Promise<unknown> { ... }
|
|
160
|
-
async set(path: string, data: object): Promise<void> { ... }
|
|
161
|
-
async update(path: string, data: object): Promise<void> { ... }
|
|
162
|
-
async remove(path: string): Promise<void> { ... }
|
|
163
|
-
subscribe(path: string | undefined, callback: (records: RecordArray) => void): () => void {
|
|
164
|
-
return () => undefined
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
---
|
|
170
|
-
|
|
171
|
-
## Grid — CRUD table/gallery
|
|
172
|
-
|
|
173
|
-
```tsx
|
|
174
|
-
<Grid
|
|
175
|
-
path="/users" // DataProvider-backed — oppure records={array} per in-memory
|
|
176
|
-
columns={[
|
|
177
|
-
{ key: 'name', label: 'Nome', sortable: true },
|
|
178
|
-
{ key: 'email', label: 'Email' },
|
|
179
|
-
{ key: 'role', label: 'Ruolo', render: ({ value }) => <Badge>{value}</Badge> },
|
|
180
|
-
{ key: 'date', label: 'Data', render: 'date' },
|
|
181
|
-
]}
|
|
182
|
-
actions={['add', 'edit', 'delete']} // oppure Record<string, GridAction>
|
|
183
|
-
view="table" // 'table' | 'gallery'
|
|
184
|
-
pagination={{ limit: 20 }}
|
|
185
|
-
groupBy="status"
|
|
186
|
-
sortable={{ field: 'name', dir: 'asc' }}
|
|
187
|
-
form={<FormFields />}
|
|
188
|
-
onSave={async ({ record, action }) => record._key}
|
|
189
|
-
onComplete={async ({ record, action }) => true}
|
|
190
|
-
selection={{ mode: 'multiple', onChange: (sel) => {} }}
|
|
191
|
-
reorderable
|
|
192
|
-
onReorder={(records, meta) => {}}
|
|
193
|
-
header={(ctx) => <div>{ctx.title}</div>}
|
|
194
|
-
footer={(ctx) => <div>{ctx.records.length} records</div>}
|
|
195
|
-
/>
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
| Prop | Tipo | Note |
|
|
199
|
-
|---|---|---|---|
|
|
200
|
-
| `path` | `string \| "fromUrl"` | Path provider-backed |
|
|
201
|
-
| `records` | `RecordArray` | Dati in memoria (alternativa a path) |
|
|
202
|
-
| `columns` | `GridColumn[]` | `{ key, label, sortable?, render?, className? }` |
|
|
203
|
-
| `actions` | `Array<'add'\|'edit'\|'delete'> \| Record<string, GridAction>` | |
|
|
204
|
-
| `view` | `'table' \| 'gallery'` | |
|
|
205
|
-
| `groupBy` | `string \| string[]` | |
|
|
206
|
-
| `pagination` | `PaginationParams` | `{ limit, page?, maxPageButtons?, align?, sticky? }` |
|
|
207
|
-
| `sortable` | `boolean \| OrderConfig` | |
|
|
208
|
-
| `selection` | `false \| 'single' \| 'multiple' \| GridSelectionConfig` | |
|
|
209
|
-
| `form` | `ReactElement \| ((ctx) => ReactNode)` | Form condiviso per CRUD |
|
|
210
|
-
| `onSave` | `GridMutationSaveHandler` | `async ({ record, action }) => string` |
|
|
211
|
-
| `onDelete` | `GridMutationDeleteHandler` | `async ({ record }) => string` |
|
|
212
|
-
| `onComplete` | `GridAfterActionHandler` | `async ({ record, action }) => boolean` |
|
|
213
|
-
| `onRowClick` | `(record) => void` | |
|
|
214
|
-
| `reorderable` | `boolean` | Abilita drag & drop righe |
|
|
215
|
-
| `onReorder` | `GridReorderHandler` | `(records, meta) => void` |
|
|
216
|
-
| `onLoad` | `(records) => TRecord[]` | Trasforma records dopo il fetch |
|
|
217
|
-
| `editDeepLink` | `boolean` | Sincronizza modal edit con URL |
|
|
218
|
-
| `audit` | `boolean` | Logga mutazioni |
|
|
219
|
-
| `sticky` | `'top' \| 'bottom'` | Header/footer sticky |
|
|
220
|
-
| `wrapperClassName` | `string` | CSS wrapper esterno |
|
|
221
|
-
| `loading` | `boolean` | Stato loading override |
|
|
222
|
-
| `title` | `ReactNode` | Titolo header grid |
|
|
223
|
-
| `before` / `after` | `ReactNode` | Slot prima/dopo il contenuto |
|
|
224
|
-
| `header` / `footer` | `ReactNode \| ((ctx) => ReactNode)` | Header/footer custom |
|
|
225
|
-
| `searchable` | `boolean \| GridSearchConfig` | Search box nell'header default — `{ fields?, placeholder? }` |
|
|
226
|
-
| `filters` | `GridFilterConfig[]` | Checkbox toggle nell'header default, applicati PRIMA di `searchable` — `{ key, label, defaultValue?, predicate(record, value) }` |
|
|
227
|
-
|
|
228
|
-
**GridAction kind:** `'modal'` (default), `'route'`, `'external'`, `'inline'`, `'delete'`
|
|
229
|
-
|
|
230
|
-
---
|
|
231
|
-
|
|
232
|
-
## Form — form CRUD con lifecycle
|
|
233
|
-
|
|
234
|
-
```tsx
|
|
235
|
-
<Form
|
|
236
|
-
path="/products"
|
|
237
|
-
appearance="card" // 'card' | 'empty'
|
|
238
|
-
showBack
|
|
239
|
-
defaultValues={{ status: 'draft' }}
|
|
240
|
-
keyGenerator={() => `prod_${Date.now()}`}
|
|
241
|
-
onLoad={(data) => { /* side-effect hook, non trasforma */ }}
|
|
242
|
-
onSave={async ({ record, action }) => { /* return storagePath or undefined */ }}
|
|
243
|
-
onDelete={async ({ record }) => { /* confirm */ }}
|
|
244
|
-
onComplete={async ({ action }) => { if (action === 'save') navigate('/products') }}
|
|
245
|
-
>
|
|
246
|
-
<Input name="title" label="Titolo" required />
|
|
247
|
-
<Input name="price" label="Prezzo" type="number" />
|
|
248
|
-
<Input name="address.city" label="Città" /> {/* oggetto annidato */}
|
|
249
|
-
<Repeat name="tags" defaultLength={1}>
|
|
250
|
-
{(i) => <Input name={`tags.${i}`} label={`Tag ${i+1}`} />}
|
|
251
|
-
</Repeat>
|
|
252
|
-
</Form>
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
| Prop | Tipo | Note |
|
|
256
|
-
|---|---|---|---|
|
|
257
|
-
| `path` | `string` | Path provider-backed |
|
|
258
|
-
| `appearance` | `'card' \| 'empty'` | Guscio visuale |
|
|
259
|
-
| `controller` | `FormController` | Controller condiviso per bottoni custom esterni |
|
|
260
|
-
| `showBack` | `boolean` | Pulsante Back nel footer |
|
|
261
|
-
| `defaultValues` | `RecordProps` | Valori iniziali per nuovo record |
|
|
262
|
-
| `keyGenerator` | `(record) => string` | Generatore chiave primaria custom |
|
|
263
|
-
| `draftBucket` | `string` | Bucket stabile che isola la bozza locale; omettilo per disabilitare la persistenza |
|
|
264
|
-
| `noticeAnchorRef` | `React.RefObject<HTMLElement>` | Ancoraggio sticky per notifiche |
|
|
265
|
-
| `showNotice` | `boolean` | Mostra notifiche save/delete (default `true`) |
|
|
266
|
-
| `children` | `ReactNode \| ((fields) => ReactNode) \| (({ record }) => ReactNode)` | Contenuto form |
|
|
267
|
-
| `header` | `ReactNode` | Contenuto header custom |
|
|
268
|
-
| `footer` | `ReactNode` | Contenuto footer custom |
|
|
269
|
-
| `wrapperClassName` | `string` | CSS wrapper esterno |
|
|
270
|
-
| `headerClassName` | `string` | CSS header |
|
|
271
|
-
| `className` | `string` | CSS body form |
|
|
272
|
-
| `footerClassName` | `string` | CSS footer |
|
|
273
|
-
| `log` | `boolean` | Logga ogni modifica su console |
|
|
274
|
-
| `onLoad` | `(record) => void` | Side-effect dopo il caricamento (non trasforma) |
|
|
275
|
-
| `onRecordChange` | `(record) => void` | Cambio campo in tempo reale |
|
|
276
|
-
| `onSave` | `FormSaveHandler` | `async ({ record, prevRecord?, storagePath?, action }) => string \| undefined` |
|
|
277
|
-
| `onDelete` | `FormDeleteHandler` | `async ({ record }) => string \| undefined` |
|
|
278
|
-
| `onComplete` | `FormFinallyHandler` | `async ({ record, action }) => boolean` |
|
|
279
|
-
|
|
280
|
-
**Form controller** — stato e azioni native del form accessibili da qualsiasi punto della pagina.
|
|
281
|
-
|
|
282
|
-
Due hook:
|
|
283
|
-
|
|
284
|
-
| Hook | Dove usarlo | Cosa restituisce |
|
|
285
|
-
|---|---|---|
|
|
286
|
-
| `useFormController(controller?)` | Ovunque (anche fuori dal form) | Crea o si aggancia a un `FormController`. Senza argomenti ne crea uno locale stabile. Con un `controller` esistente ci si aggancia. |
|
|
287
|
-
| `useFormActions()` | Solo dentro `<Form>` (legge dal context) | Restituisce lo stesso `FormController` del form genitore. Lancia errore se usato fuori. |
|
|
288
|
-
|
|
289
|
-
Esempio — bottone custom fuori dal form:
|
|
290
|
-
```tsx
|
|
291
|
-
const form = useFormController();
|
|
292
|
-
|
|
293
|
-
<ActionButton disabled={form.saveDisabled} onClick={() => { void form.save(); }} />
|
|
294
|
-
<Form controller={form} draftBucket="workspace/acme" onSave={...} />
|
|
295
|
-
```
|
|
296
|
-
|
|
297
|
-
Esempio — uso dentro il form (senza passare controller):
|
|
298
|
-
```tsx
|
|
299
|
-
function MySaveButton() {
|
|
300
|
-
const form = useFormActions();
|
|
301
|
-
return <LoadingButton disabled={form.saveDisabled} loading={form.isSaving} onClick={form.save} />;
|
|
302
|
-
}
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
Stato esposto dal controller: `record`, `isNewRecord`, `isDirty`, `canSave`, `canDelete`, `isSaving`, `isDeleting`, `saveDisabled`, `deleteDisabled`, `hasDraft`, `draftStatus`, `errors`, `hasErrors`.
|
|
306
|
-
|
|
307
|
-
Azioni: `save()`, `delete()`, `reset()`, `restoreDraft()`, `discardDraft()`.
|
|
308
|
-
|
|
309
|
-
---
|
|
310
|
-
|
|
311
|
-
## Campi form
|
|
312
|
-
|
|
313
|
-
### Input
|
|
314
|
-
```tsx
|
|
315
|
-
<Input name="x" label="..." required /> // text (default)
|
|
316
|
-
<Input name="price" type="number" min={0} max={100} step={0.01} />
|
|
317
|
-
<Input name="email" type="email" />
|
|
318
|
-
<Input name="pwd" type="password" />
|
|
319
|
-
<Input name="date" type="date" />
|
|
320
|
-
<Input name="color" type="color" />
|
|
321
|
-
<TextArea name="bio" minHeight={96} />
|
|
322
|
-
<Checkbox name="active" valueChecked="yes" />
|
|
323
|
-
```
|
|
324
|
-
`Input` props: `name`(req) `label` `type` `required` `disabled` `placeholder` `min` `max` `step` `validator:(v)=>string|undefined`
|
|
325
|
-
`type`: `'text' | 'number' | 'email' | 'password' | 'color' | 'date' | 'time' | 'datetime-local' | 'week' | 'month' | 'range' | 'checkbox' | 'radio' | 'url'`
|
|
326
|
-
|
|
327
|
-
**NOTA**: il tipo HTML si passa con `type`. `inputType` e i sotto-componenti `Input.Number`/`Input.Email`/`Input.Date` non esistono.
|
|
328
|
-
|
|
329
|
-
### Select / Autocomplete / Checklist
|
|
330
|
-
```tsx
|
|
331
|
-
<Select name="role" options={[{ label: 'Admin', value: 'admin' }]} />
|
|
332
|
-
<Select name="categoryId" optionsSource={{ path: '/categories', labelField: 'name', valueField: '_key' }} />
|
|
333
|
-
<
|
|
334
|
-
<
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
| Prop | Tipo | Note |
|
|
338
|
-
|---|---|---|
|
|
339
|
-
| `options` | `Option[] \| string[] \| number[]` | |
|
|
340
|
-
| `optionsSource` | `{ path, labelField, valueField?, where?, order?, fieldMap?, onLoad? }` | Carica da provider |
|
|
341
|
-
| `order` | `{ field: 'label' \| 'value', dir: 'asc' \| 'desc' }` | |
|
|
342
|
-
| `placeholderOption` | `Option` | Solo Select |
|
|
343
|
-
| `placeholder` | `string` | Solo Autocomplete |
|
|
344
|
-
| `creatable` | `boolean` | Solo Autocomplete — crea nuovi valori |
|
|
345
|
-
| `onCreate` | `(value) => Promise<void>` | Solo Autocomplete |
|
|
346
|
-
| `
|
|
347
|
-
| `
|
|
348
|
-
| `
|
|
349
|
-
| `
|
|
350
|
-
| `
|
|
351
|
-
| `
|
|
352
|
-
| `
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
|
366
|
-
|
|
367
|
-
| `
|
|
368
|
-
| `
|
|
369
|
-
| `
|
|
370
|
-
| `
|
|
371
|
-
| `
|
|
372
|
-
| `
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
|
384
|
-
|
|
385
|
-
| `
|
|
386
|
-
| `
|
|
387
|
-
| `
|
|
388
|
-
| `
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
|
432
|
-
|
|
433
|
-
| `
|
|
434
|
-
| `
|
|
435
|
-
| `
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
|
459
|
-
|
|
460
|
-
| `
|
|
461
|
-
| `
|
|
462
|
-
| `
|
|
463
|
-
| `
|
|
464
|
-
| `
|
|
465
|
-
| `
|
|
466
|
-
| `
|
|
467
|
-
| `
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
|
485
|
-
|
|
486
|
-
| `
|
|
487
|
-
| `
|
|
488
|
-
| `
|
|
489
|
-
| `
|
|
490
|
-
| `
|
|
491
|
-
| `
|
|
492
|
-
| `
|
|
493
|
-
| `
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
```
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
<
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
```
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
<
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
|
567
|
-
|
|
568
|
-
| `
|
|
569
|
-
| `
|
|
570
|
-
| `
|
|
571
|
-
| `
|
|
572
|
-
| `
|
|
573
|
-
| `
|
|
574
|
-
| `
|
|
575
|
-
| `
|
|
576
|
-
| `
|
|
577
|
-
| `
|
|
578
|
-
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
|
600
|
-
|
|
601
|
-
| `
|
|
602
|
-
| `
|
|
603
|
-
| `
|
|
604
|
-
| `
|
|
605
|
-
| `
|
|
606
|
-
| `
|
|
607
|
-
| `
|
|
608
|
-
| `
|
|
609
|
-
| `
|
|
610
|
-
| `
|
|
611
|
-
| `
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
|
635
|
-
|
|
636
|
-
| `
|
|
637
|
-
| `
|
|
638
|
-
| `
|
|
639
|
-
| `
|
|
640
|
-
| `
|
|
641
|
-
| `
|
|
642
|
-
| `
|
|
643
|
-
| `
|
|
644
|
-
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
```
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
```
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
|
681
|
-
|
|
682
|
-
| `
|
|
683
|
-
| `
|
|
684
|
-
| `
|
|
685
|
-
| `
|
|
686
|
-
| `
|
|
687
|
-
| `
|
|
688
|
-
| `
|
|
689
|
-
| `
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
```
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
|
702
|
-
|
|
703
|
-
| `
|
|
704
|
-
| `
|
|
705
|
-
| `
|
|
706
|
-
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
<
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
```
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
<ContextMenu.
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
|
747
|
-
|
|
748
|
-
| `
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
<
|
|
771
|
-
<
|
|
772
|
-
<
|
|
773
|
-
<
|
|
774
|
-
<
|
|
775
|
-
<
|
|
776
|
-
<
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
<
|
|
788
|
-
<
|
|
789
|
-
<
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
<
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
|
801
|
-
|
|
802
|
-
| `
|
|
803
|
-
| `
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
|
812
|
-
|
|
813
|
-
| `
|
|
814
|
-
| `
|
|
815
|
-
| `
|
|
816
|
-
| `
|
|
817
|
-
| `
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
|
834
|
-
|
|
835
|
-
| `
|
|
836
|
-
| `badge` | `
|
|
837
|
-
| `
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
|
841
|
-
|
|
842
|
-
| `
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
`
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
//
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
const
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
|
988
|
-
|
|
989
|
-
| `
|
|
990
|
-
| `
|
|
991
|
-
| `
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
```
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
```
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
|
1131
|
-
|
|
1132
|
-
|
|
|
1133
|
-
|
|
|
1134
|
-
|
|
|
1135
|
-
|
|
|
1136
|
-
|
|
|
1137
|
-
|
|
|
1138
|
-
|
|
|
1139
|
-
|
|
|
1140
|
-
|
|
|
1141
|
-
|
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
|
1148
|
-
|
|
1
|
+
# @llmnative/react — Riferimento completo
|
|
2
|
+
|
|
3
|
+
Framework React AI-first per generazione deterministica di UI. Data-driven di default, schema-driven opzionale.
|
|
4
|
+
Versione 1.0.0 — Apache-2.0
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install @llmnative/react
|
|
8
|
+
import '@llmnative/react/dist/index.css'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Regole fondamentali
|
|
14
|
+
|
|
15
|
+
- Tutti gli import da `'@llmnative/react'` — mai da sottodirectory
|
|
16
|
+
- I provider sono iniettati via `<App providers={...}>` — mai importati direttamente nei componenti
|
|
17
|
+
- Usare `useDataProvider()`, `useAuthProvider()`, ecc. nei componenti, non istanze dirette
|
|
18
|
+
- TypeScript strict: no `any` — usare `unknown`, `Record<string, unknown>`, tipi specifici
|
|
19
|
+
- Naming props: `view` (presentazione contenuto), `appearance` (guscio visuale), `layout` (disposizione spaziale), `mode` (comportamento operativo), `variant` (tono semantico), `position` (ancoraggio), `size` (scala)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## App — Entry point
|
|
24
|
+
|
|
25
|
+
```tsx
|
|
26
|
+
<App
|
|
27
|
+
providers={{
|
|
28
|
+
firebase: { // FirebaseConfig — tutte le chiavi flat
|
|
29
|
+
apiKey, authDomain, databaseURL, projectId,
|
|
30
|
+
storageBucket, messagingSenderId, appId,
|
|
31
|
+
},
|
|
32
|
+
supabase: { url, anonKey },
|
|
33
|
+
google: { clientId, scope }, // flat — NON { oAuth2: { clientId } }
|
|
34
|
+
dropbox: { clientId }, // DropboxConfig
|
|
35
|
+
mock: { data: mockData },
|
|
36
|
+
ai: { // AIConfig — NON una prop separata di <App>
|
|
37
|
+
openaiApiKey, geminiApiKey, anthropicApiKey,
|
|
38
|
+
deepSeekApiKey, mistralApiKey, openRouterApiKey,
|
|
39
|
+
groqApiKey,
|
|
40
|
+
cloudflare: { apiToken, accountId }, // Cloudflare Workers AI
|
|
41
|
+
},
|
|
42
|
+
proxy: { enable: true, route: '/api/proxy' },
|
|
43
|
+
custom: { data: { myAdapter: new MyDataProvider() } },
|
|
44
|
+
services: {
|
|
45
|
+
data: 'dbRealtime', // DataDriverName
|
|
46
|
+
storage: 'firestorage', // StorageDriverName | null
|
|
47
|
+
auth: 'googleAuth', // AuthDriverName | null
|
|
48
|
+
email: 'gmail', // EmailDriverName | null
|
|
49
|
+
ai: 'openai', // AIDriverName | null
|
|
50
|
+
credentials: 'googleServiceAccount',
|
|
51
|
+
},
|
|
52
|
+
}}
|
|
53
|
+
iconProvider="lucide" // 'lucide' | 'phosphor' | AppIconProviderConfig
|
|
54
|
+
themeProvider="default" // 'default' | 'flat' | 'cyber' | AppThemeProviderConfig
|
|
55
|
+
menuConfig={menuConfig}
|
|
56
|
+
importPage={(path) => import(path)}
|
|
57
|
+
LayoutDefault={MyLayout} // layout wrapper custom (opzionale)
|
|
58
|
+
appName="MyApp" // nome app (opzionale)
|
|
59
|
+
i18n={{ locale: 'it', translations: { ... } }}
|
|
60
|
+
contextProviders={MyContextProvider}
|
|
61
|
+
errorReportUrl="/api/errors"
|
|
62
|
+
debug={import.meta.env.DEV}
|
|
63
|
+
/>
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### AppThemeProviderConfig
|
|
67
|
+
```ts
|
|
68
|
+
{
|
|
69
|
+
mode?: 'light' | 'dark' | 'system';
|
|
70
|
+
theme: string;
|
|
71
|
+
themes?: Record<string, {
|
|
72
|
+
preset?: { colors?: ColorScale; radius?: number; font?: string };
|
|
73
|
+
motion?: MotionRegistry;
|
|
74
|
+
components?: Record<string, Partial<ThemeComponent>>;
|
|
75
|
+
}>;
|
|
76
|
+
themeOverride?: Record<string, Partial<ThemeComponent>>;
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Built-in themes: `default`, `flat`, `cyber`.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Provider System
|
|
85
|
+
|
|
86
|
+
### Matrix completa
|
|
87
|
+
|
|
88
|
+
| Servizio | Interfaccia | Driver | Config key | Obbligatorio |
|
|
89
|
+
|---|---|---|---|---|
|
|
90
|
+
| data | `DataProviderAdapter` | `mock`, `dbRealtime`, `firestoreDb`, `supabaseDb` | `providers.mock/firebase/supabase` | sì (fallback: mock) |
|
|
91
|
+
| storage | `StorageProviderAdapter` | `firestorage`, `supabaseStorage` | `providers.firebase/supabase` | no |
|
|
92
|
+
| auth | `AuthProviderAdapter` | `googleAuth`, `firebaseAuth`, `supabaseAuth`, `dropboxAuth` | `providers.google/firebase/supabase/dropbox` | sì (fallback: googleAuth) |
|
|
93
|
+
| email | `EmailProviderAdapter` | `gmail` | `providers.google` | no |
|
|
94
|
+
| ai | `AIProviderAdapter` | `openai`, `gemini`, `anthropic`, `deepseek`, `mistral`, `openrouter`, `opencode`, `cloudflare`, `groq` | `Config.tsx AIConfig` | no |
|
|
95
|
+
| credentials | `CredentialsAdapter` | `googleServiceAccount` | `providers.google.serviceAccount` | no |
|
|
96
|
+
|
|
97
|
+
### Hooks
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
const data = useDataProvider() // DataProviderAdapter | null
|
|
101
|
+
const storage = useStorageProvider() // StorageProviderAdapter | null
|
|
102
|
+
const auth = useAuthProvider() // AuthProviderAdapter | null
|
|
103
|
+
const email = useEmailProvider() // EmailProviderAdapter | null
|
|
104
|
+
const ai = useAIProvider() // AIProviderAdapter | null
|
|
105
|
+
const creds = useCredentialsProvider() // CredentialsAdapter | null
|
|
106
|
+
|
|
107
|
+
// Named provider (multi-provider setup)
|
|
108
|
+
const supabase = useDataProvider('supabase')
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Interfacce principali
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
// DataProviderAdapter
|
|
115
|
+
read(path, options?) / set(path, data) / update(path, data) / remove(path)
|
|
116
|
+
subscribe(path, callback: (records: RecordArray) => void): () => void
|
|
117
|
+
count?(path, where?): Promise<number>
|
|
118
|
+
|
|
119
|
+
// StorageProviderAdapter
|
|
120
|
+
upload(file, path): Promise<string|undefined>
|
|
121
|
+
getURL(path) / download(path): Promise<Blob|undefined> / delete(path): Promise<boolean>
|
|
122
|
+
|
|
123
|
+
// AuthProviderAdapter
|
|
124
|
+
signIn(intent?, options?) / signOut() / getUser(): UserProfile|null
|
|
125
|
+
onAuthChange(callback) / getAccessToken(): Promise<string|null>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Auth sign-in methods
|
|
129
|
+
|
|
130
|
+
| method | Firebase | Supabase | Google |
|
|
131
|
+
|---|---|---|---|
|
|
132
|
+
| `password` | ✅ | ✅ | — |
|
|
133
|
+
| `anonymous` | ✅ | ✅ | — |
|
|
134
|
+
| `magic_link` | — | ✅ | — |
|
|
135
|
+
| `oauth` (github, apple…) | ✅ | ✅ | — |
|
|
136
|
+
| GIS + scoped token | — | — | ✅ |
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
await auth.signIn({ method: 'password', email: '...', password: '...' })
|
|
140
|
+
await auth.signIn({ method: 'oauth', provider: 'github' })
|
|
141
|
+
// Google: token per API scope
|
|
142
|
+
const token = await auth.getAccessToken('https://www.googleapis.com/auth/gmail.send')
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### CredentialsAdapter (Google Service Account)
|
|
146
|
+
```tsx
|
|
147
|
+
// Chiama Google APIs lato browser senza Node.js
|
|
148
|
+
<App providers={{ google: { serviceAccount: { clientEmail, privateKey } }, services: { credentials: 'googleServiceAccount' } }} />
|
|
149
|
+
|
|
150
|
+
const creds = useCredentialsProvider()
|
|
151
|
+
const token = await creds.getToken('https://www.googleapis.com/auth/solar')
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Provider custom
|
|
155
|
+
```ts
|
|
156
|
+
import { DataProviderAdapter, RecordArray } from '@llmnative/react'
|
|
157
|
+
|
|
158
|
+
export class RestDataProvider implements DataProviderAdapter {
|
|
159
|
+
async read(path: string): Promise<unknown> { ... }
|
|
160
|
+
async set(path: string, data: object): Promise<void> { ... }
|
|
161
|
+
async update(path: string, data: object): Promise<void> { ... }
|
|
162
|
+
async remove(path: string): Promise<void> { ... }
|
|
163
|
+
subscribe(path: string | undefined, callback: (records: RecordArray) => void): () => void {
|
|
164
|
+
return () => undefined
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Grid — CRUD table/gallery
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
<Grid
|
|
175
|
+
path="/users" // DataProvider-backed — oppure records={array} per in-memory
|
|
176
|
+
columns={[
|
|
177
|
+
{ key: 'name', label: 'Nome', sortable: true },
|
|
178
|
+
{ key: 'email', label: 'Email' },
|
|
179
|
+
{ key: 'role', label: 'Ruolo', render: ({ value }) => <Badge>{value}</Badge> },
|
|
180
|
+
{ key: 'date', label: 'Data', render: 'date' },
|
|
181
|
+
]}
|
|
182
|
+
actions={['add', 'edit', 'delete']} // oppure Record<string, GridAction>
|
|
183
|
+
view="table" // 'table' | 'gallery'
|
|
184
|
+
pagination={{ limit: 20 }}
|
|
185
|
+
groupBy="status"
|
|
186
|
+
sortable={{ field: 'name', dir: 'asc' }}
|
|
187
|
+
form={<FormFields />}
|
|
188
|
+
onSave={async ({ record, action }) => record._key}
|
|
189
|
+
onComplete={async ({ record, action }) => true}
|
|
190
|
+
selection={{ mode: 'multiple', onChange: (sel) => {} }}
|
|
191
|
+
reorderable
|
|
192
|
+
onReorder={(records, meta) => {}}
|
|
193
|
+
header={(ctx) => <div>{ctx.title}</div>}
|
|
194
|
+
footer={(ctx) => <div>{ctx.records.length} records</div>}
|
|
195
|
+
/>
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
| Prop | Tipo | Note |
|
|
199
|
+
|---|---|---|---|
|
|
200
|
+
| `path` | `string \| "fromUrl"` | Path provider-backed |
|
|
201
|
+
| `records` | `RecordArray` | Dati in memoria (alternativa a path) |
|
|
202
|
+
| `columns` | `GridColumn[]` | `{ key, label, sortable?, render?, className? }` |
|
|
203
|
+
| `actions` | `Array<'add'\|'edit'\|'delete'> \| Record<string, GridAction>` | |
|
|
204
|
+
| `view` | `'table' \| 'gallery'` | |
|
|
205
|
+
| `groupBy` | `string \| string[]` | |
|
|
206
|
+
| `pagination` | `PaginationParams` | `{ limit, page?, maxPageButtons?, align?, sticky? }` |
|
|
207
|
+
| `sortable` | `boolean \| OrderConfig` | |
|
|
208
|
+
| `selection` | `false \| 'single' \| 'multiple' \| GridSelectionConfig` | |
|
|
209
|
+
| `form` | `ReactElement \| ((ctx) => ReactNode)` | Form condiviso per CRUD |
|
|
210
|
+
| `onSave` | `GridMutationSaveHandler` | `async ({ record, action }) => string` |
|
|
211
|
+
| `onDelete` | `GridMutationDeleteHandler` | `async ({ record }) => string` |
|
|
212
|
+
| `onComplete` | `GridAfterActionHandler` | `async ({ record, action }) => boolean` |
|
|
213
|
+
| `onRowClick` | `(record) => void` | |
|
|
214
|
+
| `reorderable` | `boolean` | Abilita drag & drop righe |
|
|
215
|
+
| `onReorder` | `GridReorderHandler` | `(records, meta) => void` |
|
|
216
|
+
| `onLoad` | `(records) => TRecord[]` | Trasforma records dopo il fetch |
|
|
217
|
+
| `editDeepLink` | `boolean` | Sincronizza modal edit con URL |
|
|
218
|
+
| `audit` | `boolean` | Logga mutazioni |
|
|
219
|
+
| `sticky` | `'top' \| 'bottom'` | Header/footer sticky |
|
|
220
|
+
| `wrapperClassName` | `string` | CSS wrapper esterno |
|
|
221
|
+
| `loading` | `boolean` | Stato loading override |
|
|
222
|
+
| `title` | `ReactNode` | Titolo header grid |
|
|
223
|
+
| `before` / `after` | `ReactNode` | Slot prima/dopo il contenuto |
|
|
224
|
+
| `header` / `footer` | `ReactNode \| ((ctx) => ReactNode)` | Header/footer custom |
|
|
225
|
+
| `searchable` | `boolean \| GridSearchConfig` | Search box nell'header default — `{ fields?, placeholder? }` |
|
|
226
|
+
| `filters` | `GridFilterConfig[]` | Checkbox toggle nell'header default, applicati PRIMA di `searchable` — `{ key, label, defaultValue?, predicate(record, value) }` |
|
|
227
|
+
|
|
228
|
+
**GridAction kind:** `'modal'` (default), `'route'`, `'external'`, `'inline'`, `'delete'`
|
|
229
|
+
|
|
230
|
+
---
|
|
231
|
+
|
|
232
|
+
## Form — form CRUD con lifecycle
|
|
233
|
+
|
|
234
|
+
```tsx
|
|
235
|
+
<Form
|
|
236
|
+
path="/products"
|
|
237
|
+
appearance="card" // 'card' | 'empty'
|
|
238
|
+
showBack
|
|
239
|
+
defaultValues={{ status: 'draft' }}
|
|
240
|
+
keyGenerator={() => `prod_${Date.now()}`}
|
|
241
|
+
onLoad={(data) => { /* side-effect hook, non trasforma */ }}
|
|
242
|
+
onSave={async ({ record, action }) => { /* return storagePath or undefined */ }}
|
|
243
|
+
onDelete={async ({ record }) => { /* confirm */ }}
|
|
244
|
+
onComplete={async ({ action }) => { if (action === 'save') navigate('/products') }}
|
|
245
|
+
>
|
|
246
|
+
<Input name="title" label="Titolo" required />
|
|
247
|
+
<Input name="price" label="Prezzo" type="number" />
|
|
248
|
+
<Input name="address.city" label="Città" /> {/* oggetto annidato */}
|
|
249
|
+
<Repeat name="tags" defaultLength={1}>
|
|
250
|
+
{(i) => <Input name={`tags.${i}`} label={`Tag ${i+1}`} />}
|
|
251
|
+
</Repeat>
|
|
252
|
+
</Form>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
| Prop | Tipo | Note |
|
|
256
|
+
|---|---|---|---|
|
|
257
|
+
| `path` | `string` | Path provider-backed |
|
|
258
|
+
| `appearance` | `'card' \| 'empty'` | Guscio visuale |
|
|
259
|
+
| `controller` | `FormController` | Controller condiviso per bottoni custom esterni |
|
|
260
|
+
| `showBack` | `boolean` | Pulsante Back nel footer |
|
|
261
|
+
| `defaultValues` | `RecordProps` | Valori iniziali per nuovo record |
|
|
262
|
+
| `keyGenerator` | `(record) => string` | Generatore chiave primaria custom |
|
|
263
|
+
| `draftBucket` | `string` | Bucket stabile che isola la bozza locale; omettilo per disabilitare la persistenza |
|
|
264
|
+
| `noticeAnchorRef` | `React.RefObject<HTMLElement>` | Ancoraggio sticky per notifiche |
|
|
265
|
+
| `showNotice` | `boolean` | Mostra notifiche save/delete (default `true`) |
|
|
266
|
+
| `children` | `ReactNode \| ((fields) => ReactNode) \| (({ record }) => ReactNode)` | Contenuto form |
|
|
267
|
+
| `header` | `ReactNode` | Contenuto header custom |
|
|
268
|
+
| `footer` | `ReactNode` | Contenuto footer custom |
|
|
269
|
+
| `wrapperClassName` | `string` | CSS wrapper esterno |
|
|
270
|
+
| `headerClassName` | `string` | CSS header |
|
|
271
|
+
| `className` | `string` | CSS body form |
|
|
272
|
+
| `footerClassName` | `string` | CSS footer |
|
|
273
|
+
| `log` | `boolean` | Logga ogni modifica su console |
|
|
274
|
+
| `onLoad` | `(record) => void` | Side-effect dopo il caricamento (non trasforma) |
|
|
275
|
+
| `onRecordChange` | `(record) => void` | Cambio campo in tempo reale |
|
|
276
|
+
| `onSave` | `FormSaveHandler` | `async ({ record, prevRecord?, storagePath?, action }) => string \| undefined` |
|
|
277
|
+
| `onDelete` | `FormDeleteHandler` | `async ({ record }) => string \| undefined` |
|
|
278
|
+
| `onComplete` | `FormFinallyHandler` | `async ({ record, action }) => boolean` |
|
|
279
|
+
|
|
280
|
+
**Form controller** — stato e azioni native del form accessibili da qualsiasi punto della pagina.
|
|
281
|
+
|
|
282
|
+
Due hook:
|
|
283
|
+
|
|
284
|
+
| Hook | Dove usarlo | Cosa restituisce |
|
|
285
|
+
|---|---|---|
|
|
286
|
+
| `useFormController(controller?)` | Ovunque (anche fuori dal form) | Crea o si aggancia a un `FormController`. Senza argomenti ne crea uno locale stabile. Con un `controller` esistente ci si aggancia. |
|
|
287
|
+
| `useFormActions()` | Solo dentro `<Form>` (legge dal context) | Restituisce lo stesso `FormController` del form genitore. Lancia errore se usato fuori. |
|
|
288
|
+
|
|
289
|
+
Esempio — bottone custom fuori dal form:
|
|
290
|
+
```tsx
|
|
291
|
+
const form = useFormController();
|
|
292
|
+
|
|
293
|
+
<ActionButton disabled={form.saveDisabled} onClick={() => { void form.save(); }} />
|
|
294
|
+
<Form controller={form} draftBucket="workspace/acme" onSave={...} />
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Esempio — uso dentro il form (senza passare controller):
|
|
298
|
+
```tsx
|
|
299
|
+
function MySaveButton() {
|
|
300
|
+
const form = useFormActions();
|
|
301
|
+
return <LoadingButton disabled={form.saveDisabled} loading={form.isSaving} onClick={form.save} />;
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
Stato esposto dal controller: `record`, `isNewRecord`, `isDirty`, `canSave`, `canDelete`, `isSaving`, `isDeleting`, `saveDisabled`, `deleteDisabled`, `hasDraft`, `draftStatus`, `errors`, `hasErrors`.
|
|
306
|
+
|
|
307
|
+
Azioni: `save()`, `delete()`, `reset()`, `restoreDraft()`, `discardDraft()`.
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Campi form
|
|
312
|
+
|
|
313
|
+
### Input
|
|
314
|
+
```tsx
|
|
315
|
+
<Input name="x" label="..." required /> // text (default)
|
|
316
|
+
<Input name="price" type="number" min={0} max={100} step={0.01} />
|
|
317
|
+
<Input name="email" type="email" />
|
|
318
|
+
<Input name="pwd" type="password" />
|
|
319
|
+
<Input name="date" type="date" />
|
|
320
|
+
<Input name="color" type="color" />
|
|
321
|
+
<TextArea name="bio" minHeight={96} />
|
|
322
|
+
<Checkbox name="active" valueChecked="yes" />
|
|
323
|
+
```
|
|
324
|
+
`Input` props: `name`(req) `label` `type` `required` `disabled` `placeholder` `min` `max` `step` `validator:(v)=>string|undefined`
|
|
325
|
+
`type`: `'text' | 'number' | 'email' | 'password' | 'color' | 'date' | 'time' | 'datetime-local' | 'week' | 'month' | 'range' | 'checkbox' | 'radio' | 'url'`
|
|
326
|
+
|
|
327
|
+
**NOTA**: il tipo HTML si passa con `type`. `inputType` e i sotto-componenti `Input.Number`/`Input.Email`/`Input.Date` non esistono.
|
|
328
|
+
|
|
329
|
+
### Select / Autocomplete / Checklist
|
|
330
|
+
```tsx
|
|
331
|
+
<Select name="role" options={[{ label: 'Admin', value: 'admin' }]} />
|
|
332
|
+
<Select name="categoryId" optionsSource={{ path: '/categories', labelField: 'name', valueField: '_key' }} />
|
|
333
|
+
<Autocomplete name="userId" optionsSource={{ path: '/users', labelField: 'email' }} maxItems={1} />
|
|
334
|
+
<Checklist name="tags" options={['a', 'b', 'c']} />
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
| Prop | Tipo | Note |
|
|
338
|
+
|---|---|---|
|
|
339
|
+
| `options` | `Option[] \| string[] \| number[]` | |
|
|
340
|
+
| `optionsSource` | `{ path, labelField, valueField?, where?, order?, fieldMap?, onLoad? }` | Carica da provider |
|
|
341
|
+
| `order` | `{ field: 'label' \| 'value', dir: 'asc' \| 'desc' }` | |
|
|
342
|
+
| `placeholderOption` | `Option` | Solo Select |
|
|
343
|
+
| `placeholder` | `string` | Solo Autocomplete |
|
|
344
|
+
| `creatable` | `boolean` | Solo Autocomplete — crea nuovi valori |
|
|
345
|
+
| `onCreate` | `(value) => void \| boolean \| Promise<void \| boolean>` | Solo Autocomplete — restituire `false` per non selezionare il valore creato (es. aprire un modulo di creazione) |
|
|
346
|
+
| `listClassName` / `optionClassName` | `string` | Solo Autocomplete — classi della lista e delle voci (anche da tema `Autocomplete.listClassName` / `optionClassName`) |
|
|
347
|
+
| `minItems` / `maxItems` | `number` | Solo Autocomplete |
|
|
348
|
+
| `itemClassName` | `string` | Solo Checklist |
|
|
349
|
+
| `validator` | `(value) => string \| undefined` | Tutte le varianti |
|
|
350
|
+
| `readOnlyAfterSet` | `boolean` | readonly dopo primo valore |
|
|
351
|
+
| `disabled` | `boolean` | |
|
|
352
|
+
| `title` | `string` | |
|
|
353
|
+
| `feedback` | `string` | Messaggio di errore/suggerimento |
|
|
354
|
+
|
|
355
|
+
`Autocomplete` è una combobox accessibile: la lista (in un portal, larga quanto il campo, stile popover del tema) mostra solo le label e filtra per label/value senza distinzione di maiuscole e accenti; i chip mostrano la label; tastiera: frecce, Invio, Esc (chiude solo la lista, non il `Modal`), Tab, Backspace su campo vuoto rimuove l'ultimo valore. Con `creatable` compare la voce "Crea «testo»". Il valore salvato è sempre l'array dei `value`. `Autocomplete` e `Checklist` sono export nominati: `Select.Autocomplete`/`Select.Checklist` non esistono.
|
|
356
|
+
|
|
357
|
+
### UploadImage / UploadDocument / UploadCSV
|
|
358
|
+
```tsx
|
|
359
|
+
<UploadImage name="cover" label="Copertina" /> {/* con crop editor */}
|
|
360
|
+
<UploadDocument name="pdf" label="PDF" />
|
|
361
|
+
<UploadCSV name="data" label="Importa CSV" /> {/* con preview tabellare */}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
**UploadImageProps** (eredita FormFieldProps):
|
|
365
|
+
| Prop | Tipo | Note |
|
|
366
|
+
|---|---|---|
|
|
367
|
+
| `name` | `string` (required) | |
|
|
368
|
+
| `uploadPath` | `string` | Path storage per upload automatico |
|
|
369
|
+
| `accept` | `string` | MIME types accettati |
|
|
370
|
+
| `editable` | `boolean` | Abilita crop editor |
|
|
371
|
+
| `multiple` | `boolean` | Multipla selezione |
|
|
372
|
+
| `max` | `number` | Numero massimo file |
|
|
373
|
+
| `feedback` | `string` | Testo placeholder drop zone |
|
|
374
|
+
| `srcsetWidths` | `number[]` | Larghezze varianti responsive |
|
|
375
|
+
| `previewHeight` / `previewWidth` | `number` | Dimensioni preview |
|
|
376
|
+
|
|
377
|
+
**UploadDocumentProps** (eredita FormFieldProps): `name`, `uploadPath`, `accept`, `editable`, `multiple`, `max`
|
|
378
|
+
|
|
379
|
+
### UploadCSV
|
|
380
|
+
```tsx
|
|
381
|
+
<UploadCSV name="data" label="Importa CSV" />
|
|
382
|
+
```
|
|
383
|
+
| Prop | Tipo |
|
|
384
|
+
|---|---|
|
|
385
|
+
| `name` | `string` (required) |
|
|
386
|
+
| `onDataLoaded` | `(data: UploadCSVData) => void` |
|
|
387
|
+
| `onParseField` | `([key, value]) => [key, value] \| undefined` |
|
|
388
|
+
| `normalizeKeys` | `boolean` |
|
|
389
|
+
| `removeEmptyFields` | `boolean` |
|
|
390
|
+
| `delimiter` | `string` |
|
|
391
|
+
| `label` / `icon` | Opzionali |
|
|
392
|
+
|
|
393
|
+
### ImageField
|
|
394
|
+
```tsx
|
|
395
|
+
<ImageField
|
|
396
|
+
name="hero"
|
|
397
|
+
label="Hero image"
|
|
398
|
+
defaultValue={{ src: 'https://example.com/hero.jpg', alt: 'Hero shot' }}
|
|
399
|
+
srcsetWidths={[400, 800]}
|
|
400
|
+
uploadPath="/content/images"
|
|
401
|
+
/>
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Gestisce un oggetto `{ src, alt, srcset?, sizes? }`, preview inline, crop editor e generazione opzionale di varianti responsive.
|
|
405
|
+
|
|
406
|
+
### CropImage
|
|
407
|
+
```tsx
|
|
408
|
+
<CropImage img={imageObj} title="Ritaglia" scales={{ '1:1': 1, '4:3': 1.33 }} />
|
|
409
|
+
```
|
|
410
|
+
Componente di ritaglio indipendente. `img` ha tipo `{ fileName, variants }` dove ogni variante è `{ fileName, type, scale, top, left, width, height, base64? }`. Usato internamente da `UploadImage` e `ImageField`, ma esportato pubblicamente per uso standalone.
|
|
411
|
+
|
|
412
|
+
### CodeEditor
|
|
413
|
+
```tsx
|
|
414
|
+
<CodeEditor
|
|
415
|
+
name="code"
|
|
416
|
+
label="Template"
|
|
417
|
+
language="html"
|
|
418
|
+
placeholder="Scrivi HTML..."
|
|
419
|
+
minHeight={200}
|
|
420
|
+
maxHeight={600}
|
|
421
|
+
disabled={false}
|
|
422
|
+
required
|
|
423
|
+
validateSyntax
|
|
424
|
+
commands={[{ name: 'wrap-section', handler: ({ value }) => `<section>\n${value}\n</section>` }]}
|
|
425
|
+
commandsTrigger="/"
|
|
426
|
+
/>
|
|
427
|
+
```
|
|
428
|
+
Editor basato su CodeMirror con syntax highlighting e validazione. Linguaggi: `liquid`, `html`, `json`, `js`, `ts`, `css`.
|
|
429
|
+
Quando `commands` e presente, `CodeEditor` apre internamente `ContextMenu` sul trigger configurato.
|
|
430
|
+
|
|
431
|
+
| Prop extra | Tipo |
|
|
432
|
+
|---|---|
|
|
433
|
+
| `language` | `CodeEditorLanguage` |
|
|
434
|
+
| `placeholder` | `string` |
|
|
435
|
+
| `minHeight` / `maxHeight` | `number` |
|
|
436
|
+
| `validateSyntax` | `boolean` |
|
|
437
|
+
| `validator` | `(value) => string \| undefined \| Promise<string \| undefined>` |
|
|
438
|
+
| `extensions` | `unknown[]` |
|
|
439
|
+
|
|
440
|
+
### RichText
|
|
441
|
+
```tsx
|
|
442
|
+
<RichText
|
|
443
|
+
name="body"
|
|
444
|
+
label="Article body"
|
|
445
|
+
placeholder="Scrivi..."
|
|
446
|
+
toolbar="fixed" // 'fixed' | 'floating' | false
|
|
447
|
+
toolbarCommands={['bold', 'italic', 'heading1', '|', 'bulletList', 'link']}
|
|
448
|
+
outputFormat="html" // 'html' | 'json' | 'text'
|
|
449
|
+
statusBar={{ tagBreadcrumb: true, wordCount: true }}
|
|
450
|
+
minHeight={200}
|
|
451
|
+
maxHeight={800}
|
|
452
|
+
imageUpload={{ path: '/content/images', srcsetWidths: [400, 800] }}
|
|
453
|
+
commands={[{ name: 'summary', icon: 'sparkles' }]}
|
|
454
|
+
commandsTrigger="/"
|
|
455
|
+
/>
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
| Prop extra | Tipo |
|
|
459
|
+
|---|---|
|
|
460
|
+
| `placeholder` | `string` |
|
|
461
|
+
| `disabled` | `boolean` |
|
|
462
|
+
| `feedback` | `string` |
|
|
463
|
+
| `toolbar` | `'fixed' \| 'floating' \| false` |
|
|
464
|
+
| `toolbarCommands` | `ToolbarCommand[]` |
|
|
465
|
+
| `outputFormat` | `'html' \| 'json' \| 'text'` |
|
|
466
|
+
| `statusBar` | `boolean \| StatusBarConfig` |
|
|
467
|
+
| `minHeight` / `maxHeight` | `number` |
|
|
468
|
+
| `imageUpload` | `RichTextImageUploadConfig` |
|
|
469
|
+
| `documentUpload` | `RichTextDocumentUploadConfig` |
|
|
470
|
+
| `validator` | `(value) => string \| undefined` |
|
|
471
|
+
|
|
472
|
+
### Repeat — array dinamici
|
|
473
|
+
```tsx
|
|
474
|
+
<Repeat name="items" defaultLength={3} minItems={1} maxItems={10} label="Elementi" layout="vertical" readOnly>
|
|
475
|
+
{({ index, record, records, remove }) => (
|
|
476
|
+
<>
|
|
477
|
+
<Input name={`items.${index}.name`} label="Nome" />
|
|
478
|
+
<UploadImage name={`items.${index}.photo`} label="Foto" />
|
|
479
|
+
</>
|
|
480
|
+
)}
|
|
481
|
+
</Repeat>
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
| Prop | Tipo | Note |
|
|
485
|
+
|---|---|---|
|
|
486
|
+
| `name` | `string` (required) | |
|
|
487
|
+
| `children` | `ReactNode \| ((args) => ReactNode)` | Render prop con `{ index, record, records, remove }` |
|
|
488
|
+
| `defaultLength` | `number` | Righe iniziali |
|
|
489
|
+
| `minItems` / `maxItems` | `number` | Min/max righe |
|
|
490
|
+
| `layout` | `'vertical' \| 'horizontal' \| 'inline'` | |
|
|
491
|
+
| `label` | `string` | Etichetta gruppo |
|
|
492
|
+
| `labelPosition` | `'top' \| 'bottom'` | |
|
|
493
|
+
| `hideRemoveButton` | `boolean` | Nascondi rimozione |
|
|
494
|
+
| `readOnly` | `boolean` | Sola lettura |
|
|
495
|
+
| `onChange` | `FieldOnChange` | |
|
|
496
|
+
| `onAdd` / `onRemove` | `(records) => void` | Callback aggiunta/rimozione |
|
|
497
|
+
|
|
498
|
+
### Prompt — campo AI-potenziato
|
|
499
|
+
```tsx
|
|
500
|
+
<Prompt
|
|
501
|
+
name="summary"
|
|
502
|
+
mode="run" // 'edit' | 'run'
|
|
503
|
+
onRunPrompt={async (prompt, options) => AI.fetch(prompt)}
|
|
504
|
+
commands={[{ name: 'translate', icon: 'languages', handler: (v) => `Translate:\n${v}` }]}
|
|
505
|
+
commandsTrigger="/" // default automatico quando commands e presente
|
|
506
|
+
attachments
|
|
507
|
+
statusItems={['tokensIn', 'tokensOut', 'contextPercent', 'duration']}
|
|
508
|
+
/>
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
Quando `commands` e presente in `mode="run"`, `Prompt` monta internamente `ContextMenu` sulla textarea del risultato. Il trigger di default e `"/"`; `commandsTrigger` permette di cambiarlo. Il menu diventa searchable automaticamente sopra una soglia interna di comandi.
|
|
512
|
+
|
|
513
|
+
`renderFallback`: quando l'AI non e configurata, `Prompt` monta un fallback testuale; passa `renderFallback` per personalizzarlo (riceve `{ name, label, required, onChange }`).
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
## UI Primitives
|
|
518
|
+
|
|
519
|
+
### Alert
|
|
520
|
+
```tsx
|
|
521
|
+
<Alert variant="success">Salvato!</Alert>
|
|
522
|
+
<Alert variant="warning" icon placement="fixed" timeout={3000}>Attenzione</Alert>
|
|
523
|
+
```
|
|
524
|
+
`variant`: `'info' | 'success' | 'warning' | 'danger' | 'primary' | 'secondary' | 'light' | 'dark'`
|
|
525
|
+
`appearance`: `'default' | 'text'`
|
|
526
|
+
`placement`: `'inline' | 'fixed'`
|
|
527
|
+
|
|
528
|
+
### Badge
|
|
529
|
+
```tsx
|
|
530
|
+
<Badge variant="success">Attivo</Badge>
|
|
531
|
+
```
|
|
532
|
+
`variant`: stessi valori di Alert
|
|
533
|
+
|
|
534
|
+
### Button variants
|
|
535
|
+
```tsx
|
|
536
|
+
<ActionButton variant="primary" label="Salva" icon="save" onClick={handleSave} />
|
|
537
|
+
<LoadingButton variant="primary" label="Invia" onClick={async (e, setMsg) => { await save(); setMsg('Salvato!') }} />
|
|
538
|
+
<BackLink label="Indietro" />
|
|
539
|
+
<GoSite href="https://..." label="Visita" />
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
**IButton props:** `variant`, `label`, `icon`, `badge`, `disabled`, `ariaLabel`, `title`, `iconClassName`, `onClick`
|
|
543
|
+
**LoadingButton aggiunge:** `loading`, `loadingLabel` — `onClick` riceve `(e, setMessage?) => Promise<unknown>`
|
|
544
|
+
|
|
545
|
+
### Card
|
|
546
|
+
```tsx
|
|
547
|
+
<Card title="Titolo" header={<Actions />} footer={<Pagination />} loading={isLoading}>
|
|
548
|
+
{children}
|
|
549
|
+
</Card>
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### Modal
|
|
553
|
+
```tsx
|
|
554
|
+
<Modal title="Conferma" size="md" position="center" onClose={() => {}} onSave={handleSave} onDelete={handleDelete} motion="fade" allowFullscreen stackedBehind>
|
|
555
|
+
{content}
|
|
556
|
+
</Modal>
|
|
557
|
+
<ModalYesNo title="Sicuro?" onClose={() => {}} onYes={async () => true}>Eliminare il record?</ModalYesNo>
|
|
558
|
+
<ModalOk title="Completato!" onClose={() => {}}>Salvato.</ModalOk>
|
|
559
|
+
```
|
|
560
|
+
`size`: `'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'fullscreen'`
|
|
561
|
+
`position`: `'center' | 'top' | 'left' | 'right' | 'bottom'`
|
|
562
|
+
`ModalYesNo` e `ModalOk` sono export separati: `Modal.YesNo` e `Modal.Ok` non esistono.
|
|
563
|
+
Tastiera: Esc chiude il modal in cima alla pila (`closeOnEscape`, default `true`).
|
|
564
|
+
Accessibilità: il dialog ha `role="dialog"`, `aria-modal="true"` (non con `stackedBehind`) e `aria-labelledby` verso `title` (o `header` stringa) — nei test si trova con `getByRole('dialog', { name })`.
|
|
565
|
+
|
|
566
|
+
| Prop | Tipo | Note |
|
|
567
|
+
|---|---|---|
|
|
568
|
+
| `children` | `ReactNode` (required) | |
|
|
569
|
+
| `title` | `ReactNode` | |
|
|
570
|
+
| `header` / `footer` | `ReactNode \| false` | Custom header/footer |
|
|
571
|
+
| `size` / `position` | Come sopra | |
|
|
572
|
+
| `onClose` | `() => void` | |
|
|
573
|
+
| `onSave` / `onDelete` | `(e) => Promise<boolean>` | |
|
|
574
|
+
| `allowFullscreen` | `boolean` | Toggle fullscreen |
|
|
575
|
+
| `showCancel` | `boolean` | Default `true` |
|
|
576
|
+
| `stackedBehind` | `boolean` | Modal seconda in pila |
|
|
577
|
+
| `closeOnBackdrop` | `boolean` | Default `true` |
|
|
578
|
+
| `closeOnEscape` | `boolean` | Default `true` — solo il modal in cima reagisce |
|
|
579
|
+
| `zIndex` | `number` | |
|
|
580
|
+
| `motion` | `MotionReference` | Preset animazione |
|
|
581
|
+
| Classi CSS header/title/subtitle/body/footer | `string` | |
|
|
582
|
+
|
|
583
|
+
### Table
|
|
584
|
+
```tsx
|
|
585
|
+
<Table
|
|
586
|
+
columns={[{ key: 'name', label: 'Nome', sort: true }]}
|
|
587
|
+
records={records}
|
|
588
|
+
onRowClick={(record) => {}}
|
|
589
|
+
selection="multiple"
|
|
590
|
+
sortable={{ field: 'name', dir: 'asc' }}
|
|
591
|
+
pagination={{ limit: 20 }}
|
|
592
|
+
onReorder={(records, meta) => {}}
|
|
593
|
+
onSelectionChange={(sel) => {}}
|
|
594
|
+
groupBy="status"
|
|
595
|
+
renderCell={(record, key, index) => <CustomCell />}
|
|
596
|
+
/>
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
| Prop | Tipo |
|
|
600
|
+
|---|---|
|
|
601
|
+
| `columns` | `TableHeaderProp[]` |
|
|
602
|
+
| `records` | `RecordArray` |
|
|
603
|
+
| `sortable` | `boolean \| OrderConfig` |
|
|
604
|
+
| `pagination` | `PaginationParams` |
|
|
605
|
+
| `selection` | `'single' \| 'multiple'` |
|
|
606
|
+
| `onRowClick` | `(record) => void` |
|
|
607
|
+
| `onReorder` | `(records, meta) => void` |
|
|
608
|
+
| `onSelectionChange` | `(selection) => void` |
|
|
609
|
+
| `renderCell` | `(record, key, index) => ReactNode` |
|
|
610
|
+
| `groupBy` | `string \| string[]` |
|
|
611
|
+
| `activeKey` | `string \| null` |
|
|
612
|
+
| `selectedKeys` | `string[]` |
|
|
613
|
+
| `header` / `footer` | `ReactNode` |
|
|
614
|
+
| `headerClassName` / `bodyClassName` / `footerClassName` / `scrollClassName` / `selectedClassName` / `heightClassName` | `string` |
|
|
615
|
+
|
|
616
|
+
### Gallery
|
|
617
|
+
```tsx
|
|
618
|
+
<Gallery
|
|
619
|
+
records={records}
|
|
620
|
+
overlays={[{ position: 'topRight', badge: { variant: 'success', children: 'New' } }]}
|
|
621
|
+
columns={3}
|
|
622
|
+
gap={2}
|
|
623
|
+
sortable={{ field: 'name', dir: 'asc' }}
|
|
624
|
+
pagination={{ limit: 20 }}
|
|
625
|
+
onRowClick={(record) => {}}
|
|
626
|
+
onSelectionChange={(sel) => {}}
|
|
627
|
+
header="Titolo"
|
|
628
|
+
groupBy="status"
|
|
629
|
+
/>
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
`position`: `'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' | 'middleLeft' | 'middleRight'`
|
|
633
|
+
|
|
634
|
+
| Prop | Tipo |
|
|
635
|
+
|---|---|
|
|
636
|
+
| `records` | `GalleryRecord[]` (`{ img?, thumbnail?, name?, width?, height? }`) |
|
|
637
|
+
| `columns` | `1 \| 2 \| 3 \| 4 \| 6` |
|
|
638
|
+
| `gap` | `0 \| 1 \| 2 \| 3 \| 4 \| 5` |
|
|
639
|
+
| `overlays` | `GalleryOverlay[]` |
|
|
640
|
+
| `sortable` | `boolean \| OrderConfig` |
|
|
641
|
+
| `pagination` | `PaginationParams` |
|
|
642
|
+
| `onRowClick` | `(record) => void` |
|
|
643
|
+
| `onSelectionChange` | `(selection) => void` |
|
|
644
|
+
| `groupBy` | `string \| string[]` |
|
|
645
|
+
| `selectedKeys` | `string[]` |
|
|
646
|
+
| `header` / `footer` | `ReactNode` |
|
|
647
|
+
| Classi CSS: `headerClassName`, `bodyClassName`, `footerClassName`, `scrollClassName`, `selectedClassName` | `string` |
|
|
648
|
+
|
|
649
|
+
### Tab
|
|
650
|
+
```tsx
|
|
651
|
+
import { Tab, TabItem } from '@llmnative/react'
|
|
652
|
+
|
|
653
|
+
<Tab defaultIndex={0} layout="top">
|
|
654
|
+
<TabItem label="Generale">{content1}</TabItem>
|
|
655
|
+
<TabItem label="SEO">{content2}</TabItem>
|
|
656
|
+
</Tab>
|
|
657
|
+
```
|
|
658
|
+
`layout`: `'default' | 'top' | 'left' | 'right' | 'bottom'`
|
|
659
|
+
Le schede sono `<TabItem>`, export separato: `Tab.Item` non esiste.
|
|
660
|
+
|
|
661
|
+
### Image
|
|
662
|
+
```tsx
|
|
663
|
+
<Image src="..." fit="cover" position="center" width={400} height={300} />
|
|
664
|
+
```
|
|
665
|
+
`fit`: `'cover' | 'contain' | 'fill' | 'scale-down' | 'none'`
|
|
666
|
+
|
|
667
|
+
### Pagination (standalone)
|
|
668
|
+
```tsx
|
|
669
|
+
<Pagination records={items} limit={20}>
|
|
670
|
+
{(pageRecords, offset) => pageRecords.map(r => <Row key={r._key} record={r} />)}
|
|
671
|
+
</Pagination>
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
### Percentage
|
|
675
|
+
```tsx
|
|
676
|
+
<Percentage value={75} appearance="bar" variant="success" showText />
|
|
677
|
+
<Percentage value={0.75} appearance="circle" size={60} />
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
| Prop | Tipo | Default |
|
|
681
|
+
|---|---|---|
|
|
682
|
+
| `value` | `number` | — |
|
|
683
|
+
| `max` | `number` | `100` |
|
|
684
|
+
| `min` | `number` | — |
|
|
685
|
+
| `appearance` | `'bar' \| 'circle'` | `'bar'` |
|
|
686
|
+
| `variant` | `ColorType` | — |
|
|
687
|
+
| `trackVariant` | `ColorType` | — |
|
|
688
|
+
| `thickness` | `number` | — (px) |
|
|
689
|
+
| `showText` | `boolean` | — |
|
|
690
|
+
| `size` | `number` | Diametro in modalità `'circle'` (px) |
|
|
691
|
+
| `fontSize` | `number` | — |
|
|
692
|
+
| `label` | `string` | — |
|
|
693
|
+
|
|
694
|
+
### DescriptionList
|
|
695
|
+
Coppie etichetta → valore in sola lettura (profilo, dati di un record, riepiloghi), rese come `<dl>`/`<dt>`/`<dd>`. Da usare al posto di markup `<dl>` scritto a mano; per dati modificabili usare i campi di `<Form>`, per molti record `<Table>`/`<Grid>`.
|
|
696
|
+
```tsx
|
|
697
|
+
<DescriptionList items={[{ label: 'Email', value: 'a@b.it' }, { label: 'Telefono', value: null }]} />
|
|
698
|
+
<DescriptionList layout="stacked" columns={4} truncate items={[{ label: 'Stato', value: <Badge variant="success">Spedito</Badge> }]} />
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
| Prop | Tipo | Default |
|
|
702
|
+
|---|---|---|
|
|
703
|
+
| `items` | `DescriptionListItem[]` — `{ key?, label, value?, title? }` | — |
|
|
704
|
+
| `layout` | `'horizontal' \| 'stacked'` | `'horizontal'` (etichette a sinistra, impilate su mobile) |
|
|
705
|
+
| `columns` | `1 \| 2 \| 3 \| 4 \| 5 \| 6` | `1` (solo `stacked`, dal breakpoint `lg`) |
|
|
706
|
+
| `labelWidth` | `string` (lunghezza CSS) | `'12rem'` (solo `horizontal`) |
|
|
707
|
+
| `emptyValue` | `ReactNode` | `'—'` per `null` / `undefined` / `''` (lo `0` resta) |
|
|
708
|
+
| `truncate` | `boolean` | `false`; un valore testuale diventa il suo `title` |
|
|
709
|
+
| Classi | `wrapperClassName`, `className`, `itemClassName`, `labelClassName`, `valueClassName` | tema `DescriptionList` |
|
|
710
|
+
|
|
711
|
+
### Dropdown
|
|
712
|
+
```tsx
|
|
713
|
+
<Dropdown trigger={<Icon name="menu" />} position="end">
|
|
714
|
+
<DropdownItem label="Modifica" icon="edit" onClick={() => {}} />
|
|
715
|
+
<DropdownItem label="Elimina" icon="trash" variant="danger" />
|
|
716
|
+
<DropdownDivider />
|
|
717
|
+
<DropdownHeader label="Azioni rapide" />
|
|
718
|
+
</Dropdown>
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
Per elenchi caricati dinamicamente usare `AsyncDropdown`: gestisce ricerca con debounce,
|
|
722
|
+
cancellazione delle richieste precedenti tramite `AbortSignal` e stati loading/empty/error.
|
|
723
|
+
|
|
724
|
+
```tsx
|
|
725
|
+
<AsyncDropdown
|
|
726
|
+
trigger={<ActionButton label="Select user" />}
|
|
727
|
+
loadItems={(query, signal) => users.search(query, { signal })}
|
|
728
|
+
getItemId={user => user.id}
|
|
729
|
+
renderItem={user => user.name}
|
|
730
|
+
onSelect={user => setUser(user)}
|
|
731
|
+
selectedId={user?.id}
|
|
732
|
+
/>
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
### ContextMenu
|
|
736
|
+
```tsx
|
|
737
|
+
<ContextMenu trigger="/" searchable>
|
|
738
|
+
<ContextMenu.Heading>Comandi</ContextMenu.Heading>
|
|
739
|
+
<ContextMenu.Item label="Titolo" value="# " icon="heading" />
|
|
740
|
+
<ContextMenu.Separator />
|
|
741
|
+
<ContextMenu.Item label="Bold" value="**bold**" icon="bold" />
|
|
742
|
+
</ContextMenu>
|
|
743
|
+
```
|
|
744
|
+
Funziona su qualsiasi `<textarea>` / `<input>` figlio. Trigger custom (`/`, `@`, `{`), filtro searchable, navigazione tastiera, floating positioning.
|
|
745
|
+
|
|
746
|
+
| Prop | Tipo | Note |
|
|
747
|
+
|---|---|---|
|
|
748
|
+
| `trigger` | `string` (required) | Es. `"/"`, `"@"`, `"{"` |
|
|
749
|
+
| `searchable` | `boolean` | Filtro testuale |
|
|
750
|
+
| `controlled` | `ContextMenuControlledState` | Stato controllato esterno |
|
|
751
|
+
| `onSelect` | `(item, context) => void` | |
|
|
752
|
+
|
|
753
|
+
**EditorCommand** (usato da `commands` prop su `RichText`, `CodeEditor`, `Prompt`):
|
|
754
|
+
```ts
|
|
755
|
+
{
|
|
756
|
+
name: string;
|
|
757
|
+
description?: string;
|
|
758
|
+
icon?: string;
|
|
759
|
+
group?: string;
|
|
760
|
+
consumeSuffix?: string;
|
|
761
|
+
handler?: (context: TextCommandContext) => string | Promise<string>;
|
|
762
|
+
}
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
`consumeSuffix` elimina una chiusura automatica adiacente durante la sostituzione del trigger,
|
|
766
|
+
utile quando un comando cambia famiglia sintattica (per esempio da `{{ ... }}` a `{% ... %}`).
|
|
767
|
+
|
|
768
|
+
### Altre primitive
|
|
769
|
+
```tsx
|
|
770
|
+
<Icon name="user" size={20} /> // usa provider icone configurato
|
|
771
|
+
<ImageAvatar src="..." title="Mario" alt="Mario" width={40} height={40} />
|
|
772
|
+
<Loader show={isLoading}>{content}</Loader> // indicatore al posto dei figli finché show è true
|
|
773
|
+
<Loader show>{null}</Loader> // solo indicatore (es. pagina in caricamento)
|
|
774
|
+
<Code language="tsx">{snippet}</Code> // syntax highlight
|
|
775
|
+
<GridSystem.Row><GridSystem.Col xs={12} md={6}>...</GridSystem.Col></GridSystem.Row>
|
|
776
|
+
<LocaleSwitcher locales={[{ code: 'it', label: 'Italiano' }]} />
|
|
777
|
+
<ThemeSwitcher />
|
|
778
|
+
<ProviderSwitcher />
|
|
779
|
+
<LayoutBuilder components={...} />
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
---
|
|
783
|
+
|
|
784
|
+
## Blocks
|
|
785
|
+
|
|
786
|
+
```tsx
|
|
787
|
+
<Breadcrumbs trail={[{ label: 'Home', href: '/' }, { label: 'Utenti' }]} separator="/" jsonLd />
|
|
788
|
+
<Brand url="/" label="MyApp" logo="/logo.svg" width={120} />
|
|
789
|
+
<Search onQueryChange={(e) => setQ(e.target.value)} />
|
|
790
|
+
<Notifications items={[...]} seeAllUrl="/notifications" onMarkAllRead={() => {}} />
|
|
791
|
+
<Carousel images={[...]} />
|
|
792
|
+
<Toolbar items={[...]}>
|
|
793
|
+
{children}
|
|
794
|
+
</Toolbar>
|
|
795
|
+
<SideNav />
|
|
796
|
+
<ListCard records={records} onRowClick={(r) => {}} />
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
**Brand:**
|
|
800
|
+
| Prop | Tipo |
|
|
801
|
+
|---|---|
|
|
802
|
+
| `url` | `string` |
|
|
803
|
+
| `label` | `string` |
|
|
804
|
+
| `logo` | `string` |
|
|
805
|
+
| `width` / `height` | `number` |
|
|
806
|
+
| `wrapperClassName` / `className` / `logoClassName` / `labelClassName` | `string` |
|
|
807
|
+
|
|
808
|
+
**SideNav:**
|
|
809
|
+
| Prop | Tipo |
|
|
810
|
+
|---|---|
|
|
811
|
+
| `items` | `SideNavItemDef[]` (in alternativa a `menuKey`) |
|
|
812
|
+
| `menuKey` | `string` — voci dal menu registrato in `<App>` |
|
|
813
|
+
| `bottomItems` | `SideNavItemDef[]` — voci fissate in fondo, sopra il footer e fuori dalla lista scorrevole (es. Impostazioni) |
|
|
814
|
+
| `header` / `footer` | `ReactNode` |
|
|
815
|
+
| `collapsed` + `onCollapsedChange` / `defaultCollapsed` | `boolean` — compressione controllata o no |
|
|
816
|
+
| `showIcons` / `showCollapseButton` | `boolean` (default `true`) |
|
|
817
|
+
| `embedded` | `boolean` — solo le voci, senza guscio (drawer, pannelli); `bottomItems` seguono le voci principali |
|
|
818
|
+
|
|
819
|
+
`SideNavItemDef`: `{ path, title?, icon?, group?, end?, badge?, children? }`. La voce attiva ha
|
|
820
|
+
`aria-current="page"`.
|
|
821
|
+
|
|
822
|
+
```tsx
|
|
823
|
+
<SideNav
|
|
824
|
+
items={[{ path: '/dashboard', title: 'Dashboard', icon: 'home' }]}
|
|
825
|
+
bottomItems={[{ path: '/settings', title: 'Impostazioni', icon: 'settings' }]}
|
|
826
|
+
/>
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
**ImageAvatar:**
|
|
830
|
+
| Prop | Tipo |
|
|
831
|
+
|---|---|
|
|
832
|
+
| `src` | `string` (obbligatoria) |
|
|
833
|
+
| `width` / `height` | `number` |
|
|
834
|
+
| `title` / `alt` | `string` |
|
|
835
|
+
| `fit` | `AvatarFit` |
|
|
836
|
+
| `badge` | `BadgeDescriptor` |
|
|
837
|
+
| `feedback` | `ReactNode` |
|
|
838
|
+
|
|
839
|
+
**Search:**
|
|
840
|
+
| Prop | Tipo |
|
|
841
|
+
|---|---|
|
|
842
|
+
| `onQueryChange` | `(e: React.ChangeEvent<HTMLInputElement>) => void` |
|
|
843
|
+
|
|
844
|
+
**Notifications:**
|
|
845
|
+
| Prop | Tipo |
|
|
846
|
+
|---|---|
|
|
847
|
+
| `items` | `NotificationItem[]` |
|
|
848
|
+
| `badge` | `BadgeProps` |
|
|
849
|
+
| `seeAllUrl` | `string` |
|
|
850
|
+
| `onMarkAllRead` | `() => void` |
|
|
851
|
+
| `motion` | `MotionReference` |
|
|
852
|
+
|
|
853
|
+
**ListCard:**
|
|
854
|
+
| Prop | Tipo |
|
|
855
|
+
|---|---|
|
|
856
|
+
| `title` / `description` / `meta` | `ReactNode` |
|
|
857
|
+
| `badge` | `ReactNode` |
|
|
858
|
+
| `icon` | `string \| ReactNode` |
|
|
859
|
+
| `trailing` / `footer` / `children` | `ReactNode` |
|
|
860
|
+
| `href` | `string` |
|
|
861
|
+
| `external` / `active` / `disabled` / `dashed` / `compact` | `boolean` |
|
|
862
|
+
| `align` | `'start' \| 'center'` |
|
|
863
|
+
| `onClick` | `(e) => void` |
|
|
864
|
+
| Classi CSS: `titleClassName`, `descriptionClassName`, `metaClassName`, `badgeClassName`, `leadingClassName`, `bodyClassName`, `headerClassName`, `trailingClassName`, `footerClassName` | `string` |
|
|
865
|
+
|
|
866
|
+
**Dropdown:**
|
|
867
|
+
| Prop | Tipo |
|
|
868
|
+
|---|---|
|
|
869
|
+
| `trigger` | `string \| ReactNode \| DropdownTogglerProps` |
|
|
870
|
+
| `badge` | `BadgeProps` |
|
|
871
|
+
| `header` / `footer` | `ReactNode` |
|
|
872
|
+
| `defaultOpen` / `open` | `boolean` |
|
|
873
|
+
| `onOpenChange` | `(open) => void` |
|
|
874
|
+
| `position` | `'start' \| 'end'` |
|
|
875
|
+
| `placement` | `'auto' \| 'top' \| 'bottom'` |
|
|
876
|
+
| `strategy` | `'fixed' \| 'absolute'` |
|
|
877
|
+
|
|
878
|
+
// Menu — renderizza una sezione del menuConfig passato ad <App>
|
|
879
|
+
<Menu menuKey="main" /> // 'main' o qualsiasi chiave definita in MenuConfig
|
|
880
|
+
<Menu menuKey="topbar" className="nav flex-row gap-2" />
|
|
881
|
+
// NON usare context= — la prop si chiama menuKey
|
|
882
|
+
|
|
883
|
+
// AuthButton — login/logout con il provider auth configurato
|
|
884
|
+
<AuthButton />
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
---
|
|
888
|
+
|
|
889
|
+
## Widgets
|
|
890
|
+
|
|
891
|
+
### MarkdownReader
|
|
892
|
+
```tsx
|
|
893
|
+
<MarkdownReader
|
|
894
|
+
content={markdownString}
|
|
895
|
+
onInternalLinkClick={(href, e) => { e.preventDefault(); navigate(href) }}
|
|
896
|
+
wrapperClassName="prose"
|
|
897
|
+
/>
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
### ImageEditor
|
|
901
|
+
```tsx
|
|
902
|
+
<ImageEditor src="..." onSave={(dataUrl) => save(dataUrl)} />
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
`ImageEditor` e un singolo componente pubblico. La libreria pesante `tui-image-editor` viene importata lazy a runtime dentro il componente, quindi non viene scaricata finche l'editor non viene effettivamente montato.
|
|
906
|
+
|
|
907
|
+
### TabDynamic
|
|
908
|
+
Tab con lazy loading dei contenuti — gestione dinamica aggiunta/rimozione tab.
|
|
909
|
+
|
|
910
|
+
---
|
|
911
|
+
|
|
912
|
+
## AI Integration
|
|
913
|
+
|
|
914
|
+
```ts
|
|
915
|
+
import { useAIProvider, getAIModelCatalog, createAIProviderRegistry } from '@llmnative/react'
|
|
916
|
+
|
|
917
|
+
// Hooks
|
|
918
|
+
const provider = useAIProvider() // provider di default
|
|
919
|
+
const registry = useAIProviderRegistry() // tutti i provider AI registrati
|
|
920
|
+
const catalog = getAIModelCatalog() // modelli disponibili
|
|
921
|
+
// ogni modello: { id, provider, model, label, pricing? } — pricing in USD per 1M token
|
|
922
|
+
// { input, output, currency: 'USD', source: 'provider' | 'models.dev' }; assente = prezzo
|
|
923
|
+
// non trovato (il modello resta in lista). isFreeAIModel(m) → input e output a 0.
|
|
924
|
+
// Il model picker di Prompt/Chatbot mostra "$in / $out", evidenzia i free, segnala i mancanti,
|
|
925
|
+
// e raggruppa per provider (ChatbotModelOption.group = providerLabel) con header sticky.
|
|
926
|
+
|
|
927
|
+
// Provider adapter restituito da useAIProvider()
|
|
928
|
+
const text = await provider.fetch("Scrivi un titolo per: {keyword}", { keyword: "React" })
|
|
929
|
+
const json = await provider.json("Lista di 5 categorie per blog tech")
|
|
930
|
+
const arr = await provider.array("5 tag per: machine learning")
|
|
931
|
+
|
|
932
|
+
// Provider custom
|
|
933
|
+
class MyAIProvider implements AIProviderAdapter {
|
|
934
|
+
async fetch(prompt: string, options?: AIRequestOptions): Promise<string> { ... }
|
|
935
|
+
async json<T>(prompt: string, options?: AIRequestOptions): Promise<T> { ... }
|
|
936
|
+
async array(prompt: string, options?: AIRequestOptions): Promise<string[]> { ... }
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// Factory per registri esterni
|
|
940
|
+
const registry = createAIProviderRegistry(definitions)
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
Configurazione API key in `Config.tsx`: `geminiApiKey`, `openaiApiKey`, `anthropicApiKey`, `deepSeekApiKey`, `mistralApiKey`, `openRouterApiKey`, `groqApiKey`, `cloudflare: { apiToken, accountId }` (Cloudflare Workers AI: l'account id fa parte dell'URL; discovery via `ai/models/search`, modelli Workers Paid esclusi salvo `includePaidModels`).
|
|
944
|
+
|
|
945
|
+
---
|
|
946
|
+
|
|
947
|
+
## Theme e icone
|
|
948
|
+
|
|
949
|
+
```tsx
|
|
950
|
+
const theme = useThemeController()
|
|
951
|
+
theme.applyTheme('flat') // 'default' | 'flat' | 'cyber' | custom
|
|
952
|
+
theme.setMode('dark') // 'light' | 'dark' | 'system'
|
|
953
|
+
|
|
954
|
+
const icons = useIconController()
|
|
955
|
+
icons.setProvider('phosphor') // 'lucide' | 'phosphor' | custom
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
---
|
|
959
|
+
|
|
960
|
+
## Head (SEO)
|
|
961
|
+
|
|
962
|
+
```ts
|
|
963
|
+
useHead({
|
|
964
|
+
title: 'Pagina',
|
|
965
|
+
description: '...',
|
|
966
|
+
robots: 'noindex',
|
|
967
|
+
canonical: 'https://...',
|
|
968
|
+
og: { title: '...', image: '...' }
|
|
969
|
+
})
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
---
|
|
973
|
+
|
|
974
|
+
## Global state (localStorage)
|
|
975
|
+
|
|
976
|
+
```ts
|
|
977
|
+
const vars = useGlobalVars()
|
|
978
|
+
getGlobalVars('key')
|
|
979
|
+
setGlobalVars({ key: 'value' })
|
|
980
|
+
removeGlobalVars('key')
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
---
|
|
984
|
+
|
|
985
|
+
## Hooks completi
|
|
986
|
+
|
|
987
|
+
| Hook | Ritorna |
|
|
988
|
+
|---|---|
|
|
989
|
+
| `useDataProvider(name?)` | `DataProviderAdapter \| null` |
|
|
990
|
+
| `useStorageProvider(name?)` | `StorageProviderAdapter \| null` |
|
|
991
|
+
| `useAuthProvider(name?)` | `AuthProviderAdapter \| null` |
|
|
992
|
+
| `useEmailProvider(name?)` | `EmailProviderAdapter \| null` |
|
|
993
|
+
| `useAIProvider(name?)` | `AIProviderAdapter \| null` |
|
|
994
|
+
| `useAIProviderRegistry()` | `Record<string, AIProviderAdapter>` |
|
|
995
|
+
| `useCredentialsProvider()` | `CredentialsAdapter \| null` |
|
|
996
|
+
| `useTheme()` | `Theme \| undefined` |
|
|
997
|
+
| `useThemeController()` | `ThemeController` |
|
|
998
|
+
| `useIconController()` | `IconController` |
|
|
999
|
+
| `useAccessToken(clientID, renew?)` | `boolean` | Ottiene token per servizio esterno (true = ok) |
|
|
1000
|
+
| `useGlobalVars()` | `Record<string, unknown>` |
|
|
1001
|
+
| `useHead()` | `HeadController` |
|
|
1002
|
+
| `useDocumentHead()` | `HeadController` |
|
|
1003
|
+
| `useAssetsHead(title?)` | `void` |
|
|
1004
|
+
| `useLanguageHead(lang?)` | `void` |
|
|
1005
|
+
| `useSocialHead(config)` | `void` |
|
|
1006
|
+
| `usePaginationHead(config)` | `void` |
|
|
1007
|
+
| `usePwaHead(config)` | `void` |
|
|
1008
|
+
| `useSchemaOrgHead(config)` | `void` |
|
|
1009
|
+
| `useHeadLinks(links)` | `void` |
|
|
1010
|
+
| `useProxy()` | `(url, init?) => Promise<Response>` |
|
|
1011
|
+
| `useImage()` | `UseImageResult` |
|
|
1012
|
+
| `useI18n()` | `I18nController` |
|
|
1013
|
+
| `useMenu()` | Menu hook |
|
|
1014
|
+
| `useFormController(controller?)` | `FormController` |
|
|
1015
|
+
| `useFormActions()` | `FormController` |
|
|
1016
|
+
| `useApiProvider(pathname?)` | `ApiProviderAdapter \| null` |
|
|
1017
|
+
| `useSetProvider(service, driver?)` | `void` |
|
|
1018
|
+
| `useProvider(name)` | `ProviderConfigurationState` |
|
|
1019
|
+
| `useProviderSession()` | `ProviderSession` |
|
|
1020
|
+
| `useDropBoxConnect(config?)` | `DropBoxConnectResult` |
|
|
1021
|
+
| `useEnterMotion(baseStyle?, ref?, fallback?)` | `CSSProperties` |
|
|
1022
|
+
| `useMotionState(active, ref?, fallback?, baseStyle?)` | `CSSProperties` |
|
|
1023
|
+
| `useMotionEffect(ref?, fallback?)` | `ResolvedMotionEffect` |
|
|
1024
|
+
| `usePressMotion(hovered, baseStyle?, ref?, fallback?)` | `{ style, pressHandlers }` |
|
|
1025
|
+
| `useIconProvider()` | `IconProvider` |
|
|
1026
|
+
|
|
1027
|
+
---
|
|
1028
|
+
|
|
1029
|
+
## Tipi chiave
|
|
1030
|
+
|
|
1031
|
+
```ts
|
|
1032
|
+
type RecordProps = Record<string, FieldValue>
|
|
1033
|
+
type FieldValue = string | number | boolean | null | undefined | Record<string, unknown> | unknown[]
|
|
1034
|
+
type RecordArray = RecordProps[]
|
|
1035
|
+
|
|
1036
|
+
type OrderConfig = { field: string; dir: 'asc' | 'desc' }
|
|
1037
|
+
type WhereClause = { field: string; op: '==' | '!=' | '<' | '<=' | '>' | '>=' | 'in' | 'not-in' | 'array-contains' | 'array-contains-any'; value: unknown }
|
|
1038
|
+
|
|
1039
|
+
interface UIProps {
|
|
1040
|
+
before?: React.ReactNode
|
|
1041
|
+
after?: React.ReactNode
|
|
1042
|
+
wrapperClassName?: string
|
|
1043
|
+
className?: string
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
type UserProfile = { uid?: string; email?: string; displayName?: string; photoURL?: string; [key: string]: unknown }
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
---
|
|
1050
|
+
|
|
1051
|
+
## Utilities
|
|
1052
|
+
|
|
1053
|
+
```ts
|
|
1054
|
+
import { cn, converter, Sanitizer, Order, path, fetchJson, fetchRest, fetchWithRetry } from '@llmnative/react'
|
|
1055
|
+
import { getKeywordIdeas, getGoogleTrendsData, getGoogleTrendsRelated } from '@llmnative/react'
|
|
1056
|
+
|
|
1057
|
+
cn('base', condition && 'extra', className) // tailwind-merge classname
|
|
1058
|
+
new Sanitizer().sanitize(html) // XSS sanitizer (non usare dangerouslySetInnerHTML)
|
|
1059
|
+
new Order().order(records, { field: 'name', dir: 'asc' }) // ordina record array
|
|
1060
|
+
fetchJson(url, init?) // fetch + JSON parse + error handling
|
|
1061
|
+
fetchRest(path, init?) // fetch REST con auth
|
|
1062
|
+
getKeywordIdeas(keyword) // Google Ads Keyword Ideas
|
|
1063
|
+
getGoogleTrendsData(keyword) // Google Trends interest over time
|
|
1064
|
+
getGoogleTrendsRelated(keyword) // Google Trends related queries
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
---
|
|
1068
|
+
|
|
1069
|
+
## Scaffold (npx create)
|
|
1070
|
+
|
|
1071
|
+
```bash
|
|
1072
|
+
npx @llmnative/react create
|
|
1073
|
+
npx @llmnative/react create --yes --provider=mock --template=admin
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
Flag disponibili: `--name` `--provider=firebase|supabase|mock|custom` `--ai-provider` `--icon-provider` `--theme` `--template=blank|admin|crm|inventory|project`
|
|
1077
|
+
|
|
1078
|
+
**5 template disponibili:**
|
|
1079
|
+
| Template | Pagine incluse | Uso tipico |
|
|
1080
|
+
|---|---|---|
|
|
1081
|
+
| `blank` | home | App vuota — massima libertà |
|
|
1082
|
+
| `admin` | home, users, roles, settings | Dashboard admin/backoffice |
|
|
1083
|
+
| `crm` | home, companies, contacts, deals | CRM |
|
|
1084
|
+
| `inventory` | home + gestione inventario | Magazzino |
|
|
1085
|
+
| `project` | home + gestione progetti | PM tool |
|
|
1086
|
+
|
|
1087
|
+
**Struttura generata in-place nella current directory:**
|
|
1088
|
+
```
|
|
1089
|
+
src/
|
|
1090
|
+
index.tsx ← App entry
|
|
1091
|
+
vite-env.d.ts
|
|
1092
|
+
conf/
|
|
1093
|
+
app.ts ← providers config letta da .env (VITE_PROVIDER, VITE_AI_PROVIDER...)
|
|
1094
|
+
menu.ts ← MenuConfig con path, title, icon, page (layout opzionale: c'è LayoutDefault)
|
|
1095
|
+
data/
|
|
1096
|
+
mockData.ts ← dati fake per MockDataProvider
|
|
1097
|
+
layouts/
|
|
1098
|
+
Default.tsx ← layout wrapper (header + sidebar + main + footer)
|
|
1099
|
+
sections/
|
|
1100
|
+
Header.tsx / Sidebar.tsx / PageHeader.tsx / Footer.tsx ← solo componenti pubblici (SideNav, AuthButton, ActionButton, Brand, Breadcrumbs)
|
|
1101
|
+
pages/
|
|
1102
|
+
home/HomePage.tsx
|
|
1103
|
+
[altre pagine dal template scelto]
|
|
1104
|
+
styles/
|
|
1105
|
+
globals.css ← @import '@llmnative/react/dist/index.css' + Tailwind override
|
|
1106
|
+
.env ← VITE_PROVIDER, VITE_THEME, chiavi API (non committare)
|
|
1107
|
+
.env.example ← stessa struttura, valori vuoti (committare)
|
|
1108
|
+
vite.config.ts / tsconfig.json / postcss.config.js / index.html
|
|
1109
|
+
```
|
|
1110
|
+
|
|
1111
|
+
**Pattern conf/app.ts** (auto-generato, leggere per capire come funziona):
|
|
1112
|
+
```ts
|
|
1113
|
+
import type { AppProvidersConfig } from '@llmnative/react'
|
|
1114
|
+
import { mockData } from '../data/mockData'
|
|
1115
|
+
const env = import.meta.env
|
|
1116
|
+
|
|
1117
|
+
export const providers: AppProvidersConfig = {
|
|
1118
|
+
mock: { data: mockData },
|
|
1119
|
+
ai: { openaiApiKey: env.VITE_OPENAI_API_KEY ?? '' }, // AI dentro providers, non prop <App>
|
|
1120
|
+
services: { data: 'mock', auth: 'googleAuth' },
|
|
1121
|
+
}
|
|
1122
|
+
```
|
|
1123
|
+
|
|
1124
|
+
Driver per `VITE_PROVIDER`: `firebase` → `firestoreDb` + `firestorage` + `firebaseAuth`; `supabase` → `supabaseDb` + `supabaseStorage` + `supabaseAuth`; `mock` → `mock` + `googleAuth`. `index.tsx` passa `LayoutDefault={Default}` ad `<App>`.
|
|
1125
|
+
|
|
1126
|
+
---
|
|
1127
|
+
|
|
1128
|
+
## Gotchas — pitfall frequenti
|
|
1129
|
+
|
|
1130
|
+
| Errore | Causa | Fix |
|
|
1131
|
+
|---|---|---|
|
|
1132
|
+
| `aiConfig` prop non esiste su `<App>` | La config AI va dentro `providers.ai`, NON come prop separata | `providers={{ ai: { openaiApiKey } }}` |
|
|
1133
|
+
| `inputType` o `Input.Number`/`Input.Email` non esistono | Il tipo HTML è la prop `type` | `<Input type="number">`, `<Input type="email">`, `<Input type="date">` |
|
|
1134
|
+
| `Menu context=` non esiste | La prop si chiama `menuKey` | `<Menu menuKey="main" />` |
|
|
1135
|
+
| `SignInButton` non esiste | Rinominato | `<AuthButton />` |
|
|
1136
|
+
| `firebase: { config: ... }` errato | FirebaseConfig è flat | `firebase: { apiKey, authDomain, databaseURL, ... }` |
|
|
1137
|
+
| `google: { oAuth2: { clientId } }` errato | GoogleProviderConfig è flat | `google: { clientId, scope }` |
|
|
1138
|
+
| `children` su `Grid` | Grid non accetta children | Usa `form={<Fields />}` per il form inline |
|
|
1139
|
+
| `recordId` su `Form` | Prop non esiste | La chiave del record viene da URL automaticamente |
|
|
1140
|
+
| `MenuConfig` non importabile | Era type interno | Ora esportato: `import type { MenuConfig } from '@llmnative/react'` |
|
|
1141
|
+
| `GridCellContext` non importabile | Mancava dall'index | Ora esportato: `import type { GridCellContext } from '@llmnative/react'` |
|
|
1142
|
+
|
|
1143
|
+
---
|
|
1144
|
+
|
|
1145
|
+
## Dove cercare nel codice sorgente
|
|
1146
|
+
|
|
1147
|
+
| Cosa | File |
|
|
1148
|
+
|---|---|
|
|
1149
|
+
| Form | `src/components/widgets/Form.tsx` |
|
|
1150
|
+
| Grid | `src/components/widgets/Grid.tsx` |
|
|
1151
|
+
| Prompt | `src/components/widgets/Prompt.tsx` |
|
|
1152
|
+
| Campi form | `src/components/ui/fields/` |
|
|
1153
|
+
| Blocks (Menu, Toolbar, Notifications, Dropdown…) | `src/components/blocks/` |
|
|
1154
|
+
| Provider data Firebase (RTDB) | `src/providers/data/firebase.ts` |
|
|
1155
|
+
| Provider data Firestore | `src/providers/data/firestore.ts` |
|
|
1156
|
+
| Provider data Supabase | `src/providers/data/supabase.ts` |
|
|
1157
|
+
| Provider storage Firebase | `src/providers/storage/firebase.ts` |
|
|
1158
|
+
| Provider storage Supabase | `src/providers/storage/supabase.ts` |
|
|
1159
|
+
| Provider auth Firebase | `src/providers/auth/firebase/FirebaseAuthProvider.ts` |
|
|
1160
|
+
| Provider auth Supabase | `src/providers/auth/supabase/` |
|
|
1161
|
+
| Provider auth Google | `src/providers/auth/google/` |
|
|
1162
|
+
| Provider auth Dropbox | `src/providers/auth/dropbox/` |
|
|
1163
|
+
| Provider email Gmail | `src/providers/email/google/GmailEmailProvider.ts` |
|
|
1164
|
+
| Provider AI definitions | `src/providers/ai/` |
|
|
1165
|
+
| Provider credenziali | `src/providers/credentials/` |
|
|
1166
|
+
| Provider session | `src/providers/ProviderSession.ts` |
|
|
1167
|
+
| Proxy CORS | `src/providers/proxy.ts` |
|
|
1168
|
+
| Motion system | `src/motion.ts` |
|
|
1169
|
+
| I18n | `src/I18n.tsx` |
|
|
1170
|
+
| Dropbox integration | `src/providers/dropbox/` |
|
|
1171
|
+
| Interfaccia DataProvider | `src/providers/data/DataProvider.ts` |
|
|
1172
|
+
| Interfaccia StorageProvider | `src/providers/storage/StorageProvider.ts` |
|
|
1173
|
+
| Interfaccia AuthProvider | `src/providers/auth/AuthProvider.ts` |
|
|
1174
|
+
| Interfaccia EmailProvider | `src/providers/email/EmailProvider.ts` |
|
|
1175
|
+
| Interfaccia AIProvider | `src/providers/ai/AIProvider.ts` |
|
|
1176
|
+
| Registry provider | `src/providers/manifest.ts` |
|
|
1177
|
+
| Config provider | `src/providers/ProviderConfiguration.ts` |
|
|
1178
|
+
| SEO / Google Trends | `src/providers/seo/google/` |
|
|
1179
|
+
| Sistema tema | `src/Theme.tsx` |
|
|
1180
|
+
| Config runtime | `src/Config.tsx` |
|
|
1181
|
+
| Tipi TypeScript | `src/types/` |
|
|
1182
|
+
| Utilities pure | `src/libs/` |
|