@tinya-code/gas-admin-ui 0.2.0-alpha.0 → 0.2.0-alpha.10

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 (48) hide show
  1. package/README.md +347 -2
  2. package/fesm2022/{tinya-code-gas-admin-ui-calendar-view-FfMmRiix.mjs → tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs} +12 -7
  3. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs.map +1 -0
  4. package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs +107 -0
  5. package/fesm2022/tinya-code-gas-admin-ui-color-Dljh_By3.mjs.map +1 -0
  6. package/fesm2022/{tinya-code-gas-admin-ui-condition-evaluator-B1it00js.mjs → tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs} +2 -2
  7. package/fesm2022/{tinya-code-gas-admin-ui-condition-evaluator-B1it00js.mjs.map → tinya-code-gas-admin-ui-condition-evaluator-DY2k1M3C.mjs.map} +1 -1
  8. package/fesm2022/{tinya-code-gas-admin-ui-data-source.service-CvL9YHyu.mjs → tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs} +2 -2
  9. package/fesm2022/{tinya-code-gas-admin-ui-data-source.service-CvL9YHyu.mjs.map → tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs.map} +1 -1
  10. package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs +122 -0
  11. package/fesm2022/tinya-code-gas-admin-ui-dropdown-C3blvmE1.mjs.map +1 -0
  12. package/fesm2022/{tinya-code-gas-admin-ui-error-mapper.service-CFYzwAbD.mjs → tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs} +2 -2
  13. package/fesm2022/{tinya-code-gas-admin-ui-error-mapper.service-CFYzwAbD.mjs.map → tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs.map} +1 -1
  14. package/fesm2022/{tinya-code-gas-admin-ui-file-Cc2DUvgR.mjs → tinya-code-gas-admin-ui-file-Dmxioog7.mjs} +3 -3
  15. package/fesm2022/{tinya-code-gas-admin-ui-file-Cc2DUvgR.mjs.map → tinya-code-gas-admin-ui-file-Dmxioog7.mjs.map} +1 -1
  16. package/fesm2022/{tinya-code-gas-admin-ui-form-view-Dyp3oWnC.mjs → tinya-code-gas-admin-ui-form-view-C709rc57.mjs} +13 -10
  17. package/fesm2022/tinya-code-gas-admin-ui-form-view-C709rc57.mjs.map +1 -0
  18. package/fesm2022/{tinya-code-gas-admin-ui-image-BlDRZe-F.mjs → tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs} +3 -3
  19. package/fesm2022/{tinya-code-gas-admin-ui-image-BlDRZe-F.mjs.map → tinya-code-gas-admin-ui-image-7UhJ7nqI.mjs.map} +1 -1
  20. package/fesm2022/{tinya-code-gas-admin-ui-kanban-view-zlQLxOlJ.mjs → tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs} +4 -4
  21. package/fesm2022/{tinya-code-gas-admin-ui-kanban-view-zlQLxOlJ.mjs.map → tinya-code-gas-admin-ui-kanban-view-Dl0Ol5Kb.mjs.map} +1 -1
  22. package/fesm2022/{tinya-code-gas-admin-ui-list-D6Z4fVTb.mjs → tinya-code-gas-admin-ui-list-C8ijJHAx.mjs} +2 -2
  23. package/fesm2022/{tinya-code-gas-admin-ui-list-D6Z4fVTb.mjs.map → tinya-code-gas-admin-ui-list-C8ijJHAx.mjs.map} +1 -1
  24. package/fesm2022/{tinya-code-gas-admin-ui-multiselect-fNpo9uMF.mjs → tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs} +3 -3
  25. package/fesm2022/{tinya-code-gas-admin-ui-multiselect-fNpo9uMF.mjs.map → tinya-code-gas-admin-ui-multiselect-PB28e-jl.mjs.map} +1 -1
  26. package/fesm2022/{tinya-code-gas-admin-ui-relation-BSqbQB28.mjs → tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs} +3 -3
  27. package/fesm2022/{tinya-code-gas-admin-ui-relation-BSqbQB28.mjs.map → tinya-code-gas-admin-ui-relation-ykpJ3PYl.mjs.map} +1 -1
  28. package/fesm2022/{tinya-code-gas-admin-ui-render-registry-CIOMJ68Y.mjs → tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs} +59 -29
  29. package/fesm2022/tinya-code-gas-admin-ui-render-registry-CULkxrSt.mjs.map +1 -0
  30. package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs +161 -0
  31. package/fesm2022/tinya-code-gas-admin-ui-rich-select-CIHaOrbU.mjs.map +1 -0
  32. package/fesm2022/{tinya-code-gas-admin-ui-schema-catalog-C9BqNPt8.mjs → tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs} +276 -26
  33. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs.map +1 -0
  34. package/fesm2022/{tinya-code-gas-admin-ui-seed-DD6zUd4v.mjs → tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs} +2 -2
  35. package/fesm2022/{tinya-code-gas-admin-ui-seed-DD6zUd4v.mjs.map → tinya-code-gas-admin-ui-seed-CcCA3FdQ.mjs.map} +1 -1
  36. package/fesm2022/{tinya-code-gas-admin-ui-select-BUbb36-T.mjs → tinya-code-gas-admin-ui-select-CDwX9yNO.mjs} +57 -53
  37. package/fesm2022/tinya-code-gas-admin-ui-select-CDwX9yNO.mjs.map +1 -0
  38. package/fesm2022/{tinya-code-gas-admin-ui-upload.service-BWZkVOBg.mjs → tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs} +2 -2
  39. package/fesm2022/{tinya-code-gas-admin-ui-upload.service-BWZkVOBg.mjs.map → tinya-code-gas-admin-ui-upload.service-CZPEPCeN.mjs.map} +1 -1
  40. package/fesm2022/tinya-code-gas-admin-ui.mjs +98 -46
  41. package/fesm2022/tinya-code-gas-admin-ui.mjs.map +1 -1
  42. package/package.json +1 -1
  43. package/types/tinya-code-gas-admin-ui.d.ts +397 -58
  44. package/fesm2022/tinya-code-gas-admin-ui-calendar-view-FfMmRiix.mjs.map +0 -1
  45. package/fesm2022/tinya-code-gas-admin-ui-form-view-Dyp3oWnC.mjs.map +0 -1
  46. package/fesm2022/tinya-code-gas-admin-ui-render-registry-CIOMJ68Y.mjs.map +0 -1
  47. package/fesm2022/tinya-code-gas-admin-ui-schema-catalog-C9BqNPt8.mjs.map +0 -1
  48. package/fesm2022/tinya-code-gas-admin-ui-select-BUbb36-T.mjs.map +0 -1
package/README.md CHANGED
@@ -69,6 +69,167 @@ export const environment = {
69
69
  };
70
70
  ```
71
71
 
72
+ ### REST Driver (NestJS / standard REST)
73
+
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 }`.
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
80
+ export const appConfig: ApplicationConfig = {
81
+ providers: [
82
+ provideBrowserGlobalErrorListeners(),
83
+ provideRouter(routes),
84
+ provideGasAdmin({
85
+ schemas,
86
+ auth: authSchema,
87
+ driver: 'rest', // <-- Enables standard REST transport
88
+ }),
89
+ { provide: API_URL, useValue: environment.apiUrl },
90
+ ],
91
+ };
92
+ ```
93
+
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
106
+
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
+ - **Parameterized paths (context markers)**: the engine resolves markers such as `{auth.user.id}` client-side *before* sending, so the backend just declares the real route — e.g. `GET /api/v1/businesses/:idBusiness/orders` matches an endpoint declared as `/businesses/{auth.user.id}/orders`. See **Dynamic Values in Endpoint Paths** below.
117
+
118
+ `driver` defaults to `'gas'` if omitted. Existing Apps Script projects do not require any configuration changes.
119
+
120
+ ### Dynamic Values in Endpoint Paths (context sources)
121
+
122
+ Standard REST backends (NestJS, Express, FastAPI) often route by an id in the path — `/businesses/:idBusiness/orders`. Declare the value as a **context marker** in the endpoint and the engine resolves it right before the request leaves. It works for both drivers; endpoints without markers are untouched.
123
+
124
+ #### Marker syntax
125
+
126
+ | Marker | Resolves from | Example |
127
+ | --- | --- | --- |
128
+ | `{key}` | The record key of the current detail/edit operation (unchanged behavior) | `/orders/{key}` → `/orders/ord-7` |
129
+ | `{auth.<path>}` | The session: the `user` payload and `token` your backend returned at login | `{auth.user.id}` → `u_1` |
130
+ | `{config.<path>}` | The global config map, merged from your `singleton` schemas at bootstrap | `{config.public_base_url}` → `https://…` |
131
+ | `{<source>}` / `{<source>.<path>}` | Any source you register through `valueSources` | `{tenant}` → `acme` |
132
+
133
+ Every substituted value is URL-encoded per segment, exactly like `{key}`. Nested paths work (`{auth.user.id}`), and markers can be combined in one path and mixed with `{key}`.
134
+
135
+ #### Built-in sources
136
+
137
+ - **`auth`** — the session held by `AuthService`. Any field your backend includes in the login / `/auth/me` payload is reachable: the library stores the whole `user` object, so nothing new has to be wired for a field your backend already sends.
138
+ - **`config`** — the merged response of every `singleton` schema loaded at bootstrap.
139
+
140
+ #### Register your own source
141
+
142
+ ```ts
143
+ // src/app/app.config.ts
144
+ import { provideGasAdmin } from '@tinya-code/gas-admin-ui';
145
+
146
+ provideGasAdmin({
147
+ schemas,
148
+ auth: authSchema,
149
+ driver: 'rest',
150
+ valueSources: {
151
+ // Name → getter evaluated at request time (not at registration).
152
+ tenant: () => myStore.currentTenantId(),
153
+ },
154
+ });
155
+ ```
156
+
157
+ Then any endpoint can use it:
158
+
159
+ ```ts
160
+ // schemas/orders.schema.ts
161
+ endpoint: {
162
+ list: '/tenants/{tenant}/businesses/{auth.user.id}/orders',
163
+ get: '/tenants/{tenant}/businesses/{auth.user.id}/orders/{key}',
164
+ update: '/tenants/{tenant}/businesses/{auth.user.id}/orders/{key}',
165
+ }
166
+ ```
167
+
168
+ #### Fail loud, never half-filled
169
+
170
+ A marker that cannot resolve surfaces as an error in the request's **error channel** — the same place any request failure already lands. The engine never sends a URL with an empty segment or with the literal marker text.
171
+
172
+ | Situation | Result |
173
+ | --- | --- |
174
+ | Source not registered | Error naming the path and the marker; **no request is sent** |
175
+ | Session/config field missing or empty | Error naming the path and the marker; **no request is sent** |
176
+ | A registered getter throws | Its error propagates as-is |
177
+ | `{key}` left unresolved (`list`, `create`) | Error explaining that `key` applies to `get`/`update`/`remove` only |
178
+
179
+ #### Notes
180
+
181
+ - Resolution covers every operation — `list`, `get`, `create`, `update`, `remove` and custom `request` — and both drivers.
182
+ - Built-in sources win: `valueSources` cannot shadow `auth` or `config`.
183
+ - Nothing changes for existing schemas: a path without markers produces a byte-identical request.
184
+
185
+ ### Firebase ID Token Refresh (REST driver)
186
+
187
+ Firebase ID tokens expire after **1 hour**. When `driver: 'rest'` is active, `gas-admin-ui` calls your adapter's optional `getIdToken()` before every REST request so the `Authorization: Bearer <token>` header always carries a fresh token.
188
+
189
+ ```ts
190
+ // src/app/auth/firebase-auth.adapter.ts
191
+ import type { FirebaseAuthAdapter } from '@tinya-code/gas-admin-ui';
192
+ import { getAuth } from 'firebase/auth';
193
+
194
+ export class AppFirebaseAuthAdapter implements FirebaseAuthAdapter {
195
+ private readonly auth = getAuth();
196
+
197
+ async signInWithGoogle(): Promise<string> { /* ... */ }
198
+ async signInWithEmailAndPassword(email: string, password: string): Promise<string> { /* ... */ }
199
+ async signOut(): Promise<void> { /* ... */ }
200
+
201
+ /** Returns a fresh Firebase ID token before each REST request. */
202
+ async getIdToken(): Promise<string> {
203
+ const user = this.auth.currentUser;
204
+ if (!user) throw new Error('No Firebase user signed in');
205
+ return user.getIdToken(true); // force refresh if expired or near-expiry
206
+ }
207
+ }
208
+ ```
209
+
210
+ ```ts
211
+ // src/app/app.config.ts
212
+ import { provideFirebaseAdapter } from '@tinya-code/gas-admin-ui';
213
+ import { AppFirebaseAuthAdapter } from './auth/firebase-auth.adapter';
214
+
215
+ export const appConfig = {
216
+ providers: [
217
+ // ...
218
+ provideFirebaseAdapter(new AppFirebaseAuthAdapter()),
219
+ ],
220
+ };
221
+ ```
222
+
223
+ **Behavior:**
224
+
225
+ | Adapter implements `getIdToken()`? | Result |
226
+ |---|---|
227
+ | Yes | Called before every REST request; returned value used as `Authorization: Bearer <token>` |
228
+ | No | Current stored session token is used (unchanged behavior) |
229
+ | `getIdToken()` throws | Error surfaces in the request's error channel; **no request is sent** |
230
+
231
+ This only affects `driver: 'rest'`. The GAS driver continues sending the session token inside the envelope exactly as today.
232
+
72
233
  ### 4. Declare routes — `app.routes.ts`
73
234
 
74
235
  ```ts
@@ -577,6 +738,190 @@ src/app/schemas/
577
738
 
578
739
  `scaffold` fails with exit 1 if the target folder already contains `*.ts` files — use `--force` to regenerate.
579
740
 
741
+
742
+
743
+ ## Response unwrapping and custom endpoints (engine flexibility)
744
+
745
+ ### Response unwrapping (opt-in)
746
+
747
+ By default the engine does not assume any envelope: a `list` accepts a plain array or `{ items, ... }`, and `get`/`create`/`update` return the raw body. To unwrap a nested envelope (e.g. `{ data, meta }`), declare `responseUnwrap` on the resource's `endpoint`:
748
+
749
+ ```ts
750
+ endpoint: {
751
+ list: '/admin/products',
752
+ get: '/admin/products/{key}',
753
+ responseUnwrap: { dataKey: 'data', metaKey: 'meta' }, // applies to all ops by default
754
+ },
755
+ ```
756
+
757
+ - `dataKey`: key holding the real payload (`data`, `items`, `result`, …).
758
+ - `metaKey`: optional key with metadata (pagination, totals). Read for renewed tokens; not returned to the consumer.
759
+ - `apply`: optional `'list' | 'get' | 'create' | 'update' | 'remove' | 'all'` (default `'all'`).
760
+
761
+ Without the declaration, behavior is unchanged.
762
+
763
+ ### Custom endpoints
764
+
765
+ Declare extra endpoints beyond the five standard CRUD operations on the resource:
766
+
767
+ ```ts
768
+ import type { ResourceSchema } from '@tinya-code/gas-admin-ui';
769
+
770
+ export const productsSchema: ResourceSchema = {
771
+ // ...
772
+ customEndpoints: {
773
+ recalcRanks: {
774
+ name: 'recalcRanks',
775
+ method: 'POST',
776
+ path: '/admin/products/{key}/ranks/recalculate',
777
+ payloadMode: 'json', // default
778
+ },
779
+ uploadImage: {
780
+ name: 'uploadImage',
781
+ method: 'POST',
782
+ path: '/admin/products/{key}/image',
783
+ payloadMode: 'multipart',
784
+ },
785
+ },
786
+ };
787
+ ```
788
+
789
+ Invoke them programmatically through `ApiService`:
790
+
791
+ ```ts
792
+ const api = inject(ApiService);
793
+ api.invokeCustomEndpoint<{ ok: boolean }>(schema.customEndpoints!.recalcRanks, { value: 5 }, undefined, 'prod-1')
794
+ .subscribe((res) => console.log(res));
795
+
796
+ const fd = new FormData();
797
+ fd.append('file', file);
798
+ api.invokeCustomEndpoint<{ url: string }>(schema.customEndpoints!.uploadImage, fd, undefined, 'prod-1')
799
+ .subscribe((res) => console.log(res.url));
800
+ ```
801
+
802
+ - Any HTTP method (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`) is supported.
803
+ - `payloadMode: 'multipart'` (or passing a `FormData`) sends the body without `Content-Type: application/json`, so the browser sets the multipart boundary.
804
+ - A per-endpoint `responseUnwrap` overrides the resource-level one.
805
+
806
+ ### Global default unwrap
807
+ When every resource wraps responses the same way, declare it once in `provideGasAdmin` instead of repeating `responseUnwrap` per resource:
808
+
809
+ ```ts
810
+ provideGasAdmin({
811
+ schemas,
812
+ defaultResponseUnwrap: { dataKey: 'data', metaKey: 'meta' },
813
+ });
814
+ ```
815
+
816
+ Merge rule: the per-resource declaration wins on every key it declares; the global default fills in the rest. Resources that declare nothing inherit the global default. No `defaultResponseUnwrap` ⇒ only per-resource declarations apply (unchanged behavior).
817
+
818
+ ```ts
819
+ // global: { dataKey: 'payload' }
820
+ // resource:{ dataKey: 'data' } ⇒ effective: { dataKey: 'data' } (resource wins)
821
+ ```
822
+
823
+ ### Pagination — `listPage()`
824
+
825
+ `list()` still returns `T[]` and discards the cursor. To read the pagination metadata the backend already sends (`{ items, nextCursor, total }` or inside `meta`), opt in with `listPage()`:
826
+
827
+ ```ts
828
+ const api = inject(ApiService);
829
+
830
+ // First page
831
+ const page = await firstValueFrom(api.listPage<Product>(productsSchema.endpoint));
832
+ page.items; // Product[]
833
+ page.nextCursor; // string | null (null on the last page)
834
+ page.total; // number (0 when the backend omits it)
835
+ page.hasMore; // boolean
836
+
837
+ // Next page: pass the cursor back as a normal payload
838
+ const next = await firstValueFrom(
839
+ api.listPage<Product>(productsSchema.endpoint, { cursor: page.nextCursor, limit: 20 }),
840
+ );
841
+ ```
842
+
843
+ | Field | Source | Fallback |
844
+ |-------|--------|----------|
845
+ | `items` | Same unwrapping rules as `list()` (`dataKey` or `{ items }`) | — |
846
+ | `nextCursor` | `pagination.cursorKey` or `meta.nextCursor` | auto-detect `nextCursor`; empty/absent ⇒ `null` |
847
+ | `total` | `pagination.totalKey` or `meta.total` | auto-detect `total`; absent ⇒ `0` |
848
+ | `hasMore` | `pagination.hasMoreKey` (boolean wins) | derived: cursor present ⇒ `true`, else `false` |
849
+
850
+ Custom keys are declared on the resource's unwrap:
851
+
852
+ ```ts
853
+ endpoint: {
854
+ list: '/admin/products',
855
+ responseUnwrap: {
856
+ dataKey: 'results',
857
+ metaKey: 'meta',
858
+ pagination: { cursorKey: 'after', totalKey: 'count', hasMoreKey: 'remaining' },
859
+ },
860
+ },
861
+ ```
862
+
863
+ Rules:
864
+
865
+ - **Opt-in.** Without `listPage()`, nothing changes: `list()` keeps its `T[]` contract.
866
+ - **Auto-detection.** When no keys are declared, the engine reads the common ones (`nextCursor`, `total`, `hasMore`) from `meta` (if `metaKey` is declared and present) or from the body root.
867
+ - **`hasMore` derivation.** An explicit boolean flag wins; otherwise the cursor's presence decides it. No cursor ⇒ `hasMore: false`.
868
+ - **Payloads.** `cursor`/`limit` (or any pagination params) travel as the normal request payload — query params on REST `GET`, `payload` field in the GAS envelope.
869
+
870
+ ### Migration notes
871
+
872
+ Existing schemas keep working unchanged: no `responseUnwrap`, no `customEndpoints`, no `defaultResponseUnwrap` and no `listPage()` means the engine behaves exactly as before.
873
+
874
+ One deliberate change: a `list` whose body is neither an array nor an object (e.g. `null` or a plain string) now **throws a descriptive error** instead of silently returning `[]`. An object without recognized keys still returns `[]` (unchanged), so only a broken list contract surfaces.
875
+
876
+
877
+ ### Read-only resources
878
+
879
+ A resource that only displays data should not offer create/edit/delete. Declare
880
+ it as read-only in **either** of these ways — the engine honors both:
881
+
882
+ 1. **Declare `permissions` (explicit, legacy-friendly):**
883
+ ```ts
884
+ export const dashboardSchema: ResourceSchema = {
885
+ // ...
886
+ permissions: { create: false, update: false, remove: false },
887
+ };
888
+ ```
889
+ 2. **Omit the write endpoints (inferred):** if the schema declares an
890
+ `endpoint` with at least one entry but no `create`/`update`/`remove`, the
891
+ engine treats those operations as unavailable — the "Create" button, the
892
+ edit form and the delete action are hidden, and boolean columns render as
893
+ badges instead of editable toggles. A schema with no `endpoint` at all keeps
894
+ the legacy optimistic behavior (buttons shown, backend decides).
895
+
896
+ ```ts
897
+ endpoint: {
898
+ get: '/admin/dashboard', // only read — no create/update/remove
899
+ },
900
+ ```
901
+
902
+ This is what makes a dashboard or a report resource read-only without
903
+ repeating `permissions`.
904
+
905
+ ### Responsive widget layout (`mobileWidth`)
906
+
907
+ Dashboard widgets use a 12-column grid. By default every widget spans the
908
+ full width on mobile (stacked) and `width` columns from the `sm` breakpoint
909
+ up. To control the mobile layout declaratively, set `mobileWidth` (1–12):
910
+
911
+ ```ts
912
+ widgets: [
913
+ { type: 'metric-card', key: 'totalClients', label: 'Total Clients', width: 2, mobileWidth: 6 }, // 2/row on mobile, 2/12 on desktop
914
+ { type: 'metric-card', key: 'activeClients30d', label: 'Active (30d)', width: 2, mobileWidth: 6 },
915
+ { type: 'metric-card', key: 'visitsThisMonth', label: 'Visits', width: 4 }, // full width on mobile (default), 4/12 on desktop
916
+ ],
917
+ ```
918
+
919
+ - `mobileWidth` defaults to `12` (full width, stacked) — existing schemas
920
+ keep their current mobile behavior.
921
+ - `width` keeps controlling `sm`/`lg` (the desktop grid).
922
+ - Both accept any value 1–12; Tailwind classes are emitted as complete
923
+ literals so the JIT picks them up.
924
+
580
925
  ---
581
926
 
582
927
  ## Public API
@@ -586,13 +931,13 @@ Only what `public-api.ts` exports is stable contract; everything else is private
586
931
  | Group | Exports |
587
932
  | --- | --- |
588
933
  | Wiring & catalog | `provideGasAdmin`, `provideFirebaseAdapter`, `GAS_SCHEMAS`, `setSchemaCatalog`, `getSchema`, `allSchemas`, `getAuth`, type `GasAdminConfig` |
589
- | Tokens & adapters | `API_URL`, `FIREBASE_AUTH_ADAPTER`, type `FirebaseAuthAdapter` |
934
+ | Tokens & adapters | `API_URL`, `API_DRIVER`, `DEFAULT_RESPONSE_UNWRAP`, `VALUE_SOURCES`, `FIREBASE_AUTH_ADAPTER`, type `FirebaseAuthAdapter`, type `ValueSources` |
590
935
  | Routes & guard | `authGuard`, `shellRoutes`, `resourceRoutes` |
591
936
  | Pages | `Login`, `Dashboard`, `ListView`, `FormView`, `ResourcePage` |
592
937
  | Components | `Sidebar`, `ComposerHost`, `FieldHost`, `ConfirmDialog`, `Toast` |
593
938
  | Services | `ApiService`, `AuthService` (+ `SESSION_TOKEN_KEY`, `SESSION_USER_KEY`), `NotificationService`, `DataSourceService`, `UploadService`, `ErrorMapperService`, `apiErrorInterceptor` |
594
939
  | Forms | `buildFormSchema`, `seedModel`, `buildPayload` |
595
- | Schema models | `ResourceSchema`, `AuthSchema`, `AuthProvider`, `FirebaseAuthConfig`, `ResourceAction`, `ApiError` and the full field/widget/view/layout vocabulary |
940
+ | Schema models | `ResourceSchema`, `AuthSchema`, `AuthProvider`, `FirebaseAuthConfig`, `ResourceAction`, `ApiError`, `PageResult`, `ResponseUnwrap`, `CustomEndpointDefinition`, `HttpMethod` and the full field/widget/view/layout vocabulary |
596
941
  | Render registries | `fieldDescriptors`, `columnFormatDescriptors`, `viewDescriptors`, `layoutDescriptors`, `renderKinds` |
597
942
  | Custom render | `registerRenderDescriptor`, `unregisterRenderDescriptor`, `provideRenderDescriptors`, `getRenderDescriptor`, `isSupported`, `isRenderSupported`, types `RenderKind`, `ComponentRenderKind` |
598
943
  | Composition | `validateComposition`, `MAX_DEPTH`, `SchemaCompositionError` |
@@ -1,11 +1,11 @@
1
- import { i as getSchema } from "./tinya-code-gas-admin-ui-schema-catalog-C9BqNPt8.mjs";
1
+ import { i as getSchema } from "./tinya-code-gas-admin-ui-schema-catalog-D0Pk0wAN.mjs";
2
2
  import { t as Button } from "./tinya-code-gas-admin-ui-button-BQjNW8KB.mjs";
3
- import { t as DataSourceService } from "./tinya-code-gas-admin-ui-data-source.service-CvL9YHyu.mjs";
4
- import { t as ErrorMapperService } from "./tinya-code-gas-admin-ui-error-mapper.service-CFYzwAbD.mjs";
3
+ import { t as DataSourceService } from "./tinya-code-gas-admin-ui-data-source.service-96FSbN72.mjs";
4
+ import { t as ErrorMapperService } from "./tinya-code-gas-admin-ui-error-mapper.service-DY5y1FjE.mjs";
5
5
  import { t as EmptyState } from "./tinya-code-gas-admin-ui-empty-state-BMc0QtoT.mjs";
6
6
  import { t as Skeleton } from "./tinya-code-gas-admin-ui-skeleton-DnWK0tcZ.mjs";
7
7
  import * as i0 from "@angular/core";
8
- import { ChangeDetectionStrategy, Component, computed, effect, inject, signal } from "@angular/core";
8
+ import { ChangeDetectionStrategy, Component, InjectionToken, computed, effect, inject, signal } from "@angular/core";
9
9
  import { ActivatedRoute } from "@angular/router";
10
10
  import { LucideChevronLeft, LucideChevronRight } from "@lucide/angular";
11
11
  import { toSignal } from "@angular/core/rxjs-interop";
@@ -85,6 +85,10 @@ function todayCivil() {
85
85
  const day = String(now.getDate()).padStart(2, "0");
86
86
  return `${now.getFullYear()}-${month}-${day}`;
87
87
  }
88
+ const CALENDAR_TODAY = new InjectionToken("gas.admin.calendarToday", {
89
+ providedIn: "root",
90
+ factory: () => todayCivil
91
+ });
88
92
  function addDays(ms, days) {
89
93
  return ms + days * DAY_MS;
90
94
  }
@@ -333,7 +337,8 @@ var CalendarView = class CalendarView {
333
337
  }, ...ngDevMode ? [{ debugName: "config" }] : /* istanbul ignore next */ []);
334
338
  explicitMode = signal(void 0, ...ngDevMode ? [{ debugName: "explicitMode" }] : /* istanbul ignore next */ []);
335
339
  mode = computed(() => this.explicitMode() ?? this.config()?.defaultView ?? "month", ...ngDevMode ? [{ debugName: "mode" }] : /* istanbul ignore next */ []);
336
- cursor = signal(todayCivil(), ...ngDevMode ? [{ debugName: "cursor" }] : /* istanbul ignore next */ []);
340
+ cursor = signal(inject(CALENDAR_TODAY)(), ...ngDevMode ? [{ debugName: "cursor" }] : /* istanbul ignore next */ []);
341
+ todayFn = inject(CALENDAR_TODAY);
337
342
  visibleRange = computed(() => this.config() === void 0 ? null : visibleRangeFor(this.mode(), this.cursor()), ...ngDevMode ? [{ debugName: "visibleRange" }] : /* istanbul ignore next */ []);
338
343
  rangeLabel = computed(() => this.config() === void 0 ? "" : rangeLabelFor(this.mode(), this.cursor()), ...ngDevMode ? [{ debugName: "rangeLabel" }] : /* istanbul ignore next */ []);
339
344
  status = signal("loading", ...ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []);
@@ -386,7 +391,7 @@ var CalendarView = class CalendarView {
386
391
  this.cursor.set(shiftCursor(this.mode(), this.cursor(), 1));
387
392
  }
388
393
  today() {
389
- this.cursor.set(todayCivil());
394
+ this.cursor.set(this.todayFn());
390
395
  }
391
396
  setMode(mode) {
392
397
  this.explicitMode.set(mode);
@@ -1019,4 +1024,4 @@ i0.ɵɵngDeclareClassMetadata({
1019
1024
  });
1020
1025
  export { CalendarView };
1021
1026
 
1022
- //# sourceMappingURL=tinya-code-gas-admin-ui-calendar-view-FfMmRiix.mjs.map
1027
+ //# sourceMappingURL=tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tinya-code-gas-admin-ui-calendar-view-DCY8bj2V.mjs","names":[],"sources":["../../../projects/gas-admin-ui/src/lib/views/calendar-view/calendar-model.ts","../../../projects/gas-admin-ui/src/lib/views/calendar-view/calendar-view.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\nimport type { CalendarViewConfig, FieldSchema } from '../../core/models/schema.model';\nimport type { SourceRow } from '../../core/services/data-source.service';\n\n/**\n * Pure date + event-mapping model for the generic calendar view (R9.8.3).\n *\n * Date model (decided): backend `date` fields are ISO civil dates\n * `YYYY-MM-DD` treated as CALENDAR DAYS — never shifted through the browser\n * timezone. Every parse goes through `Date.UTC` on the literal parts, never\n * `new Date('YYYY-MM-DD')` in local time (that lands on the previous day\n * west of Greenwich).\n */\n\n/** Milliseconds in one civil day — every day-step of the model uses this. */\nexport const DAY_MS = 86_400_000;\n\n/** Visible modes of the calendar. */\nexport type CalendarMode = 'month' | 'week' | 'day' | 'agenda';\n\n/** Inclusive pair of civil days (`YYYY-MM-DD`) that the view paints. */\nexport interface CivilRange {\n desde: string;\n hasta: string;\n}\n\n/** One event painted from a source row. */\nexport interface CalEvent {\n key: string;\n title: string;\n /** True first day (`YYYY-MM-DD`), before any clamp to the visible range. */\n startDay: string;\n /** True last day, inclusive. Equals `startDay` for single-day events. */\n endDay: string;\n color?: string;\n row: SourceRow;\n}\n\n/** A row excluded from the grid because its interval is inconsistent. */\nexport interface CalendarProblem {\n row: SourceRow;\n reason: string;\n}\n\n/** Result of mapping raw rows onto calendar events (R9.8.3). */\nexport interface CalendarMapping {\n events: CalEvent[];\n /** Rows whose `startField` is empty or not a valid civil date. */\n undated: SourceRow[];\n /** Rows whose `endField` makes the interval impossible. */\n invalid: CalendarProblem[];\n}\n\n/** Spanish reasons shown next to problem records (UI copy). */\nexport const REASON_UNDATED = 'Sin fecha de inicio válida';\nexport const REASON_INVALID_END = 'Fecha de fin inválida';\nexport const REASON_END_BEFORE_START = 'El fin es anterior al inicio';\n\nconst MONTHS = [\n 'enero',\n 'febrero',\n 'marzo',\n 'abril',\n 'mayo',\n 'junio',\n 'julio',\n 'agosto',\n 'septiembre',\n 'octubre',\n 'noviembre',\n 'diciembre',\n];\n\nconst MONTHS_SHORT = [\n 'ene',\n 'feb',\n 'mar',\n 'abr',\n 'may',\n 'jun',\n 'jul',\n 'ago',\n 'sep',\n 'oct',\n 'nov',\n 'dic',\n];\n\n/** Weekday names Monday-first (the calendar's week start, decided). */\nconst WEEKDAYS_SHORT = ['lun', 'mar', 'mié', 'jue', 'vie', 'sáb', 'dom'];\nconst WEEKDAYS = ['lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado', 'domingo'];\n\n/** Weekday headings of the Monday-first grid, in display order. */\nexport const WEEKDAY_HEADINGS: readonly string[] = WEEKDAYS_SHORT;\n\nconst DAY_PATTERN = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\n\n/**\n * `YYYY-MM-DD` → UTC milliseconds at midnight of that civil day.\n *\n * Strict: the string must be exactly three zero-padded parts and name a real\n * calendar day (`2026-02-30` is rejected, not rolled into March). Empty,\n * malformed or non-string input → `null` (the caller decides whether that\n * means \"undated\" or \"invalid\").\n */\nexport function parseDay(value: unknown): number | null {\n if (typeof value !== 'string') return null;\n const match = DAY_PATTERN.exec(value.trim());\n if (match === null) return null;\n const year = Number(match[1]);\n const month = Number(match[2]);\n const day = Number(match[3]);\n const ms = Date.UTC(year, month - 1, day);\n const back = new Date(ms);\n if (\n back.getUTCFullYear() !== year ||\n back.getUTCMonth() !== month - 1 ||\n back.getUTCDate() !== day\n ) {\n return null;\n }\n return ms;\n}\n\n/** UTC milliseconds at midnight → `YYYY-MM-DD` (inverse of `parseDay`). */\nexport function formatDay(ms: number): string {\n const date = new Date(ms);\n const month = String(date.getUTCMonth() + 1).padStart(2, '0');\n const day = String(date.getUTCDate()).padStart(2, '0');\n return `${date.getUTCFullYear()}-${month}-${day}`;\n}\n\n/**\n * Today as a civil day, in the USER's local calendar date — the local parts\n * are read off `new Date()` and formatted manually, so no timezone shift can\n * sneak in through a round-trip.\n */\nexport function todayCivil(): string {\n const now = new Date();\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const day = String(now.getDate()).padStart(2, '0');\n return `${now.getFullYear()}-${month}-${day}`;\n}\n\n/**\n * Token inyectable para \"hoy\" (YYYY-MM-DD). Permite a los tests fijar la\n * fecha del sistema sin depender del reloj real (evita flakiness por mes).\n * Por defecto usa `todayCivil()` (fecha real del sistema).\n */\nexport const CALENDAR_TODAY = new InjectionToken<() => string>(\n 'gas.admin.calendarToday',\n { providedIn: 'root', factory: () => todayCivil },\n);\n\n/** Day-step on civil days (safe: every `ms` is a UTC midnight). */\nexport function addDays(ms: number, days: number): number {\n return ms + days * DAY_MS;\n}\n\n/** 0 = Monday … 6 = Sunday (the calendar's Monday-first convention). */\nexport function dayOfWeekMon0(ms: number): number {\n return (new Date(ms).getUTCDay() + 6) % 7;\n}\n\n/**\n * Move a civil day by whole months, clamping the day-of-month to the target\n * month's length (Jan 31 + 1 month → Feb 28).\n */\nexport function shiftMonths(ms: number, months: number): number {\n const date = new Date(ms);\n const target = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + months, 1));\n const lastDay = new Date(\n Date.UTC(target.getUTCFullYear(), target.getUTCMonth() + 1, 0),\n ).getUTCDate();\n return Date.UTC(\n target.getUTCFullYear(),\n target.getUTCMonth(),\n Math.min(date.getUTCDate(), lastDay),\n );\n}\n\n/** Monday of the 6-week (42-day) grid that shows the cursor's month. */\nfunction monthGridStartMs(cursorMs: number): number {\n const date = new Date(cursorMs);\n const firstOfMonth = Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1);\n return addDays(firstOfMonth, -dayOfWeekMon0(firstOfMonth));\n}\n\n/**\n * The 42 civil days of the month grid (6 Monday-first weeks), so the grid\n * always has the same shape and the de-emphasized edge days are complete.\n */\nexport function monthGridDays(cursorDay: string): string[] {\n const cursor = parseDay(cursorDay);\n if (cursor === null) return [];\n const start = monthGridStartMs(cursor);\n return Array.from({ length: 42 }, (_, index) => formatDay(addDays(start, index)));\n}\n\n/** The 7 civil days of the Monday-first week containing the cursor. */\nexport function weekDays(cursorDay: string): string[] {\n const cursor = parseDay(cursorDay);\n if (cursor === null) return [];\n const start = addDays(cursor, -dayOfWeekMon0(cursor));\n return Array.from({ length: 7 }, (_, index) => formatDay(addDays(start, index)));\n}\n\n/** Every civil day of an inclusive range (empty when the range is invalid). */\nexport function daysInRange(range: CivilRange): string[] {\n const from = parseDay(range.desde);\n const to = parseDay(range.hasta);\n if (from === null || to === null || to < from) return [];\n const days: string[] = [];\n for (let ms = from; ms <= to; ms = addDays(ms, 1)) {\n days.push(formatDay(ms));\n }\n return days;\n}\n\n/**\n * Visible `{desde, hasta}` for a mode + cursor.\n *\n * - `month`: the 6-week grid range (superset of the month) — with the\n * backend-range optimization the fetch already covers the edge weeks.\n * - `week`: the Monday-first week of the cursor.\n * - `day`: the cursor itself.\n * - `agenda`: a fixed 30-day window from the cursor.\n */\nexport function visibleRangeFor(mode: CalendarMode, cursorDay: string): CivilRange {\n const cursor = parseDay(cursorDay);\n if (cursor === null) return { desde: '', hasta: '' };\n switch (mode) {\n case 'month': {\n const start = monthGridStartMs(cursor);\n return { desde: formatDay(start), hasta: formatDay(addDays(start, 41)) };\n }\n case 'week': {\n const start = addDays(cursor, -dayOfWeekMon0(cursor));\n return { desde: formatDay(start), hasta: formatDay(addDays(start, 6)) };\n }\n case 'day':\n return { desde: formatDay(cursor), hasta: formatDay(cursor) };\n case 'agenda':\n return { desde: formatDay(cursor), hasta: formatDay(addDays(cursor, 29)) };\n }\n}\n\n/** Move the cursor one step of the current mode (agenda steps 30 days). */\nexport function shiftCursor(mode: CalendarMode, cursorDay: string, delta: 1 | -1): string {\n const cursor = parseDay(cursorDay) ?? parseDay(todayCivil()) ?? Date.UTC(1970, 0, 1);\n switch (mode) {\n case 'month':\n return formatDay(shiftMonths(cursor, delta));\n case 'week':\n return formatDay(addDays(cursor, 7 * delta));\n case 'day':\n return formatDay(addDays(cursor, delta));\n case 'agenda':\n return formatDay(addDays(cursor, 30 * delta));\n }\n}\n\n/** True when both civil days fall in the same year+month. */\nexport function sameMonth(a: string, b: string): boolean {\n const msA = parseDay(a);\n const msB = parseDay(b);\n if (msA === null || msB === null) return false;\n const dateA = new Date(msA);\n const dateB = new Date(msB);\n return (\n dateA.getUTCFullYear() === dateB.getUTCFullYear() && dateA.getUTCMonth() === dateB.getUTCMonth()\n );\n}\n\n/** Depth-first lookup of a field by key through `group`/`list` nesting. */\nexport function findFieldByKey(\n fields: readonly FieldSchema[],\n key: string,\n): FieldSchema | undefined {\n for (const field of fields) {\n if (field.key === key) return field;\n if (field.type === 'group') {\n const hit = findFieldByKey(field.fields, key);\n if (hit !== undefined) return hit;\n } else if (field.type === 'list') {\n const hit = findFieldByKey(field.itemFields, key);\n if (hit !== undefined) return hit;\n }\n }\n return undefined;\n}\n\n/**\n * Pure row → event mapping (R9.8.3): the single place that decides what is a\n * paintable event, what is undated and what is an impossible interval.\n *\n * - Empty/invalid `startField` → `undated` (surfaced, never hidden).\n * - `endField` of type `time` → same civil day as the start (time-of-day\n * within the start day; it never spans days).\n * - Empty `endField` (date) → single-day event on the start.\n * - `end < start` → `invalid` with a visible reason; not painted at all.\n */\nexport function mapCalendarRows(\n rows: readonly SourceRow[],\n config: CalendarViewConfig,\n options: { keyField: string; endFieldType?: string },\n): CalendarMapping {\n const events: CalEvent[] = [];\n const undated: SourceRow[] = [];\n const invalid: CalendarProblem[] = [];\n\n for (const row of rows) {\n const startMs = parseDay(row[config.startField]);\n if (startMs === null) {\n undated.push(row);\n continue;\n }\n let endMs = startMs;\n if (config.endField !== undefined && options.endFieldType !== 'time') {\n const rawEnd = row[config.endField];\n const empty = rawEnd === null || rawEnd === undefined || rawEnd === '';\n if (!empty) {\n const parsedEnd = parseDay(rawEnd);\n if (parsedEnd === null) {\n invalid.push({ row, reason: REASON_INVALID_END });\n continue;\n }\n if (parsedEnd < startMs) {\n invalid.push({ row, reason: REASON_END_BEFORE_START });\n continue;\n }\n endMs = parsedEnd;\n }\n }\n const key = String(row[options.keyField] ?? '');\n const title = String(row[config.titleField] ?? '').trim() || key;\n const rawColor = config.colorField !== undefined ? row[config.colorField] : undefined;\n const color = typeof rawColor === 'string' && rawColor !== '' ? rawColor : undefined;\n events.push({ key, title, startDay: formatDay(startMs), endDay: formatDay(endMs), color, row });\n }\n\n return { events, undated, invalid };\n}\n\n/**\n * Bucket events per civil day inside the visible range.\n *\n * A multi-day event appears under EVERY day it touches; the painted span is\n * clamped to the range (the event keeps its true `startDay`/`endDay` — only\n * the buckets are limited), which also keeps long intervals cheap.\n */\nexport function eventsByDay(\n events: readonly CalEvent[],\n range: CivilRange,\n): Map<string, CalEvent[]> {\n const byDay = new Map<string, CalEvent[]>();\n const from = parseDay(range.desde);\n const to = parseDay(range.hasta);\n if (from === null || to === null || to < from) return byDay;\n for (const event of events) {\n const start = parseDay(event.startDay);\n const end = parseDay(event.endDay);\n if (start === null || end === null) continue;\n const paintFrom = Math.max(start, from);\n const paintTo = Math.min(end, to);\n for (let ms = paintFrom; ms <= paintTo; ms = addDays(ms, 1)) {\n const day = formatDay(ms);\n const bucket = byDay.get(day);\n if (bucket === undefined) byDay.set(day, [event]);\n else bucket.push(event);\n }\n }\n return byDay;\n}\n\n/** \"12 de octubre de 2026\" — civil parts, UTC-based (no TZ shift). */\nexport function dayLabel(day: string): string {\n const ms = parseDay(day);\n if (ms === null) return day;\n const date = new Date(ms);\n return `${date.getUTCDate()} de ${MONTHS[date.getUTCMonth()]} de ${date.getUTCFullYear()}`;\n}\n\n/** Short weekday of a civil day, Monday-first (\"lunes\" → \"lun\"). */\nexport function weekdayShort(day: string): string {\n const ms = parseDay(day);\n if (ms === null) return '';\n return WEEKDAYS_SHORT[dayOfWeekMon0(ms)];\n}\n\n/** Full weekday of a civil day (\"lunes\"). */\nexport function weekdayFull(day: string): string {\n const ms = parseDay(day);\n if (ms === null) return '';\n return WEEKDAYS[dayOfWeekMon0(ms)];\n}\n\n/** \"lunes, 12 de octubre de 2026\" — agenda/day heading. */\nexport function agendaDayLabel(day: string): string {\n const weekday = weekdayFull(day);\n return weekday !== '' ? `${weekday}, ${dayLabel(day)}` : dayLabel(day);\n}\n\n/**\n * Header label for the visible range, built from civil parts with a fixed\n * Spanish vocabulary (deterministic regardless of the runtime locale).\n */\nexport function rangeLabelFor(mode: CalendarMode, cursorDay: string): string {\n const cursor = parseDay(cursorDay);\n if (cursor === null) return '';\n const date = new Date(cursor);\n if (mode === 'month') {\n return `${MONTHS[date.getUTCMonth()]} de ${date.getUTCFullYear()}`;\n }\n const range = visibleRangeFor(mode, cursorDay);\n const desde = parseDay(range.desde);\n const hasta = parseDay(range.hasta);\n if (desde === null || hasta === null) return '';\n const startDate = new Date(desde);\n const endDate = new Date(hasta);\n const sameYear = startDate.getUTCFullYear() === endDate.getUTCFullYear();\n const left = `${startDate.getUTCDate()} ${MONTHS_SHORT[startDate.getUTCMonth()]}${\n sameYear ? '' : ` ${startDate.getUTCFullYear()}`\n }`;\n return `${left} – ${endDate.getUTCDate()} ${MONTHS_SHORT[endDate.getUTCMonth()]} ${endDate.getUTCFullYear()}`;\n}\n","import {\n ChangeDetectionStrategy,\n Component,\n computed,\n effect,\n inject,\n signal,\n} from '@angular/core';\nimport { toSignal } from '@angular/core/rxjs-interop';\nimport { ActivatedRoute } from '@angular/router';\nimport { LucideChevronLeft, LucideChevronRight } from '@lucide/angular';\n\nimport type {\n CalendarViewConfig,\n ResourceSchema,\n UiViewCalendar,\n SelectionBinding,\n} from '../../core/models/schema.model';\nimport { ErrorMapperService } from '../../core/services/error-mapper.service';\nimport { DataSourceService, type SourceRow } from '../../core/services/data-source.service';\nimport type { SelectionEmitter } from '../composer/view-context';\nimport { getSchema } from '../../core/registry/schema-catalog';\nimport { Button } from '../../shared/components/button/button';\nimport { EmptyState } from '../../shared/components/empty-state/empty-state';\nimport { Skeleton } from '../../shared/components/skeleton/skeleton';\nimport {\n agendaDayLabel,\n type CalendarMapping,\n type CalendarMode,\n type CalEvent,\n type CivilRange,\n daysInRange,\n eventsByDay,\n findFieldByKey,\n mapCalendarRows,\n monthGridDays,\n rangeLabelFor,\n REASON_END_BEFORE_START,\n REASON_UNDATED,\n sameMonth,\n shiftCursor,\n CALENDAR_TODAY,\n visibleRangeFor,\n weekDays,\n WEEKDAY_HEADINGS,\n weekdayShort,\n} from './calendar-model';\n\n/**\n * Narrowing helper: `UiViewBase` keeps `type: string` open, so comparing\n * `view.type === 'calendar'` alone does not remove the base member from the\n * union. Requiring `config` in the value is what actually proves the view is\n * a `UiViewCalendar` at runtime.\n */\nfunction calendarConfigOf(view: unknown): UiViewCalendar['config'] | undefined {\n if (!view || typeof view !== 'object') return undefined;\n const candidate = view as { type?: unknown; config?: unknown };\n return candidate.type === 'calendar' && candidate.config !== undefined\n ? (candidate.config as UiViewCalendar['config'])\n : undefined;\n}\n\ntype Status = 'loading' | 'ready' | 'error';\n\n/** Mode switcher, in display order (Spanish UI copy). */\nconst MODE_OPTIONS: readonly { mode: CalendarMode; label: string }[] = [\n { mode: 'month', label: 'Mes' },\n { mode: 'week', label: 'Semana' },\n { mode: 'day', label: 'Día' },\n { mode: 'agenda', label: 'Agenda' },\n];\n\n/** Stable empty bucket — avoids allocating a new array per day per cycle. */\nconst NO_EVENTS: readonly CalEvent[] = [];\n\n/**\n * Generic calendar view (R9.8.3): month / week / day / agenda over the rows\n * of the declared `CalendarViewConfig.source` (or the resource's own\n * listing), positioned client-side onto civil days.\n *\n * Data flow: ONE fetcher (`DataSourceService.rows`) per visible range. When\n * the schema declares `source.view` the visible `desde`/`hasta` travel as\n * `ctx` so the backend view can filter the range; without a view nothing\n * filters, the full listing is cached (30 s TTL) and reused while the user\n * navigates — both paths share this component, only the `ctx` differs.\n *\n * Read-only by design: no drag, no click-to-edit. Color never carries\n * meaning alone — every event pairs its color dot with the title text — and\n * the agenda mode is the no-grid fallback for small screens.\n */\n@Component({\n selector: 'app-calendar-view',\n changeDetection: ChangeDetectionStrategy.OnPush,\n imports: [Button, EmptyState, LucideChevronLeft, LucideChevronRight, Skeleton],\n template: `\n @if (config(); as cfg) {\n <section class=\"space-y-4\" aria-label=\"Calendario\" [attr.aria-busy]=\"status() === 'loading'\">\n <header class=\"flex flex-wrap items-center justify-between gap-3\">\n <h2 class=\"font-display text-xl font-semibold\">{{ rangeLabel() }}</h2>\n <div class=\"flex items-center gap-1\">\n <button\n app-button\n variant=\"outline\"\n size=\"icon\"\n type=\"button\"\n aria-label=\"Período anterior\"\n title=\"Período anterior\"\n (click)=\"previous()\"\n >\n <svg lucideChevronLeft size=\"16\" aria-hidden=\"true\" />\n </button>\n <button app-button variant=\"outline\" size=\"sm\" type=\"button\" (click)=\"today()\">\n Hoy\n </button>\n <button\n app-button\n variant=\"outline\"\n size=\"icon\"\n type=\"button\"\n aria-label=\"Período siguiente\"\n title=\"Período siguiente\"\n (click)=\"next()\"\n >\n <svg lucideChevronRight size=\"16\" aria-hidden=\"true\" />\n </button>\n </div>\n <div class=\"flex items-center gap-1\" role=\"group\" aria-label=\"Vista del calendario\">\n @for (option of modeOptions; track option.mode) {\n <button\n app-button\n variant=\"ghost\"\n size=\"sm\"\n type=\"button\"\n [attr.aria-pressed]=\"mode() === option.mode\"\n (click)=\"setMode(option.mode)\"\n >\n {{ option.label }}\n </button>\n }\n </div>\n </header>\n\n @switch (status()) {\n @case ('loading') {\n <app-skeleton [lines]=\"8\" [height]=\"32\" />\n }\n @case ('error') {\n <app-empty-state title=\"No se pudo cargar el calendario\" [message]=\"errorMessage()\">\n <button app-button variant=\"outline\" type=\"button\" (click)=\"retry()\">\n Reintentar\n </button>\n </app-empty-state>\n }\n @case ('ready') {\n @if (mapping().events.length === 0) {\n <app-empty-state\n title=\"Sin eventos\"\n message=\"No hay eventos con fecha válida en este período.\"\n />\n } @else {\n @switch (mode()) {\n @case ('month') {\n <div class=\"overflow-x-auto rounded-xl border border-neutral/20 bg-surface\">\n <div class=\"grid min-w-[640px] grid-cols-7\">\n @for (heading of weekdayHeadings; track heading) {\n <div\n class=\"border-b border-neutral/15 px-2 py-1.5 text-xs font-medium uppercase text-neutral\"\n >\n {{ heading }}\n </div>\n }\n @for (day of monthGrid(); track day) {\n <div\n class=\"min-h-24 border-b border-r border-neutral/10 p-1.5\"\n [attr.data-day]=\"day\"\n [class.opacity-40]=\"!isInCursorMonth(day)\"\n >\n <span class=\"text-xs font-medium\">{{ dayNumber(day) }}</span>\n <ul class=\"mt-1 space-y-0.5\">\n @for (event of eventsFor(day); track event.key) {\n <li\n class=\"flex items-center gap-1 text-xs\"\n [class.cursor-pointer]=\"seleccionable()\"\n [class.ring-2]=\"esSeleccionada(event)\"\n [attr.data-event-key]=\"event.key\"\n [attr.data-selectable]=\"seleccionable() ? 'true' : null\"\n (click)=\"seleccionar(event)\"\n >\n <span\n class=\"size-2 shrink-0 rounded-full bg-neutral/30\"\n [style.background-color]=\"event.color ?? null\"\n aria-hidden=\"true\"\n ></span>\n <span class=\"truncate\">{{ event.title }}</span>\n </li>\n }\n </ul>\n </div>\n }\n </div>\n </div>\n }\n @case ('week') {\n <div class=\"overflow-x-auto rounded-xl border border-neutral/20 bg-surface\">\n <div class=\"grid min-w-[560px] grid-cols-7\">\n @for (day of weekColumnDays(); track day) {\n <div class=\"min-h-32 border-r border-neutral/10 p-2\" [attr.data-day]=\"day\">\n <span class=\"text-xs font-medium uppercase text-neutral\">\n {{ weekdayShort(day) }} {{ dayNumber(day) }}\n </span>\n <ul class=\"mt-2 space-y-1\">\n @for (event of eventsFor(day); track event.key) {\n <li\n class=\"flex items-center gap-1.5 text-sm\"\n [class.cursor-pointer]=\"seleccionable()\"\n [class.ring-2]=\"esSeleccionada(event)\"\n [attr.data-event-key]=\"event.key\"\n [attr.data-selectable]=\"seleccionable() ? 'true' : null\"\n (click)=\"seleccionar(event)\"\n >\n <span\n class=\"size-2 shrink-0 rounded-full bg-neutral/30\"\n [style.background-color]=\"event.color ?? null\"\n aria-hidden=\"true\"\n ></span>\n <span class=\"truncate\">{{ event.title }}</span>\n </li>\n }\n </ul>\n </div>\n }\n </div>\n </div>\n }\n @case ('day') {\n <div\n class=\"rounded-xl border border-neutral/20 bg-surface p-4\"\n [attr.data-day]=\"cursor()\"\n >\n <h3 class=\"font-display text-base font-semibold\">\n {{ dayHeading(cursor()) }}\n </h3>\n @if (eventsFor(cursor()).length === 0) {\n <p class=\"mt-2 text-sm text-neutral\">Sin eventos este día.</p>\n } @else {\n <ul class=\"mt-3 space-y-2\">\n @for (event of eventsFor(cursor()); track event.key) {\n <li\n class=\"flex items-center gap-1 text-xs\"\n [class.cursor-pointer]=\"seleccionable()\"\n [class.ring-2]=\"esSeleccionada(event)\"\n [attr.data-event-key]=\"event.key\"\n [attr.data-selectable]=\"seleccionable() ? 'true' : null\"\n (click)=\"seleccionar(event)\"\n >\n <span\n class=\"size-2.5 shrink-0 rounded-full bg-neutral/30\"\n [style.background-color]=\"event.color ?? null\"\n aria-hidden=\"true\"\n ></span>\n <span>{{ event.title }}</span>\n </li>\n }\n </ul>\n }\n </div>\n }\n @case ('agenda') {\n <ol class=\"space-y-4\">\n @for (day of agendaDays(); track day) {\n <li class=\"rounded-xl border border-neutral/20 bg-surface p-4\">\n <h3 class=\"text-sm font-semibold\">{{ dayHeading(day) }}</h3>\n <ul class=\"mt-2 space-y-1\">\n @for (event of eventsFor(day); track event.key) {\n <li\n class=\"flex items-center gap-2 text-sm\"\n [class.cursor-pointer]=\"seleccionable()\"\n [class.ring-2]=\"esSeleccionada(event)\"\n [attr.data-event-key]=\"event.key\"\n [attr.data-selectable]=\"seleccionable() ? 'true' : null\"\n (click)=\"seleccionar(event)\"\n >\n <span\n class=\"size-2 shrink-0 rounded-full bg-neutral/30\"\n [style.background-color]=\"event.color ?? null\"\n aria-hidden=\"true\"\n ></span>\n <span>{{ event.title }}</span>\n </li>\n }\n </ul>\n </li>\n }\n </ol>\n }\n }\n }\n\n <!-- Problem records are VISIBLE, never silently dropped: a row\n without a valid start or with an impossible interval cannot\n be painted, but the operator must see why. -->\n @if (hasProblemRecords()) {\n <section\n class=\"rounded-xl border border-neutral/30 p-4\"\n aria-label=\"Registros sin fecha válida\"\n >\n <h3 class=\"font-display text-sm font-semibold\">Registros sin fecha válida</h3>\n <p class=\"mt-1 text-help text-neutral\">\n Estos registros no se pueden pintar en el calendario:\n </p>\n <ul class=\"mt-2 space-y-1 text-sm\">\n @for (row of mapping().undated; track $index) {\n <li>{{ recordLabel(row) }} — {{ reasonUndated }}</li>\n }\n @for (item of mapping().invalid; track $index) {\n <li>{{ recordLabel(item.row) }} — {{ item.reason }}</li>\n }\n </ul>\n </section>\n }\n }\n }\n </section>\n } @else {\n <app-empty-state\n title=\"Configuración inválida\"\n message=\"Esta vista no declara una configuración de calendario válida.\"\n />\n }\n `,\n})\nexport class CalendarView {\n private readonly route = inject(ActivatedRoute);\n // The shell mounts views via `loadComponent` (no input bindings), so the\n // schema is resolved from route data exactly like FormView does.\n private readonly routeData = toSignal(this.route.data, { requireSync: true });\n private readonly dataSource = inject(DataSourceService);\n private readonly errorMapper = inject(ErrorMapperService);\n\n /**\n * Resource schema for this view. The shell mounts views via\n * `loadComponent` (no input bindings), so the schema is resolved from\n * route data: either an explicit `schema` object, or a `resourceId` looked\n * up in the registry (same route shape FormView uses).\n */\n readonly schema = computed<ResourceSchema | undefined>(() => {\n const data = this.routeData();\n const explicit = data['schema'];\n if (explicit !== undefined) return explicit as ResourceSchema;\n const resourceId = data['resourceId'];\n return typeof resourceId === 'string' ? getSchema(resourceId) : undefined;\n });\n\n /**\n * Config resolved from `schema.uiViews[<route uiView>]`; `undefined` when\n * the route does not name a calendar view with a usable config — the\n * template then renders the invalid-config empty state instead of a grid.\n */\n readonly config = computed<CalendarViewConfig | undefined>(() => {\n const schema = this.schema();\n const viewName = this.routeData()['uiView'];\n if (schema === undefined || typeof viewName !== 'string') return undefined;\n const cfg = calendarConfigOf(schema.uiViews?.[viewName]);\n if (cfg === undefined) return undefined;\n const usable =\n typeof cfg.startField === 'string' &&\n cfg.startField !== '' &&\n typeof cfg.titleField === 'string' &&\n cfg.titleField !== '';\n return usable ? cfg : undefined;\n });\n\n /** Explicit user choice; `undefined` ⇒ the config's `defaultView`. */\n private readonly explicitMode = signal<CalendarMode | undefined>(undefined);\n\n readonly mode = computed<CalendarMode>(\n () => this.explicitMode() ?? this.config()?.defaultView ?? 'month',\n );\n\n /** Anchor civil day of the visible range; init = today (civil). */\n readonly cursor = signal<string>(inject(CALENDAR_TODAY)());\n\n /** \"Hoy\" inyectable (default: fecha real del sistema). */\n private readonly todayFn = inject(CALENDAR_TODAY);\n\n /** Visible `{desde, hasta}` derived from cursor + mode. */\n readonly visibleRange = computed<CivilRange | null>(() =>\n this.config() === undefined ? null : visibleRangeFor(this.mode(), this.cursor()),\n );\n\n readonly rangeLabel = computed(() =>\n this.config() === undefined ? '' : rangeLabelFor(this.mode(), this.cursor()),\n );\n\n readonly status = signal<Status>('loading');\n readonly rows = signal<SourceRow[]>([]);\n /** Clave seleccionada localmente cuando el binding declara `mode:'inline'`. */\n readonly seleccionada = signal<string | null>(null);\n readonly errorMessage = signal('');\n\n readonly modeOptions = MODE_OPTIONS;\n readonly weekdayHeadings = WEEKDAY_HEADINGS;\n readonly reasonUndated = REASON_UNDATED;\n\n /** Guards against a slow response overwriting a newer range's rows. */\n private loadEpoch = 0;\n\n constructor() {\n effect(() => {\n const cfg = this.config();\n const range = this.visibleRange();\n if (cfg === undefined || range === null) return;\n void this.load(cfg, range);\n });\n }\n\n /** Rows mapped to events + visible problem records (pure model). */\n readonly mapping = computed<CalendarMapping>(() => {\n const cfg = this.config();\n const schema = this.schema();\n if (cfg === undefined || schema === undefined) return { events: [], undated: [], invalid: [] };\n const endFieldType =\n cfg.endField !== undefined ? findFieldByKey(schema.fields, cfg.endField)?.type : undefined;\n return mapCalendarRows(this.rows(), cfg, {\n keyField: schema.keyField,\n endFieldType,\n });\n });\n\n /** Events bucketed per civil day, clamped to the visible range. */\n readonly eventsByDay = computed(() => {\n const range = this.visibleRange();\n if (range === null) return new Map<string, CalEvent[]>();\n return eventsByDay(this.mapping().events, range);\n });\n\n readonly monthGrid = computed(() => monthGridDays(this.cursor()));\n readonly weekColumnDays = computed(() => weekDays(this.cursor()));\n\n /** Agenda headings: the days of the range that actually have events. */\n readonly agendaDays = computed(() => {\n const range = this.visibleRange();\n if (range === null) return [] as string[];\n return daysInRange(range).filter((day) => this.eventsByDay().has(day));\n });\n\n readonly hasProblemRecords = computed(\n () => this.mapping().undated.length > 0 || this.mapping().invalid.length > 0,\n );\n\n previous(): void {\n this.cursor.set(shiftCursor(this.mode(), this.cursor(), -1));\n }\n\n next(): void {\n this.cursor.set(shiftCursor(this.mode(), this.cursor(), 1));\n }\n\n /** Reset the cursor to today (civil). */\n today(): void {\n this.cursor.set(this.todayFn());\n }\n\n setMode(mode: CalendarMode): void {\n this.explicitMode.set(mode);\n }\n\n retry(): void {\n const cfg = this.config();\n const range = this.visibleRange();\n if (cfg !== undefined && range !== null) void this.load(cfg, range);\n }\n\n /** Binding `select` declarado por la vista (R9.9.4 · decision table). */\n private binding(): SelectionBinding | undefined {\n return this.routeData()['select'] as SelectionBinding | undefined;\n }\n\n /**\n * Clic de evento. Sin declaración ⇒ inerte (el calendario no navegaba);\n * con declaración ⇒ sólo el contrato declarado corre: `route` emite al\n * dispatcher, `inline` selecciona en local, `none` es no-op explícito.\n */\n seleccionar(event: CalEvent): void {\n const binding = this.binding();\n if (binding === undefined) return;\n if (binding.mode === 'inline') {\n this.seleccionada.set(event.key);\n return;\n }\n if (binding.mode === 'none') return;\n const schema = this.schema();\n const emit = this.routeData()['emit'] as SelectionEmitter | undefined;\n emit?.({\n resource: schema?.id ?? String(this.routeData()['resourceId'] ?? ''),\n key: event.key,\n action: binding.on,\n });\n }\n\n /** El DOM declara clicable sólo si hay contrato que responder. */\n seleccionable(): boolean {\n const binding = this.binding();\n return binding !== undefined && binding.mode !== 'none';\n }\n\n esSeleccionada(event: CalEvent): boolean {\n return this.seleccionable() && this.seleccionada() === event.key;\n }\n\n eventsFor(day: string): readonly CalEvent[] {\n return this.eventsByDay().get(day) ?? NO_EVENTS;\n }\n\n dayNumber(day: string): number {\n return Number(day.slice(8, 10));\n }\n\n isInCursorMonth(day: string): boolean {\n return sameMonth(day, this.cursor());\n }\n\n /** Weekday + number (\"lun 12\") for week columns. */\n weekdayShort(day: string): string {\n return weekdayShort(day);\n }\n\n /** Full heading (\"lunes, 12 de octubre de 2026\") for day/agenda. */\n dayHeading(day: string): string {\n return agendaDayLabel(day);\n }\n\n recordLabel(row: SourceRow): string {\n const schema = this.schema();\n if (schema === undefined) return '';\n const title = String(row[schema.titleField] ?? '').trim();\n return title !== '' ? title : String(row[schema.keyField] ?? '');\n }\n\n private async load(cfg: CalendarViewConfig, range: CivilRange): Promise<void> {\n const schema = this.schema();\n if (schema === undefined) return;\n const epoch = ++this.loadEpoch;\n this.status.set('loading');\n // Optimization branch (decided): range ctx ONLY when the schema declares\n // a backend view — that's the signal that a `where` may filter on\n // `param: 'desde'`/`'hasta'`. Without a view the params would be ignored\n // (and the full listing is cached while the user navigates).\n const ctx =\n cfg.source?.view !== undefined && cfg.source.view !== ''\n ? { desde: range.desde, hasta: range.hasta }\n : undefined;\n const source = cfg.source ?? { resource: schema.id };\n try {\n const rows = await this.dataSource.rows(source, ctx);\n if (epoch !== this.loadEpoch) return;\n this.rows.set(rows);\n this.errorMessage.set('');\n this.status.set('ready');\n } catch (error) {\n if (epoch !== this.loadEpoch) return;\n this.errorMessage.set(\n this.errorMapper.map(error).general ?? 'No se pudo cargar el calendario.',\n );\n this.status.set('error');\n }\n }\n}\n"],"mappings":"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"}