emilsoftware-utilities 2.0.0-dev.64 → 2.0.0-dev.65

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.
@@ -177,6 +177,154 @@
177
177
  return groups;
178
178
  }
179
179
 
180
+ /**
181
+ * Appiattisce sia i grant Accessi sia le strutture gia raggruppate
182
+ * (`mnuAbil`, `{ codGrp, desGrp, menu }`) in una lista uniforme.
183
+ */
184
+ function flattenGrants(input) {
185
+ if (!Array.isArray(input)) return [];
186
+
187
+ const grouped = input.filter(
188
+ (item) =>
189
+ item &&
190
+ (Array.isArray(item.menu) || Array.isArray(item.menus)),
191
+ );
192
+
193
+ if (grouped.length > 0) {
194
+ const flat = [];
195
+ for (const group of grouped) {
196
+ const menus = group.menu || group.menus || [];
197
+ for (const menu of menus) {
198
+ flat.push({
199
+ codiceMenu: menu.codMnu ?? menu.codiceMenu ?? menu.code,
200
+ descrizioneMenu:
201
+ menu.desMnu ?? menu.descrizioneMenu ?? menu.label,
202
+ codiceGruppo:
203
+ group.codGrp ?? group.codiceGruppo ?? menu.codiceGruppo,
204
+ descrizioneGruppo:
205
+ group.desGrp ??
206
+ group.descrizioneGruppo ??
207
+ menu.descrizioneGruppo,
208
+ icona: menu.icon ?? menu.icona,
209
+ pagina: menu.pagina ?? menu.page,
210
+ tipo: menu.tipo ?? 'M',
211
+ });
212
+ }
213
+ }
214
+ return flat;
215
+ }
216
+
217
+ return input;
218
+ }
219
+
220
+ function asNumberMap(value) {
221
+ return value && typeof value === 'object' ? value : {};
222
+ }
223
+
224
+ function buildModel(input, options) {
225
+ const config = options || {};
226
+ const routeMap = config.routeMap || {};
227
+ const menuOrder = asNumberMap(config.menuOrder);
228
+ const groupOrder = asNumberMap(config.groupOrder);
229
+ const availableRoutes = new Set(
230
+ Array.isArray(config.availableRoutes)
231
+ ? config.availableRoutes.map(normalizePage)
232
+ : [],
233
+ );
234
+ const isRouteAvailable =
235
+ typeof config.isRouteAvailable === 'function'
236
+ ? config.isRouteAvailable
237
+ : (path) => availableRoutes.has(normalizePage(path));
238
+
239
+ const seen = new Set();
240
+ const areas = [];
241
+
242
+ flattenGrants(input).forEach((grant, index) => {
243
+ if (!grant || typeof grant !== 'object') return;
244
+ const type = String(grant.tipo || 'M').toUpperCase();
245
+ if (type !== 'M') return;
246
+
247
+ const code = String(
248
+ grant.codiceMenu || grant.codMnu || '',
249
+ )
250
+ .trim()
251
+ .toUpperCase();
252
+ if (!code) return;
253
+
254
+ const page = normalizePage(grant.pagina || grant.page);
255
+ if (!page) return;
256
+
257
+ const mappedRoute = String(routeMap[code] || '').trim();
258
+ const known = Boolean(mappedRoute) || isRouteAvailable(page);
259
+ const route = mappedRoute || (known ? page : '');
260
+ const available = Boolean(route);
261
+
262
+ const key = available ? `route:${route}` : `menu:${code}`;
263
+ if (seen.has(key)) return;
264
+ seen.add(key);
265
+
266
+ const groupName = String(
267
+ grant.descrizioneGruppo ||
268
+ grant.desGrp ||
269
+ grant.codiceGruppo ||
270
+ grant.codGrp ||
271
+ '',
272
+ ).trim();
273
+ const groupKey =
274
+ String(grant.codiceGruppo || grant.codGrp || '').trim() ||
275
+ groupName ||
276
+ 'ALTRE';
277
+
278
+ areas.push({
279
+ code,
280
+ label: String(
281
+ grant.descrizioneMenu ||
282
+ grant.desMnu ||
283
+ grant.label ||
284
+ code,
285
+ ),
286
+ group: groupName,
287
+ groupKey,
288
+ groupOrder: Number(groupOrder[groupKey] ?? 999),
289
+ order: Number(menuOrder[code] ?? index),
290
+ icon: String(grant.icona || grant.icon || 'grid-outline'),
291
+ route,
292
+ page,
293
+ url: '',
294
+ available,
295
+ reason: available
296
+ ? ''
297
+ : typeof config.reasonFor === 'function'
298
+ ? String(config.reasonFor(page) || '')
299
+ : '',
300
+ });
301
+ });
302
+
303
+ const grouped = new Map();
304
+ for (const area of areas) {
305
+ let group = grouped.get(area.groupKey);
306
+ if (!group) {
307
+ group = {
308
+ name: area.group || 'Altre aree',
309
+ order: area.groupOrder,
310
+ areas: [],
311
+ };
312
+ grouped.set(area.groupKey, group);
313
+ }
314
+ group.areas.push(area);
315
+ }
316
+
317
+ const groups = [...grouped.values()].sort(
318
+ (a, b) => a.order - b.order || a.name.localeCompare(b.name),
319
+ );
320
+ for (const group of groups) {
321
+ group.areas.sort(
322
+ (a, b) => a.order - b.order || a.label.localeCompare(b.label),
323
+ );
324
+ }
325
+ return groups;
326
+ }
327
+
180
328
  const STYLES = `
181
329
  :host {
182
330
  --es-font: var(--bs-body-font-family, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif);
@@ -300,6 +448,110 @@
300
448
  font-size: 13px;
301
449
  }
302
450
 
451
+ .default-page {
452
+ display: flex;
453
+ align-items: center;
454
+ gap: 14px;
455
+ margin-top: 16px;
456
+ padding: 14px 16px;
457
+ border: 1px solid var(--es-border);
458
+ border-left: 4px solid var(--es-primary);
459
+ border-radius: var(--es-radius);
460
+ background: var(--es-surface);
461
+ box-shadow: var(--es-shadow);
462
+ color: inherit;
463
+ text-decoration: none;
464
+ }
465
+
466
+ .default-page:hover,
467
+ .default-page:focus-visible {
468
+ border-color: var(--es-primary);
469
+ border-left-color: var(--es-primary);
470
+ box-shadow: var(--es-shadow-hover);
471
+ }
472
+
473
+ .default-page:focus-visible {
474
+ outline: 2px solid var(--es-primary);
475
+ outline-offset: 2px;
476
+ }
477
+
478
+ .default-page__icon {
479
+ display: grid;
480
+ place-items: center;
481
+ flex: 0 0 auto;
482
+ width: 44px;
483
+ height: 44px;
484
+ border-radius: 12px;
485
+ background: var(--es-primary-soft);
486
+ color: var(--es-primary);
487
+ }
488
+
489
+ .default-page__icon svg {
490
+ width: 22px;
491
+ height: 22px;
492
+ }
493
+
494
+ .default-page__copy {
495
+ display: flex;
496
+ flex: 1 1 auto;
497
+ flex-direction: column;
498
+ gap: 2px;
499
+ min-width: 0;
500
+ }
501
+
502
+ .default-page__eyebrow {
503
+ color: var(--es-muted);
504
+ font-size: 11px;
505
+ font-weight: 600;
506
+ letter-spacing: 0.08em;
507
+ text-transform: uppercase;
508
+ }
509
+
510
+ .default-page__label {
511
+ overflow: hidden;
512
+ color: var(--es-text);
513
+ font-size: 15px;
514
+ font-weight: 600;
515
+ text-overflow: ellipsis;
516
+ white-space: nowrap;
517
+ }
518
+
519
+ .default-page__url {
520
+ overflow: hidden;
521
+ color: var(--es-muted);
522
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
523
+ font-size: 11px;
524
+ text-overflow: ellipsis;
525
+ white-space: nowrap;
526
+ }
527
+
528
+ .default-page__action {
529
+ flex: 0 0 auto;
530
+ color: var(--es-primary);
531
+ font-size: 13px;
532
+ font-weight: 600;
533
+ }
534
+
535
+ .default-page--blocked {
536
+ border-left-color: var(--es-disabled-text);
537
+ background: var(--es-disabled-bg);
538
+ box-shadow: none;
539
+ cursor: not-allowed;
540
+ opacity: 0.8;
541
+ }
542
+
543
+ .default-page--blocked .default-page__icon,
544
+ .default-page--blocked .default-page__label,
545
+ .default-page--blocked .default-page__action {
546
+ color: var(--es-disabled-text);
547
+ }
548
+
549
+ .default-page__reason {
550
+ color: var(--es-muted);
551
+ font-size: 12px;
552
+ font-style: italic;
553
+ }
554
+
303
555
  .groups {
304
556
  display: grid;
305
557
  gap: 26px;
@@ -519,10 +771,18 @@
519
771
  'empty-text',
520
772
  'unavailable-template',
521
773
  'href-mode',
774
+ 'default-page',
522
775
  ];
523
776
  }
524
777
 
525
778
  #groups = [];
779
+ #grants = [];
780
+ #useGrants = false;
781
+ #routeMap = {};
782
+ #menuOrder = {};
783
+ #groupOrder = {};
784
+ #availableRoutes = [];
785
+ #isRouteAvailable = null;
526
786
  #root;
527
787
  #media;
528
788
  #observer;
@@ -571,6 +831,92 @@
571
831
 
572
832
  set groups(value) {
573
833
  this.#groups = normalizeGroups(value);
834
+ this.#useGrants = false;
835
+ this.render();
836
+ }
837
+
838
+ /**
839
+ * Grant Accessi (anche raggruppati per `menu`/`menus`). Il componente
840
+ * costruisce da solo gruppi, rotte e stato disponibile/bloccato.
841
+ */
842
+ get grants() {
843
+ return this.#grants;
844
+ }
845
+
846
+ set grants(value) {
847
+ this.#grants = Array.isArray(value) ? value : [];
848
+ this.#useGrants = true;
849
+ this.#rebuildFromGrants();
850
+ }
851
+
852
+ get routeMap() {
853
+ return this.#routeMap;
854
+ }
855
+
856
+ set routeMap(value) {
857
+ this.#routeMap = value && typeof value === 'object' ? value : {};
858
+ this.#refreshIfGrants();
859
+ }
860
+
861
+ get menuOrder() {
862
+ return this.#menuOrder;
863
+ }
864
+
865
+ set menuOrder(value) {
866
+ this.#menuOrder = value && typeof value === 'object' ? value : {};
867
+ this.#refreshIfGrants();
868
+ }
869
+
870
+ get groupOrder() {
871
+ return this.#groupOrder;
872
+ }
873
+
874
+ set groupOrder(value) {
875
+ this.#groupOrder = value && typeof value === 'object' ? value : {};
876
+ this.#refreshIfGrants();
877
+ }
878
+
879
+ get availableRoutes() {
880
+ return this.#availableRoutes;
881
+ }
882
+
883
+ set availableRoutes(value) {
884
+ this.#availableRoutes = Array.isArray(value) ? value : [];
885
+ this.#refreshIfGrants();
886
+ }
887
+
888
+ get isRouteAvailable() {
889
+ return this.#isRouteAvailable;
890
+ }
891
+
892
+ set isRouteAvailable(value) {
893
+ this.#isRouteAvailable = typeof value === 'function' ? value : null;
894
+ this.#refreshIfGrants();
895
+ }
896
+
897
+ get defaultPage() {
898
+ return this.getAttribute('default-page') || '';
899
+ }
900
+
901
+ set defaultPage(value) {
902
+ this.#setOptionalAttribute('default-page', value);
903
+ }
904
+
905
+ #refreshIfGrants() {
906
+ if (!this.#useGrants) return;
907
+ this.#rebuildFromGrants();
908
+ }
909
+
910
+ #rebuildFromGrants() {
911
+ this.#groups = buildModel(this.#grants, {
912
+ routeMap: this.#routeMap,
913
+ menuOrder: this.#menuOrder,
914
+ groupOrder: this.#groupOrder,
915
+ availableRoutes: this.#availableRoutes,
916
+ isRouteAvailable: this.#isRouteAvailable,
917
+ reasonFor: (page) =>
918
+ this.unavailableTemplate.replace('{page}', page),
919
+ });
574
920
  this.render();
575
921
  }
576
922
 
@@ -635,7 +981,7 @@
635
981
  }
636
982
 
637
983
  get theme() {
638
- return this.getAttribute('theme') || 'auto';
984
+ return this.getAttribute('theme') || 'light';
639
985
  }
640
986
 
641
987
  set theme(value) {
@@ -672,8 +1018,9 @@
672
1018
  }
673
1019
 
674
1020
  #resolveTheme() {
675
- const explicit = String(this.getAttribute('theme') || 'auto').toLowerCase();
1021
+ const explicit = String(this.getAttribute('theme') || 'light').toLowerCase();
676
1022
  if (explicit === 'light' || explicit === 'dark') return explicit;
1023
+ if (explicit !== 'auto') return 'light';
677
1024
 
678
1025
  const root = document.documentElement;
679
1026
  if (
@@ -799,7 +1146,92 @@
799
1146
  : '';
800
1147
 
801
1148
  this.#root.innerHTML =
802
- '<main class="shell">' + header + body + legend + '</main>';
1149
+ '<main class="shell">' +
1150
+ header +
1151
+ this.#renderDefaultPage() +
1152
+ body +
1153
+ legend +
1154
+ '</main>';
1155
+ }
1156
+
1157
+ #renderDefaultPage() {
1158
+ const page = normalizePage(this.defaultPage);
1159
+ if (!page) return '';
1160
+
1161
+ const areas = this.#groups.reduce(
1162
+ (all, group) => all.concat(group.areas),
1163
+ [],
1164
+ );
1165
+ const match = areas.find(
1166
+ (area) => area.route === page || area.page === page,
1167
+ );
1168
+
1169
+ let route = '';
1170
+ if (match) {
1171
+ route = match.available ? match.route : '';
1172
+ } else {
1173
+ const availableRoutes = new Set(
1174
+ this.#availableRoutes.map(normalizePage),
1175
+ );
1176
+ const known = this.#isRouteAvailable
1177
+ ? this.#isRouteAvailable(page)
1178
+ : availableRoutes.has(page);
1179
+ route = known ? page : '';
1180
+ }
1181
+
1182
+ const available = Boolean(route);
1183
+ const href = available
1184
+ ? this.#hrefFor({ available: true, route })
1185
+ : '';
1186
+ const url =
1187
+ (match && match.url) ||
1188
+ (href ? new URL(href, document.baseURI).href : page);
1189
+ const label = match ? match.label : page;
1190
+ const iconName = match ? match.icon : 'home-outline';
1191
+ const reason = available
1192
+ ? ''
1193
+ : (match && match.reason) || this.#defaultReason({ page });
1194
+
1195
+ const content =
1196
+ '<span class="default-page__icon">' +
1197
+ icon(iconName) +
1198
+ '</span>' +
1199
+ '<span class="default-page__copy">' +
1200
+ '<span class="default-page__eyebrow">Pagina di default</span>' +
1201
+ '<span class="default-page__label">' +
1202
+ escapeHtml(label) +
1203
+ '</span>' +
1204
+ (available
1205
+ ? '<span class="default-page__url" title="' +
1206
+ escapeHtml(url) +
1207
+ '">' +
1208
+ escapeHtml(url) +
1209
+ '</span>'
1210
+ : '<span class="default-page__reason">' +
1211
+ escapeHtml(reason) +
1212
+ '</span>') +
1213
+ '</span>';
1214
+
1215
+ if (available) {
1216
+ return (
1217
+ '<a class="default-page" href="' +
1218
+ escapeHtml(href) +
1219
+ '">' +
1220
+ content +
1221
+ '<span class="default-page__action">Apri</span>' +
1222
+ '</a>'
1223
+ );
1224
+ }
1225
+
1226
+ return (
1227
+ '<div class="default-page default-page--blocked" role="link" ' +
1228
+ 'aria-disabled="true" tabindex="0" title="' +
1229
+ escapeHtml(reason) +
1230
+ '">' +
1231
+ content +
1232
+ '<span class="default-page__action">Bloccata</span>' +
1233
+ '</div>'
1234
+ );
803
1235
  }
804
1236
 
805
1237
  #renderGroup(group, groupIndex) {
@@ -892,7 +1324,13 @@
892
1324
  customElements.define(TAG, AreeDisponibiliElement);
893
1325
  }
894
1326
 
895
- // Esposto per integrazioni che preferiscono costruire l'elemento da codice.
1327
+ // Esposto per integrazioni che preferiscono costruire elemento e modello
1328
+ // da codice, senza duplicare la logica di raggruppamento nelle applicazioni.
896
1329
  window.EmilSoftware = window.EmilSoftware || {};
897
1330
  window.EmilSoftware.AreeDisponibiliElement = AreeDisponibiliElement;
1331
+ window.EmilSoftware.AreeDisponibili = {
1332
+ Element: AreeDisponibiliElement,
1333
+ buildModel,
1334
+ version: '1.1.0',
1335
+ };
898
1336
  })();
@@ -185,6 +185,8 @@
185
185
  },
186
186
  ];
187
187
 
188
+ element.defaultPage = '/main';
189
+
188
190
  function log(message) {
189
191
  const line = document.createElement('p');
190
192
  line.textContent = new Date().toLocaleTimeString() + ' · ' + message;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "emilsoftware-utilities",
3
- "version": "2.0.0-dev.64",
3
+ "version": "2.0.0-dev.65",
4
4
  "description": "Utilities for EmilSoftware",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",