@tinya-code/gas-admin-ui 0.1.0-alpha.5 → 0.2.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +584 -116
  2. package/fesm2022/{tinya-code-gas-admin-ui-calendar-view-r-RXDE37.mjs → tinya-code-gas-admin-ui-calendar-view-CRTEx4f5.mjs} +4 -4
  3. package/fesm2022/{tinya-code-gas-admin-ui-calendar-view-r-RXDE37.mjs.map → tinya-code-gas-admin-ui-calendar-view-CRTEx4f5.mjs.map} +1 -1
  4. package/fesm2022/{tinya-code-gas-admin-ui-condition-evaluator-BBzWUuHl.mjs → tinya-code-gas-admin-ui-condition-evaluator-DL15SFPt.mjs} +2 -2
  5. package/fesm2022/{tinya-code-gas-admin-ui-condition-evaluator-BBzWUuHl.mjs.map → tinya-code-gas-admin-ui-condition-evaluator-DL15SFPt.mjs.map} +1 -1
  6. package/fesm2022/{tinya-code-gas-admin-ui-data-source.service-LD8AM3Rn.mjs → tinya-code-gas-admin-ui-data-source.service-BtZqPzmP.mjs} +2 -3
  7. package/fesm2022/{tinya-code-gas-admin-ui-data-source.service-LD8AM3Rn.mjs.map → tinya-code-gas-admin-ui-data-source.service-BtZqPzmP.mjs.map} +1 -1
  8. package/fesm2022/{tinya-code-gas-admin-ui-error-mapper.service-Ci0UpXtV.mjs → tinya-code-gas-admin-ui-error-mapper.service-pEOAeAzK.mjs} +2 -2
  9. package/fesm2022/{tinya-code-gas-admin-ui-error-mapper.service-Ci0UpXtV.mjs.map → tinya-code-gas-admin-ui-error-mapper.service-pEOAeAzK.mjs.map} +1 -1
  10. package/fesm2022/{tinya-code-gas-admin-ui-file-Dg2N3XQ2.mjs → tinya-code-gas-admin-ui-file-B6_vveO5.mjs} +3 -3
  11. package/fesm2022/{tinya-code-gas-admin-ui-file-Dg2N3XQ2.mjs.map → tinya-code-gas-admin-ui-file-B6_vveO5.mjs.map} +1 -1
  12. package/fesm2022/{tinya-code-gas-admin-ui-form-view-BwGupSbi.mjs → tinya-code-gas-admin-ui-form-view-D1QLZWnG.mjs} +9 -10
  13. package/fesm2022/{tinya-code-gas-admin-ui-form-view-BwGupSbi.mjs.map → tinya-code-gas-admin-ui-form-view-D1QLZWnG.mjs.map} +1 -1
  14. package/fesm2022/{tinya-code-gas-admin-ui-image-Bjw9ImlS.mjs → tinya-code-gas-admin-ui-image-lUe8nrUf.mjs} +3 -3
  15. package/fesm2022/{tinya-code-gas-admin-ui-image-Bjw9ImlS.mjs.map → tinya-code-gas-admin-ui-image-lUe8nrUf.mjs.map} +1 -1
  16. package/fesm2022/{tinya-code-gas-admin-ui-kanban-view-CeGueI57.mjs → tinya-code-gas-admin-ui-kanban-view-B1rUNjlp.mjs} +4 -4
  17. package/fesm2022/{tinya-code-gas-admin-ui-kanban-view-CeGueI57.mjs.map → tinya-code-gas-admin-ui-kanban-view-B1rUNjlp.mjs.map} +1 -1
  18. package/fesm2022/{tinya-code-gas-admin-ui-list-CMzl9MkX.mjs → tinya-code-gas-admin-ui-list-CH7b7dKh.mjs} +2 -2
  19. package/fesm2022/{tinya-code-gas-admin-ui-list-CMzl9MkX.mjs.map → tinya-code-gas-admin-ui-list-CH7b7dKh.mjs.map} +1 -1
  20. package/fesm2022/{tinya-code-gas-admin-ui-multiselect-CuWwAfdY.mjs → tinya-code-gas-admin-ui-multiselect-Bg4cnf6N.mjs} +3 -3
  21. package/fesm2022/{tinya-code-gas-admin-ui-multiselect-CuWwAfdY.mjs.map → tinya-code-gas-admin-ui-multiselect-Bg4cnf6N.mjs.map} +1 -1
  22. package/fesm2022/{tinya-code-gas-admin-ui-relation-B3W3Ynsp.mjs → tinya-code-gas-admin-ui-relation-CDEuUpKU.mjs} +3 -3
  23. package/fesm2022/{tinya-code-gas-admin-ui-relation-B3W3Ynsp.mjs.map → tinya-code-gas-admin-ui-relation-CDEuUpKU.mjs.map} +1 -1
  24. package/fesm2022/{tinya-code-gas-admin-ui-render-registry-B_D5Jhp9.mjs → tinya-code-gas-admin-ui-render-registry-mG0BDGZI.mjs} +10 -10
  25. package/fesm2022/{tinya-code-gas-admin-ui-render-registry-B_D5Jhp9.mjs.map → tinya-code-gas-admin-ui-render-registry-mG0BDGZI.mjs.map} +1 -1
  26. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-Bb0WJRwc.mjs +494 -0
  27. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-Bb0WJRwc.mjs.map +1 -0
  28. package/fesm2022/{tinya-code-gas-admin-ui-seed-CNbQtREA.mjs → tinya-code-gas-admin-ui-seed-C_tWI-Jr.mjs} +2 -2
  29. package/fesm2022/{tinya-code-gas-admin-ui-seed-CNbQtREA.mjs.map → tinya-code-gas-admin-ui-seed-C_tWI-Jr.mjs.map} +1 -1
  30. package/fesm2022/{tinya-code-gas-admin-ui-select-DzaRfHXO.mjs → tinya-code-gas-admin-ui-select-CL2kj7pM.mjs} +3 -3
  31. package/fesm2022/{tinya-code-gas-admin-ui-select-DzaRfHXO.mjs.map → tinya-code-gas-admin-ui-select-CL2kj7pM.mjs.map} +1 -1
  32. package/fesm2022/{tinya-code-gas-admin-ui-upload.service-C1g545w5.mjs → tinya-code-gas-admin-ui-upload.service-D700Obgh.mjs} +2 -2
  33. package/fesm2022/{tinya-code-gas-admin-ui-upload.service-C1g545w5.mjs.map → tinya-code-gas-admin-ui-upload.service-D700Obgh.mjs.map} +1 -1
  34. package/fesm2022/tinya-code-gas-admin-ui.mjs +65 -11
  35. package/fesm2022/tinya-code-gas-admin-ui.mjs.map +1 -1
  36. package/package.json +8 -2
  37. package/types/tinya-code-gas-admin-ui.d.ts +119 -29
  38. package/fesm2022/tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs +0 -281
  39. package/fesm2022/tinya-code-gas-admin-ui-api.service-CZrUeclM.mjs.map +0 -1
  40. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs +0 -131
  41. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-CDrz9pVN.mjs.map +0 -1
package/README.md CHANGED
@@ -1,207 +1,675 @@
1
1
  # @tinya-code/gas-admin-ui
2
2
 
3
- Motor de render del ecosistema `@tinya-code`: el front que **sólo declara** su
4
- dominio (`schemas/*.schema.ts`) y consume el motor compilado — mismo contrato
5
- que `@tinya-code/gas-admin` en el back, pero para Angular. El paquete dibuja
6
- vistas, campos, acciones, shell y rutas; vos no tocás componentes a mano.
3
+ Angular rendering engine for the `@tinya-code` ecosystem. You declare your domain in typed schema files — the engine renders views, forms, lists, actions, shell, and routes. You never write components by hand.
7
4
 
8
- ## Requisitos
5
+ ## Requirements
9
6
 
10
- | Requisito | Versión / detalle |
7
+ | Requirement | Version / detail |
11
8
  | --- | --- |
12
9
  | Angular | `22.2` (standalone, signals) |
13
- | Peers de Angular | `@angular/{cdk,common,core,forms,platform-browser,router} ^22.2` |
14
- | Iconos / charts | `@lucide/angular ^1.51`, `chart.js ^4.5` |
15
- | CSS | Tailwind v4 — con el `@source` del motor ([paso 2](#2-declará-el-source-de-tailwind)) |
16
- | Back | [`@tinya-code/gas-admin`](https://www.npmjs.com/package/@tinya-code/gas-admin) — su CLI genera y valida los schemas del front |
10
+ | Angular peers | `@angular/{cdk,common,core,forms,platform-browser,router} ^22.2` |
11
+ | Icons / charts | `@lucide/angular ^1.51`, `chart.js ^4.5` |
12
+ | CSS | Tailwind v4 — with the engine's `@source` ([step 2](#2-declare-the-tailwind-source)) |
13
+ | Backend | [`@tinya-code/gas-admin`](https://www.npmjs.com/package/@tinya-code/gas-admin) — its CLI generates and validates front-end schemas |
17
14
 
18
- ## Camino rápido desde cero
15
+ ---
19
16
 
20
- ### 1. Instalá el paquete
17
+ ## Quick start from scratch
18
+
19
+ ### 1. Install the package
21
20
 
22
21
  ```bash
23
22
  npm i @tinya-code/gas-admin-ui
24
23
  ```
25
24
 
26
- Un proyecto Angular 22.2 ya trae los `@angular/*`; verificá que estén los
27
- demás peers (`@lucide/angular`, `chart.js` y los `@angular/*` que falten).
25
+ An Angular 22.2 project already has `@angular/*` — verify the remaining peers (`@lucide/angular`, `chart.js`).
28
26
 
29
- ### 2. Declará el `@source` de Tailwind
27
+ ### 2. Declare the Tailwind source
30
28
 
31
- En `src/styles.css`:
29
+ In `src/styles.css`:
32
30
 
33
31
  ```css
32
+ /* Required: lets Tailwind scan the engine's templates for utility classes */
34
33
  @source '../node_modules/@tinya-code/gas-admin-ui/fesm2022';
34
+
35
+ @import 'tailwindcss';
36
+ @import './styles/theme.css'; /* your design tokens */
37
+ @import './styles/base.css'; /* your base resets */
35
38
  ```
36
39
 
37
- Sin esto la app compila pero la UI del motor sale sin clases (visual roto,
38
- tests en verde).
40
+ Without this the app compiles and tests pass, but the engine UI renders with no styles.
39
41
 
40
- ### 3. Generá los schemas desde el back
42
+ ### 3. Wire the engine — `app.config.ts`
41
43
 
42
- Con el CLI del back instalado en el proyecto (`npm i -D @tinya-code/gas-admin`)
43
- y el dominio declarado en `api/schema/resources/`:
44
+ ```ts
45
+ // src/app/app.config.ts
46
+ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
47
+ import { provideRouter } from '@angular/router';
48
+ import { API_URL, provideGasAdmin } from '@tinya-code/gas-admin-ui';
49
+ import { routes } from './app.routes';
50
+ import { schemas } from './schemas/registry';
51
+ import { authSchema } from './schemas/auth.schema';
52
+ import { environment } from '../environments/environment';
44
53
 
45
- ```bash
46
- npx gas-admin scaffold
54
+ export const appConfig: ApplicationConfig = {
55
+ providers: [
56
+ provideBrowserGlobalErrorListeners(),
57
+ provideRouter(routes),
58
+ provideGasAdmin({ schemas, auth: authSchema }),
59
+ { provide: API_URL, useValue: environment.apiUrl },
60
+ ],
61
+ };
47
62
  ```
48
63
 
49
- Escribe 9 ficheros en `src/app/schemas/` ([estructura](#estructura-generada-en-srcappschemas)).
50
- Si la carpeta ya tiene `*.ts` falla con exit 1; `--force` regenera.
64
+ ```ts
65
+ // src/environments/environment.ts
66
+ export const environment = {
67
+ production: false,
68
+ apiUrl: 'http://localhost:3000/api/v1',
69
+ };
70
+ ```
51
71
 
52
- ### 4. Cableá el motor — `app.config.ts`
72
+ ### REST Driver (NestJS / standard REST)
53
73
 
54
- ```ts
55
- import { API_URL, provideGasAdmin } from '@tinya-code/gas-admin-ui';
74
+ By default, the engine uses the Google Apps Script transport driver (`driver: 'gas'`), which sends every request as an HTTP `POST text/plain` with the payload envelope `{ token, method, path, payload }`.
56
75
 
76
+ If your backend is built with **NestJS**, **Express**, **FastAPI**, or any standard REST API, set `driver: 'rest'` in `provideGasAdmin`:
77
+
78
+ ```ts
79
+ // src/app/app.config.ts
57
80
  export const appConfig: ApplicationConfig = {
58
81
  providers: [
82
+ provideBrowserGlobalErrorListeners(),
59
83
  provideRouter(routes),
60
- provideGasAdmin({ schemas, auth: authSchema }),
84
+ provideGasAdmin({
85
+ schemas,
86
+ auth: authSchema,
87
+ driver: 'rest', // <-- Enables standard REST transport
88
+ }),
61
89
  { provide: API_URL, useValue: environment.apiUrl },
62
90
  ],
63
91
  };
64
92
  ```
65
93
 
66
- El motor absorbe HTTP (interceptor del envelope), los loaders de
67
- config/sesión y el catálogo de schemas; la app conserva router, environment
68
- y sus schemas de dominio.
94
+ #### Differences in Transport
95
+
96
+ | Feature | `driver: 'gas'` (default) | `driver: 'rest'` |
97
+ |---|---|---|
98
+ | **HTTP Method** | Always `POST` | Real verbs: `GET`, `POST`, `PUT`, `DELETE` |
99
+ | **URL Target** | Always `API_URL` directly | `${API_URL}${path}` (e.g. `/api/v1/products`) |
100
+ | **Auth Token** | Body JSON: `{ token: "..." }` | Header: `Authorization: Bearer <token>` |
101
+ | **Content-Type** | `text/plain` (avoids CORS preflight) | `application/json` (POST/PUT/PATCH) |
102
+ | **Query Params** | Nested in body `{ payload: { ... } }` | Real URL query string (`?view=active&sort=asc`) |
103
+ | **Envelope** | `{ token, method, path, payload }` | None (direct JSON body or query params) |
104
+
105
+ #### Expected REST Backend Contract
69
106
 
70
- ### 5. Rutas — `app.routes.ts`
107
+ When `driver: 'rest'` is active:
108
+ - **`GET /api/v1/resource`**: returns `T[]` or `{ items: T[] }`. Query parameters such as `?view=...` or filters are sent as standard query string parameters.
109
+ - **`GET /api/v1/resource/:key`**: returns `T`.
110
+ - **`POST /api/v1/resource`**: body is `{ ...data }` with `Content-Type: application/json`.
111
+ - **`PUT /api/v1/resource/:key`**: body is `{ ...data }` with `Content-Type: application/json`.
112
+ - **`DELETE /api/v1/resource/:key`**: sends HTTP DELETE.
113
+ - **`POST /api/v1/auth/login`**: body is `{ email, password }`, returns `{ token, user }`.
114
+ - **`GET /api/v1/auth/me`**: requires `Authorization: Bearer <token>`, returns `{ user }`.
115
+ - **`POST /api/v1/auth/logout`**: requires `Authorization: Bearer <token>`.
116
+
117
+ `driver` defaults to `'gas'` if omitted. Existing Apps Script projects do not require any configuration changes.
118
+
119
+ ### 4. Declare routes — `app.routes.ts`
71
120
 
72
121
  ```ts
122
+ // src/app/app.routes.ts
123
+ import { Routes } from '@angular/router';
73
124
  import { authGuard, shellRoutes } from '@tinya-code/gas-admin-ui';
125
+ import { schemas } from './schemas/registry';
126
+
127
+ const firstResource = schemas[0]?.id || 'dashboard';
74
128
 
75
129
  export const routes: Routes = [
76
130
  { path: '', pathMatch: 'full', redirectTo: firstResource },
77
- { path: 'login', loadComponent: () => import('@tinya-code/gas-admin-ui').then((m) => m.Login) },
78
- { path: '', canActivate: [authGuard], loadChildren: () => shellRoutes },
131
+ {
132
+ path: 'login',
133
+ loadComponent: () => import('@tinya-code/gas-admin-ui').then((m) => m.Login),
134
+ },
135
+ {
136
+ path: '',
137
+ canActivate: [authGuard],
138
+ loadChildren: () => shellRoutes,
139
+ },
79
140
  { path: '**', redirectTo: firstResource },
80
141
  ];
81
142
  ```
82
143
 
83
- `firstResource` lo definís en el archivo:
84
- `const firstResource = schemas[0].id;` (primer recurso del catálogo). El login
85
- queda fuera del shell a propósito: si viviera dentro, `authGuard` lo
86
- bloquearía a él mismo.
144
+ `login` lives outside the shell on purpose — if it were inside, `authGuard` would block it.
145
+
146
+ ### 5. Declare auth — `auth.schema.ts`
147
+
148
+ ```ts
149
+ // src/app/schemas/auth.schema.ts
150
+ import type { AuthSchema } from '@tinya-code/gas-admin-ui';
151
+
152
+ export const authSchema: AuthSchema = {
153
+ providers: ['password'],
154
+ loginFields: [
155
+ { key: 'email', type: 'email', required: true },
156
+ { key: 'password', type: 'password', required: true },
157
+ ],
158
+ redirectAfterLogin: '/dashboard',
159
+ rememberMe: false,
160
+ };
161
+ ```
162
+
163
+ ### 6. Declare your schemas — `registry.ts`
87
164
 
88
- ### 6. Revisá los labels de cada `*.schema.ts`
165
+ ```ts
166
+ // src/app/schemas/registry.ts
167
+ import type { ResourceSchema } from '@tinya-code/gas-admin-ui';
168
+ import { dashboardSchema } from './dashboard.schema';
169
+ import { businessSchema } from './business.schema';
170
+ import { clientsSchema } from './clients.schema';
171
+ import { ranksSchema } from './ranks.schema';
172
+ import { rewardsSchema } from './rewards.schema';
173
+ import { couponsSchema } from './coupons.schema';
174
+ import { feedbackSchema } from './feedback.schema';
175
+
176
+ export const schemas: ResourceSchema[] = [
177
+ dashboardSchema,
178
+ businessSchema,
179
+ clientsSchema,
180
+ ranksSchema,
181
+ rewardsSchema,
182
+ couponsSchema,
183
+ feedbackSchema,
184
+ ];
185
+ ```
89
186
 
90
- Los `label` salen de las keys: son copy de demo. Reemplazalos por los del
91
- dominio y sumá las vistas/filtros que quieras (el recorrido de capacidades
92
- está en la cabecera de `registry.ts`). También podés escribir toda la
93
- estructura a mano si preferís no usar el scaffold.
187
+ The array order determines both menu order and the default redirect route.
94
188
 
95
- ### 7. Levantá la app
189
+ ### 7. Start the app
96
190
 
97
191
  ```bash
98
192
  npm start
99
193
  ```
100
194
 
101
- Verificación: el shell arranca en `/login` y, con sesión, muestra el menú y
102
- las vistas derivadas de tus schemas.
195
+ The shell starts at `/login` and, once authenticated, shows the sidebar menu and views derived from your schemas.
196
+
197
+ ---
198
+
199
+ ## Schema examples (real implementations)
200
+
201
+ The examples below are taken from a production consumer of this library.
202
+
203
+ ### Dashboard — `kind: 'dashboard'`
204
+
205
+ A read-only panel with metric cards. No list, no form.
206
+
207
+ ```ts
208
+ // src/app/schemas/dashboard.schema.ts
209
+ import type { ResourceSchema } from '@tinya-code/gas-admin-ui';
210
+
211
+ export const dashboardSchema: ResourceSchema = {
212
+ id: 'dashboard',
213
+ label: 'Panel',
214
+ labelPlural: 'Dashboard',
215
+ kind: 'dashboard',
216
+ uiViews: {
217
+ overview: { type: 'dashboard' },
218
+ },
219
+ endpoint: {
220
+ get: '/admin/dashboard',
221
+ },
222
+ keyField: 'id',
223
+ titleField: 'label',
224
+ listColumns: [],
225
+ fields: [],
226
+ widgets: [
227
+ { type: 'metric-card', key: 'totalClients', label: 'Total Clients', hint: 'Registered memberships', width: 4 },
228
+ { type: 'metric-card', key: 'activeClients30d', label: 'Active (30d)', hint: 'Visited in last 30 days', width: 4 },
229
+ { type: 'metric-card', key: 'visitsThisMonth', label: 'Visits This Month', hint: 'Current month', width: 4 },
230
+ { type: 'metric-card', key: 'averageRating', label: 'Average Rating', hint: 'Historical satisfaction', width: 4 },
231
+ { type: 'metric-card', key: 'issuedCoupons', label: 'Issued Coupons', hint: 'All time', width: 4 },
232
+ { type: 'metric-card', key: 'usedCoupons', label: 'Redeemed Coupons', hint: 'Effective rewards', width: 4 },
233
+ ],
234
+ };
235
+ ```
236
+
237
+ ---
238
+
239
+ ### Singleton — `kind: 'singleton'` with sections layout
240
+
241
+ One record, no list. The form is divided into labelled sections.
242
+
243
+ ```ts
244
+ // src/app/schemas/business.schema.ts
245
+ import type { ResourceSchema } from '@tinya-code/gas-admin-ui';
246
+
247
+ export const businessSchema: ResourceSchema = {
248
+ id: 'business',
249
+ label: 'My Business',
250
+ labelPlural: 'My Business',
251
+ kind: 'singleton',
252
+ uiViews: {
253
+ settings: { type: 'form' },
254
+ },
255
+ endpoint: {
256
+ get: '/admin/businesses/current',
257
+ update: '/admin/businesses/current',
258
+ },
259
+ keyField: 'key',
260
+ titleField: 'name',
261
+ listColumns: [],
262
+ layout: {
263
+ mode: 'sections',
264
+ sections: [
265
+ { id: 'general', label: 'General' },
266
+ { id: 'branding', label: 'Design & Card' },
267
+ { id: 'social', label: 'Social & Links' },
268
+ ],
269
+ },
270
+ fields: [
271
+ { key: 'name', type: 'text', label: 'Business Name', required: true, validators: { minLength: 2, maxLength: 120 }, section: 'general', width: 6 },
272
+ { key: 'slug', type: 'slug', from: 'name', label: 'Slug', required: true, lockAfterCreate: true, section: 'general', width: 6 },
273
+ { key: 'timezone', type: 'text', label: 'Timezone', required: true, help: 'e.g. America/Lima', section: 'general', width: 6 },
274
+ {
275
+ key: 'visitResetPolicy',
276
+ type: 'select',
277
+ label: 'Visit Reset Policy',
278
+ section: 'general',
279
+ width: 6,
280
+ options: [
281
+ { value: 'none', label: 'None' },
282
+ { value: 'monthly', label: 'Monthly' },
283
+ { value: 'yearly', label: 'Yearly' },
284
+ ],
285
+ },
286
+ { key: 'colorPrimary', type: 'text', label: 'Primary Color (HEX)', placeholder: '#4A2C11', section: 'branding', width: 6 },
287
+ { key: 'colorSecondary', type: 'text', label: 'Secondary Color (HEX)', placeholder: '#D4A373', section: 'branding', width: 6 },
288
+ { key: 'logoUrl', type: 'image', label: 'Logo', readonly: true, section: 'branding', width: 12 },
289
+ { key: 'googleReviewUrl', type: 'url', label: 'Google Reviews URL', section: 'social', width: 6 },
290
+ { key: 'instagramUrl', type: 'url', label: 'Instagram', section: 'social', width: 6 },
291
+ ],
292
+ };
293
+ ```
294
+
295
+ ---
296
+
297
+ ### Collection with full CRUD — `kind: 'collection'`
298
+
299
+ List + create + edit. Includes search, filters, and field validators.
300
+
301
+ ```ts
302
+ // src/app/schemas/ranks.schema.ts
303
+ import type { ResourceSchema } from '@tinya-code/gas-admin-ui';
304
+
305
+ export const ranksSchema: ResourceSchema = {
306
+ id: 'ranks',
307
+ label: 'Rank',
308
+ labelPlural: 'Ranks',
309
+ kind: 'collection',
310
+ uiViews: {
311
+ list: { type: 'list' },
312
+ create: { type: 'form', path: 'new' },
313
+ edit: { type: 'form', path: ':key/edit' },
314
+ },
315
+ endpoint: {
316
+ list: '/admin/businesses/current/ranks',
317
+ get: '/admin/businesses/current/ranks/{key}',
318
+ create: '/admin/businesses/current/ranks',
319
+ update: '/admin/businesses/current/ranks/{key}',
320
+ remove: '/admin/businesses/current/ranks/{key}',
321
+ },
322
+ keyField: 'id',
323
+ titleField: 'name',
324
+ search: ['name'],
325
+ filters: [
326
+ { key: 'includeInactive', label: 'Include Inactive', type: 'boolean' },
327
+ ],
328
+ listColumns: [
329
+ { key: 'level', label: 'Level' },
330
+ { key: 'name', label: 'Name' },
331
+ { key: 'minVisits', label: 'Min Visits' },
332
+ { key: 'benefitType', label: 'Benefit Type' },
333
+ { key: 'benefitValue',label: 'Value' },
334
+ { key: 'active', label: 'Active', format: 'boolean' },
335
+ ],
336
+ fields: [
337
+ { key: 'name', type: 'text', label: 'Rank Name', required: true, validators: { minLength: 1, maxLength: 80 } },
338
+ { key: 'level', type: 'number', label: 'Level', required: true, min: 1, help: 'Must be unique among active ranks' },
339
+ { key: 'minVisits',type: 'number', label: 'Minimum Visits', required: true, min: 0 },
340
+ {
341
+ key: 'benefitType',
342
+ type: 'select',
343
+ label: 'Benefit Type',
344
+ required: true,
345
+ options: [
346
+ { value: 'none', label: 'None' },
347
+ { value: 'percentage_discount', label: 'Percentage Discount' },
348
+ { value: 'fixed_discount', label: 'Fixed Discount' },
349
+ { value: 'free_item', label: 'Free Item' },
350
+ { value: 'priority', label: 'Priority Service' },
351
+ ],
352
+ },
353
+ { key: 'benefitValue', type: 'number', label: 'Benefit Value' },
354
+ { key: 'benefitDescription', type: 'textarea', label: 'Benefit Description', validators: { maxLength: 200 } },
355
+ { key: 'color', type: 'text', label: 'Color (HEX)', placeholder: '#CD7F32' },
356
+ { key: 'iconUrl', type: 'image', label: 'Icon', readonly: true },
357
+ { key: 'active', type: 'boolean', label: 'Active', readonlyOn: 'create' },
358
+ ],
359
+ };
360
+ ```
361
+
362
+ ---
363
+
364
+ ### Read-only collection with detail view
365
+
366
+ List + detail only — no create, no edit form.
367
+
368
+ ```ts
369
+ // src/app/schemas/clients.schema.ts
370
+ import type { ResourceSchema } from '@tinya-code/gas-admin-ui';
371
+
372
+ export const clientsSchema: ResourceSchema = {
373
+ id: 'clients',
374
+ label: 'Client',
375
+ labelPlural: 'Clients',
376
+ kind: 'collection',
377
+ uiViews: {
378
+ list: { type: 'list' },
379
+ detail: { type: 'detail', path: ':key' },
380
+ edit: { type: 'form', path: ':key/edit' },
381
+ },
382
+ endpoint: {
383
+ list: '/admin/businesses/current/clients',
384
+ get: '/admin/businesses/current/clients/{key}',
385
+ update: '/admin/businesses/current/clients/{key}/status',
386
+ // no create / remove → those actions won't appear in the UI
387
+ },
388
+ keyField: 'membershipId',
389
+ titleField: 'name',
390
+ search: ['name', 'phone'],
391
+ filters: [
392
+ { key: 'active', label: 'Active', type: 'boolean' },
393
+ ],
394
+ listColumns: [
395
+ { key: 'name', label: 'Name' },
396
+ { key: 'phone', label: 'Phone' },
397
+ { key: 'email', label: 'Email' },
398
+ { key: 'totalVisits', label: 'Total Visits' },
399
+ { key: 'lastVisitAt', label: 'Last Visit' },
400
+ { key: 'active', label: 'Active', format: 'boolean' },
401
+ { key: 'registeredAt',label: 'Registered' },
402
+ ],
403
+ detail: {
404
+ enabled: true,
405
+ headerFields: ['name', 'phone', 'active'],
406
+ },
407
+ fields: [
408
+ { key: 'membershipId', type: 'readonly-text', label: 'Membership ID', readonly: true },
409
+ { key: 'name', type: 'text', label: 'Name', readonly: true },
410
+ { key: 'phone', type: 'phone', label: 'Phone', readonly: true },
411
+ { key: 'email', type: 'email', label: 'Email', readonly: true },
412
+ { key: 'active', type: 'boolean', label: 'Active Membership' },
413
+ { key: 'totalVisits', type: 'number', label: 'Total Visits', readonly: true },
414
+ { key: 'currentPeriodVisits', type: 'number', label: 'Period Visits', readonly: true },
415
+ { key: 'registeredAt', type: 'date', label: 'Registered At', readonly: true },
416
+ { key: 'lastVisitAt', type: 'date', label: 'Last Visit', readonly: true },
417
+ ],
418
+ };
419
+ ```
103
420
 
104
- ### 8. Validá el contrato back ↔ front
421
+ ---
105
422
 
106
- Sólo después de tener el back con el doc al día (`npx gas-admin doc`):
423
+ ### Read-only collection with select filter
424
+
425
+ ```ts
426
+ // src/app/schemas/coupons.schema.ts
427
+ import type { ResourceSchema } from '@tinya-code/gas-admin-ui';
428
+
429
+ export const couponsSchema: ResourceSchema = {
430
+ id: 'coupons',
431
+ label: 'Coupon',
432
+ labelPlural: 'Coupons',
433
+ kind: 'collection',
434
+ uiViews: {
435
+ list: { type: 'list' },
436
+ detail: { type: 'detail', path: ':key' },
437
+ },
438
+ endpoint: {
439
+ list: '/admin/businesses/current/coupons',
440
+ get: '/admin/businesses/current/coupons/{key}',
441
+ },
442
+ keyField: 'id',
443
+ titleField: 'code',
444
+ search: ['code'],
445
+ filters: [
446
+ {
447
+ key: 'status',
448
+ label: 'Status',
449
+ type: 'select',
450
+ options: [
451
+ { value: 'issued', label: 'Issued' },
452
+ { value: 'used', label: 'Redeemed' },
453
+ { value: 'expired', label: 'Expired' },
454
+ ],
455
+ },
456
+ ],
457
+ listColumns: [
458
+ { key: 'code', label: 'Code' },
459
+ { key: 'status', label: 'Status' },
460
+ { key: 'issuedAt', label: 'Issued' },
461
+ { key: 'expiresAt',label: 'Expires' },
462
+ { key: 'usedAt', label: 'Redeemed' },
463
+ ],
464
+ detail: {
465
+ enabled: true,
466
+ headerFields: ['code', 'status', 'issuedAt'],
467
+ },
468
+ fields: [
469
+ { key: 'id', type: 'readonly-text', label: 'ID', readonly: true },
470
+ { key: 'code', type: 'text', label: 'Code', readonly: true },
471
+ { key: 'status', type: 'text', label: 'Status', readonly: true },
472
+ { key: 'issuedAt', type: 'date', label: 'Issued At', readonly: true },
473
+ { key: 'expiresAt', type: 'date', label: 'Expires At', readonly: true },
474
+ { key: 'usedAt', type: 'date', label: 'Redeemed At',readonly: true },
475
+ ],
476
+ };
477
+ ```
478
+
479
+ ---
480
+
481
+ ## Firebase Auth
482
+
483
+ The package supports federated authentication with Google and Firebase Email/Password via the decoupled `FirebaseAuthAdapter`. Firebase is declared as an optional `peerDependency` — the library does not bundle it.
484
+
485
+ ### 1. Install Firebase in the consumer app
107
486
 
108
487
  ```bash
109
- npx gas-admin validate --with-front
488
+ npm i firebase
489
+ ```
490
+
491
+ ### 2. Declare providers in `AuthSchema`
492
+
493
+ ```ts
494
+ // src/app/schemas/auth.schema.ts
495
+ import type { AuthSchema } from '@tinya-code/gas-admin-ui';
496
+
497
+ // Option A — password only (default, no Firebase required)
498
+ export const authSchema: AuthSchema = {
499
+ providers: ['password'],
500
+ loginFields: [
501
+ { key: 'email', type: 'email', required: true },
502
+ { key: 'password', type: 'password', required: true },
503
+ ],
504
+ redirectAfterLogin: '/dashboard',
505
+ rememberMe: false,
506
+ };
507
+
508
+ // Option B — Google only
509
+ export const authSchema: AuthSchema = {
510
+ providers: ['google'],
511
+ loginFields: [],
512
+ redirectAfterLogin: '/dashboard',
513
+ rememberMe: false,
514
+ firebase: { exchangeEndpoint: '/auth/firebase' },
515
+ };
516
+
517
+ // Option C — Google + Password (both)
518
+ export const authSchema: AuthSchema = {
519
+ providers: ['google', 'password'],
520
+ loginFields: [
521
+ { key: 'email', type: 'email', required: true },
522
+ { key: 'password', type: 'password', required: true },
523
+ ],
524
+ redirectAfterLogin: '/dashboard',
525
+ rememberMe: false,
526
+ firebase: { exchangeEndpoint: '/auth/firebase' },
527
+ };
110
528
  ```
111
529
 
112
- Corre el `contract-check` de `@tinya-code/gas-admin`: exit 0 = los schemas del
113
- front siguen coincidiendo con la proyección del back.
530
+ ### 3. Implement the adapter and wire it in `app.config.ts`
114
531
 
115
- ## Requisitos especiales del consumidor
532
+ ```ts
533
+ // src/app/app.config.ts
534
+ import { initializeApp } from 'firebase/app';
535
+ import {
536
+ getAuth,
537
+ GoogleAuthProvider,
538
+ signInWithPopup,
539
+ signInWithEmailAndPassword,
540
+ signOut,
541
+ } from 'firebase/auth';
542
+ import {
543
+ API_URL,
544
+ provideGasAdmin,
545
+ provideFirebaseAdapter,
546
+ } from '@tinya-code/gas-admin-ui';
547
+ import type { FirebaseAuthAdapter } from '@tinya-code/gas-admin-ui';
548
+ import { environment } from '../environments/environment';
549
+
550
+ const firebaseApp = initializeApp(environment.firebase);
551
+ const firebaseAuth = getAuth(firebaseApp);
552
+
553
+ class AppFirebaseAuthAdapter implements FirebaseAuthAdapter {
554
+ async signInWithGoogle(): Promise<string> {
555
+ const cred = await signInWithPopup(firebaseAuth, new GoogleAuthProvider());
556
+ return cred.user.getIdToken();
557
+ }
558
+ async signInWithEmailAndPassword(email: string, password: string): Promise<string> {
559
+ const cred = await signInWithEmailAndPassword(firebaseAuth, email, password);
560
+ return cred.user.getIdToken();
561
+ }
562
+ async signOut(): Promise<void> {
563
+ await signOut(firebaseAuth);
564
+ }
565
+ }
116
566
 
117
- ### Tailwind v4 — `@source` obligatorio
567
+ export const appConfig: ApplicationConfig = {
568
+ providers: [
569
+ provideRouter(routes),
570
+ provideGasAdmin({ schemas, auth: authSchema }),
571
+ provideFirebaseAdapter(new AppFirebaseAuthAdapter()), // ← add this
572
+ { provide: API_URL, useValue: environment.apiUrl },
573
+ ],
574
+ };
575
+ ```
576
+
577
+ ### 4. Backend endpoint — `POST /auth/firebase`
578
+
579
+ The library sends the Firebase ID token using the **same envelope transport** as every other request. Your backend must:
580
+
581
+ 1. Receive `{ idToken: string }` in the payload.
582
+ 2. Verify it with the Firebase Admin SDK (`getAuth().verifyIdToken(idToken)`).
583
+ 3. Return `{ token, user }` — identical shape to `/auth/login`.
584
+
585
+ ```json
586
+ // Request body (Content-Type: text/plain)
587
+ { "token": null, "method": "POST", "path": "/auth/firebase", "payload": { "idToken": "..." } }
588
+
589
+ // Response
590
+ { "token": "<your-app-jwt>", "user": { "id": "...", "email": "...", "name": "...", "role": "..." }, "meta": {} }
591
+ ```
592
+
593
+ For a full consumer integration guide including a backend handler example, environment config, and a pre-production checklist, see [`doc/firebase-consumer-example.md`](../../doc/firebase-consumer-example.md).
594
+
595
+ ---
596
+
597
+ ## Consumer requirements
118
598
 
119
- Tailwind genera las utilities escaneando **tus** fuentes; el motor vive en
120
- `node_modules` y Tailwind no lo escanea solo. El `@source` del paso 2 es
121
- obligatorio: sin él la app compila y los tests quedan en verde, pero la UI
122
- del motor se renderiza sin estilos.
599
+ ### Tailwind v4 — mandatory `@source`
123
600
 
124
- ### Tests del consumidor — desde `node_modules` (sin alias)
601
+ Tailwind generates utilities by scanning **your** source files. The engine lives in `node_modules` and is not scanned by default. The `@source` directive in `styles.css` (step 2) is required — without it the app compiles and tests pass green, but the engine UI renders without any styles.
125
602
 
126
- Los specs del consumidor deben resolver `@tinya-code/gas-admin-ui` desde
127
- `node_modules` (la versión publicada en `dependencies`), **sin** alias de
128
- `tsconfig.spec.json` ni clone del repo como directorio hermano en CI: la app
129
- y los tests usan la MISMA copia compilada ⇒ Angular se instancia una sola
130
- vez (verificado en `admin-schema`: 116/116 sin alias). Para probar un cambio
131
- del motor, publicá un alpha y bumpeá la dependencia.
603
+ ### Tests — resolve from `node_modules` (no alias)
132
604
 
133
- Si alguna combinación de versiones llegara a reproducir `NG0203`/`NG01908`
134
- (doble instancia de Angular: vitest prebundlea `@angular/*` pero la lib corre
135
- en ESM nativo), el workaround es compilar la lib desde source en el grafo de
136
- tests (`paths` en `tsconfig.spec.json` apuntando al source del paquete)
137
- junto con un clone del repo como directorio hermano en CI.
605
+ Consumer specs must resolve `@tinya-code/gas-admin-ui` from `node_modules` (the published, compiled version), without `tsconfig.spec.json` aliases. This guarantees Angular is instantiated exactly once. To test a library change, publish an alpha and bump the dependency.
138
606
 
139
- ## Estructura generada en `src/app/schemas/`
607
+ ---
140
608
 
141
- `npx gas-admin scaffold` aplaná la plantilla de las vitrinas (catálogo,
142
- agenda, panel) en una carpeta plana:
609
+ ## Generated schema structure (`npx gas-admin scaffold`)
143
610
 
144
611
  ```text
145
612
  src/app/schemas/
146
- ├── auth.schema.ts # claves exactas del input de /auth/login
147
- ├── registry.ts # array ordenado que alimenta menú y rutas
148
- ├── seed-catalog.ts # siembra el catálogo para los specs
149
- ├── productos.schema.ts # vitrina catálogo
150
- ├── categorias.schema.ts # vitrina catálogo
151
- ├── pedidos.schema.ts # vitrina agenda
152
- ├── dashboard.schema.ts # vitrina panel
153
- ├── usuarios.schema.ts # vitrina panel
154
- └── sitio.schema.ts # vitrina panel
613
+ ├── auth.schema.ts # auth provider and login fields
614
+ ├── registry.ts # ordered array that feeds the menu and routes
615
+ ├── seed-catalog.ts # seeds the catalog for specs
616
+ ├── dashboard.schema.ts # kind: dashboard
617
+ ├── business.schema.ts # kind: singleton
618
+ ├── clients.schema.ts # kind: collection (read-only with detail)
619
+ ├── ranks.schema.ts # kind: collection (full CRUD)
620
+ ├── rewards.schema.ts # kind: collection (full CRUD)
621
+ ├── coupons.schema.ts # kind: collection (read-only with filters)
622
+ └── feedback.schema.ts # kind: collection (read-only with search)
155
623
  ```
156
624
 
157
- Por cada recurso, el `*.schema.ts` trae campos, validators, enums y endpoint
158
- derivados de la proyección back.
625
+ `scaffold` fails with exit 1 if the target folder already contains `*.ts` files — use `--force` to regenerate.
159
626
 
160
- ## API pública
627
+ ---
161
628
 
162
- Sólo lo exportado por `public-api.ts` es contrato; el resto es privado y
163
- puede cambiar entre alphas.
629
+ ## Public API
164
630
 
165
- | Grupo | Exportaciones |
631
+ Only what `public-api.ts` exports is stable contract; everything else is private and may change between alphas.
632
+
633
+ | Group | Exports |
166
634
  | --- | --- |
167
- | Wiring y catálogo | `provideGasAdmin`, `GAS_SCHEMAS`, `setSchemaCatalog`, `getSchema`, `allSchemas`, `getAuth`, tipo `GasAdminConfig` |
168
- | Token | `API_URL` |
169
- | Rutas y guard | `authGuard`, `shellRoutes`, `resourceRoutes` |
170
- | Páginas | `Login`, `Dashboard`, `ListView`, `FormView`, `ResourcePage` |
171
- | Componentes | `Sidebar`, `ComposerHost`, `FieldHost`, `ConfirmDialog`, `Toast` |
172
- | Servicios | `ApiService`, `AuthService` (+ `SESSION_TOKEN_KEY`, `SESSION_USER_KEY`), `NotificationService`, `DataSourceService`, `UploadService`, `ErrorMapperService`, `apiErrorInterceptor` |
635
+ | Wiring & catalog | `provideGasAdmin`, `provideFirebaseAdapter`, `GAS_SCHEMAS`, `setSchemaCatalog`, `getSchema`, `allSchemas`, `getAuth`, type `GasAdminConfig` |
636
+ | Tokens & adapters | `API_URL`, `API_DRIVER`, `FIREBASE_AUTH_ADAPTER`, type `FirebaseAuthAdapter` |
637
+ | Routes & guard | `authGuard`, `shellRoutes`, `resourceRoutes` |
638
+ | Pages | `Login`, `Dashboard`, `ListView`, `FormView`, `ResourcePage` |
639
+ | Components | `Sidebar`, `ComposerHost`, `FieldHost`, `ConfirmDialog`, `Toast` |
640
+ | Services | `ApiService`, `AuthService` (+ `SESSION_TOKEN_KEY`, `SESSION_USER_KEY`), `NotificationService`, `DataSourceService`, `UploadService`, `ErrorMapperService`, `apiErrorInterceptor` |
173
641
  | Forms | `buildFormSchema`, `seedModel`, `buildPayload` |
174
- | Modelos de schema | `ResourceSchema`, `AuthSchema`, `ResourceAction`, `ApiError` y todo el vocabulario de fields/widgets/views/layouts (`FieldSchema`, `FieldType`, `ListColumn`, `WidgetSchema`, `ResourceLayout`, `ViewDescriptor`, vistas tabs/calendar/kanban…) |
175
- | Registros de render | `fieldDescriptors`, `SUPPORTED_FIELD_TYPE_LIST`, `columnFormatDescriptors`, `COLUMN_FORMAT_TYPES`, `viewDescriptors`, `layoutDescriptors`, `renderKinds` |
176
- | Render custom | `registerRenderDescriptor`, `unregisterRenderDescriptor`, `provideRenderDescriptors`, `getRenderDescriptor`, `isSupported`, `isRenderSupported`, tipos `RenderKind`, `ComponentRenderKind` |
177
- | Composición | `validateComposition`, `MAX_DEPTH`, `SchemaCompositionError` |
178
- | Acciones custom | `registerAction`, `getAction`, `actionsFor`, `actionType`, `actionTypes`, tipos `ActionDescriptor`, `ActionSurface`, `ActionContext`, `ActionResult` |
179
- | Validadores | tipo `ValidatorName` (vocabulario del contract-check back ↔ front) |
180
- | Utilidades | `interpolateTemplate` (interpolación de templates en schemas) |
642
+ | Schema models | `ResourceSchema`, `AuthSchema`, `AuthProvider`, `FirebaseAuthConfig`, `ResourceAction`, `ApiError` and the full field/widget/view/layout vocabulary |
643
+ | Render registries | `fieldDescriptors`, `columnFormatDescriptors`, `viewDescriptors`, `layoutDescriptors`, `renderKinds` |
644
+ | Custom render | `registerRenderDescriptor`, `unregisterRenderDescriptor`, `provideRenderDescriptors`, `getRenderDescriptor`, `isSupported`, `isRenderSupported`, types `RenderKind`, `ComponentRenderKind` |
645
+ | Composition | `validateComposition`, `MAX_DEPTH`, `SchemaCompositionError` |
646
+ | Custom actions | `registerAction`, `getAction`, `actionsFor`, `actionType`, `actionTypes`, types `ActionDescriptor`, `ActionSurface`, `ActionContext`, `ActionResult` |
647
+ | Validators | type `ValidatorName` |
648
+ | Utilities | `interpolateTemplate` |
649
+
650
+ ---
651
+
652
+ ## See also
181
653
 
182
- ## Ver también
654
+ - **Backend** — domain declaration, `doc`, `validate`:
655
+ [`@tinya-code/gas-admin`](https://github.com/Tinya-Code/gas-admin) → `docs/guia-de-uso.md`.
656
+ - **Firebase consumer guide** — full wiring, backend contract, pre-production checklist:
657
+ [`doc/firebase-consumer-example.md`](../../doc/firebase-consumer-example.md).
183
658
 
184
- - **Back del ecosistema** — `init`, declaración de dominio, `doc`, `validate`:
185
- guía de `@tinya-code/gas-admin` en
186
- [`Tinya-Code/gas-admin`](https://github.com/Tinya-Code/gas-admin)
187
- (`docs/guia-de-uso.md`).
188
- - **Consumidor de referencia** — `admin-schema`: declara el dominio e integra
189
- este paquete (116 tests sin alias, specs de integración).
659
+ ---
190
660
 
191
- ## Desarrollo (este repo)
661
+ ## Development (this repo)
192
662
 
193
663
  ```bash
194
- ng build gas-admin-ui # ng-packagr → dist/gas-admin-ui (fesm2022 + types)
195
- ng test gas-admin-ui # vitest: tests del motor (fixtures congelados)
664
+ ng build gas-admin-ui # ng-packagr → dist/gas-admin-ui
665
+ ng test gas-admin-ui # vitest: engine tests
196
666
  ```
197
667
 
198
- Publicación (npm **exige** `--tag` explícito en prereleases):
668
+ Publish (npm requires explicit `--tag` for pre-releases):
199
669
 
200
670
  ```bash
201
671
  npm publish ./dist/gas-admin-ui --tag alpha
202
672
  ```
203
673
 
204
- - `dist/` es gitignored: el build es siempre local y no se commitea.
205
- - Los specs que renderizan los schemas **reales** de un consumidor no viven
206
- acá (la lib no puede importar al consumidor): viven en el repo del
207
- consumidor importando desde este paquete.
674
+ - `dist/` is gitignored — the build is always local and never committed.
675
+ - Specs that render a consumer's real schemas live in the consumer repo, not here.