@massimo-cassandro/minimo 1.5.13 → 1.6.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 (114) hide show
  1. package/design-tokens/_src/components/spinner.minimo.tokens.mjs +18 -0
  2. package/design-tokens/_src/headings.minimo.tokens.mjs +56 -0
  3. package/design-tokens/_src/link.minimo.tokens.mjs +41 -0
  4. package/design-tokens/_src/radius.minimo.tokens.mjs +44 -0
  5. package/design-tokens/_src/size.minimo.tokens.mjs +105 -0
  6. package/design-tokens/_src/status.minimo.tokens.mjs +92 -0
  7. package/design-tokens/_src/typography.minimo.tokens.mjs +123 -0
  8. package/design-tokens/_src/utility.minimo.tokens.mjs +8 -0
  9. package/design-tokens/_src/web-components/simple-datatable.minimo.tokens.mjs +47 -0
  10. package/{design-tokens-utilities → design-tokens/utilities}/build-tokens-src/config.mjs +1 -1
  11. package/{design-tokens-utilities → design-tokens/utilities}/build-tokens.mjs +1 -1
  12. package/{design-tokens-utilities → design-tokens/utilities}/check-unresolved-custom-props.mjs +2 -2
  13. package/index.js +5 -3
  14. package/package.json +13 -8
  15. package/src/components/autocomplete/autocomplete-engine.js +70 -64
  16. package/src/components/autocomplete/check-autocomplete.js +10 -2
  17. package/src/components/dismiss-alerts.js +9 -2
  18. package/src/components/form-reset-submit.js +12 -4
  19. package/src/components/inner-nav/inner-nav.js +5 -1
  20. package/src/components/modal-alert/customization-templates/custom-modal-alert.js +4 -0
  21. package/src/components/modal-alert/defaults.js +6 -3
  22. package/src/components/modal-alert/modal-alert.js +58 -20
  23. package/src/components/modal-popup/modal-popup.js +51 -29
  24. package/src/components/overlay/overlay.js +13 -1
  25. package/src/components/sf-macro/sf-macro.js +23 -12
  26. package/src/components/snackbar/snackbar.js +32 -17
  27. package/src/components/spinner/spinner.js +6 -2
  28. package/src/components/unsplash-page/fetch-unsplash-data.js +25 -2
  29. package/src/components/unsplash-page/img-breakpoints.js +11 -0
  30. package/src/components/unsplash-page/unsplash-page.js +64 -40
  31. package/src/components/unsplash-page/unsplash-page.module.css +1 -1
  32. package/src/css/forms/forms.css +4 -0
  33. package/src/utilities/classnames.js +14 -1
  34. package/src/utilities/create-id.js +19 -0
  35. package/src/utilities/dom-builder/dom-builder.js +84 -75
  36. package/src/utilities/dom-builder/domBuilderBasicSetup.js +12 -9
  37. package/src/utilities/dom-builder/parseDomString.js +31 -29
  38. package/src/utilities/enable-submit-btns.js +7 -1
  39. package/src/utilities/escapeHTML.js +13 -0
  40. package/src/utilities/sentence-case.js +5 -2
  41. package/src/utilities/strip-tags.js +8 -0
  42. package/src/utilities/title-case.js +6 -2
  43. package/src/utilities/validation-error-class.js +14 -7
  44. package/src/web-components/s-datatable-component/s-datatable-component.js +1 -0
  45. package/src/web-components/s-datatable-component/src/parse-cols.js +1 -0
  46. package/design-tokens-src/components/spinner.minimo.tokens.jsonc +0 -22
  47. package/design-tokens-src/headings.minimo.tokens.jsonc +0 -57
  48. package/design-tokens-src/link.minimo.tokens.jsonc +0 -41
  49. package/design-tokens-src/radius.minimo.tokens.jsonc +0 -45
  50. package/design-tokens-src/size.minimo.tokens.jsonc +0 -105
  51. package/design-tokens-src/status.minimo.tokens.jsonc +0 -93
  52. package/design-tokens-src/typography.minimo.tokens.jsonc +0 -129
  53. package/design-tokens-src/utility.minimo.tokens.jsonc +0 -8
  54. package/design-tokens-src/web-components/simple-datatable.minimo.tokens.jsonc +0 -50
  55. /package/{design-tokens-src → design-tokens/_src}/_readme.md +0 -0
  56. /package/{design-tokens-src → design-tokens/_src}/alerts.minimo.tokens.mjs +0 -0
  57. /package/{design-tokens-src → design-tokens/_src}/badge.minimo.tokens.mjs +0 -0
  58. /package/{design-tokens-src → design-tokens/_src}/btn-close.minimo.tokens.mjs +0 -0
  59. /package/{design-tokens-src → design-tokens/_src}/btn-color-themes.minimo.tokens.mjs +0 -0
  60. /package/{design-tokens-src → design-tokens/_src}/btn.minimo.tokens.mjs +0 -0
  61. /package/{design-tokens-src → design-tokens/_src}/color-accent.minimo.tokens.mjs +0 -0
  62. /package/{design-tokens-src → design-tokens/_src}/color-neutral.minimo.tokens.mjs +0 -0
  63. /package/{design-tokens-src → design-tokens/_src}/color-primary.minimo.tokens.mjs +0 -0
  64. /package/{design-tokens-src → design-tokens/_src}/color-secondary.minimo.tokens.mjs +0 -0
  65. /package/{design-tokens-src → design-tokens/_src}/components/sf-macro.minimo.tokens.mjs +0 -0
  66. /package/{design-tokens-src → design-tokens/_src}/components/snackbar.minimo.tokens.mjs +0 -0
  67. /package/{design-tokens-src → design-tokens/_src}/components/unsplash-page.minimo.tokens.mjs +0 -0
  68. /package/{design-tokens-src → design-tokens/_src}/flex-grid.minimo.tokens.mjs +0 -0
  69. /package/{design-tokens-src → design-tokens/_src}/form-validation.minimo.tokens.mjs +0 -0
  70. /package/{design-tokens-src → design-tokens/_src}/form.minimo.tokens.mjs +0 -0
  71. /package/{design-tokens-src → design-tokens/_src}/layout.minimo.tokens.mjs +0 -0
  72. /package/{design-tokens-src → design-tokens/_src}/modal-popup.minimo.tokens.mjs +0 -0
  73. /package/{design-tokens-src → design-tokens/_src}/shadows.minimo.tokens.mjs +0 -0
  74. /package/{design-tokens-src → design-tokens/_src}/switch.minimo.tokens.mjs +0 -0
  75. /package/{design-tokens-src → design-tokens/_src}/table.minimo.tokens.mjs +0 -0
  76. /package/{design-tokens-src → design-tokens/_src}/text.minimo.tokens.mjs +0 -0
  77. /package/{design-tokens-src → design-tokens/_src}/z-index.minimo.tokens.mjs +0 -0
  78. /package/design-tokens/{alerts.minimo.tokens.jsonc → tokens/alerts.minimo.tokens.jsonc} +0 -0
  79. /package/design-tokens/{badge.minimo.tokens.jsonc → tokens/badge.minimo.tokens.jsonc} +0 -0
  80. /package/design-tokens/{btn-close.minimo.tokens.jsonc → tokens/btn-close.minimo.tokens.jsonc} +0 -0
  81. /package/design-tokens/{btn-color-themes.minimo.tokens.jsonc → tokens/btn-color-themes.minimo.tokens.jsonc} +0 -0
  82. /package/design-tokens/{btn.minimo.tokens.jsonc → tokens/btn.minimo.tokens.jsonc} +0 -0
  83. /package/design-tokens/{color-accent.minimo.tokens.jsonc → tokens/color-accent.minimo.tokens.jsonc} +0 -0
  84. /package/design-tokens/{color-neutral.minimo.tokens.jsonc → tokens/color-neutral.minimo.tokens.jsonc} +0 -0
  85. /package/design-tokens/{color-primary.minimo.tokens.jsonc → tokens/color-primary.minimo.tokens.jsonc} +0 -0
  86. /package/design-tokens/{color-secondary.minimo.tokens.jsonc → tokens/color-secondary.minimo.tokens.jsonc} +0 -0
  87. /package/design-tokens/{components → tokens/components}/sf-macro.minimo.tokens.jsonc +0 -0
  88. /package/design-tokens/{components → tokens/components}/snackbar.minimo.tokens.jsonc +0 -0
  89. /package/design-tokens/{components → tokens/components}/spinner.minimo.tokens.jsonc +0 -0
  90. /package/design-tokens/{components → tokens/components}/unsplash-page.minimo.tokens.jsonc +0 -0
  91. /package/design-tokens/{flex-grid.minimo.tokens.jsonc → tokens/flex-grid.minimo.tokens.jsonc} +0 -0
  92. /package/design-tokens/{form-validation.minimo.tokens.jsonc → tokens/form-validation.minimo.tokens.jsonc} +0 -0
  93. /package/design-tokens/{form.minimo.tokens.jsonc → tokens/form.minimo.tokens.jsonc} +0 -0
  94. /package/design-tokens/{headings.minimo.tokens.jsonc → tokens/headings.minimo.tokens.jsonc} +0 -0
  95. /package/design-tokens/{layout.minimo.tokens.jsonc → tokens/layout.minimo.tokens.jsonc} +0 -0
  96. /package/design-tokens/{link.minimo.tokens.jsonc → tokens/link.minimo.tokens.jsonc} +0 -0
  97. /package/design-tokens/{modal-popup.minimo.tokens.jsonc → tokens/modal-popup.minimo.tokens.jsonc} +0 -0
  98. /package/design-tokens/{radius.minimo.tokens.jsonc → tokens/radius.minimo.tokens.jsonc} +0 -0
  99. /package/design-tokens/{shadows.minimo.tokens.jsonc → tokens/shadows.minimo.tokens.jsonc} +0 -0
  100. /package/design-tokens/{size.minimo.tokens.jsonc → tokens/size.minimo.tokens.jsonc} +0 -0
  101. /package/design-tokens/{status.minimo.tokens.jsonc → tokens/status.minimo.tokens.jsonc} +0 -0
  102. /package/design-tokens/{switch.minimo.tokens.jsonc → tokens/switch.minimo.tokens.jsonc} +0 -0
  103. /package/design-tokens/{table.minimo.tokens.jsonc → tokens/table.minimo.tokens.jsonc} +0 -0
  104. /package/design-tokens/{text.minimo.tokens.jsonc → tokens/text.minimo.tokens.jsonc} +0 -0
  105. /package/design-tokens/{typography.minimo.tokens.jsonc → tokens/typography.minimo.tokens.jsonc} +0 -0
  106. /package/design-tokens/{utility.minimo.tokens.jsonc → tokens/utility.minimo.tokens.jsonc} +0 -0
  107. /package/design-tokens/{web-components → tokens/web-components}/simple-datatable.minimo.tokens.jsonc +0 -0
  108. /package/design-tokens/{z-index.minimo.tokens.jsonc → tokens/z-index.minimo.tokens.jsonc} +0 -0
  109. /package/{design-tokens-utilities → design-tokens/utilities}/build-tokens-src/formats/css.mjs +0 -0
  110. /package/{design-tokens-utilities → design-tokens/utilities}/build-tokens-src/formats/penpot.mjs +0 -0
  111. /package/{design-tokens-utilities → design-tokens/utilities}/build-tokens-src/platforms.mjs +0 -0
  112. /package/{design-tokens-utilities → design-tokens/utilities}/build-tokens-src/transforms.mjs +0 -0
  113. /package/{design-tokens-utilities → design-tokens/utilities}/readme.md +0 -0
  114. /package/{design-tokens-utilities → design-tokens/utilities}/tokens-config-sample.mjs +0 -0
@@ -7,10 +7,17 @@ import * as styles from './modal-popup.module.css';
7
7
  // https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement
8
8
 
9
9
 
10
- // TODO testare la modalità iframe
10
+ // TODO test iframe mode
11
11
 
12
- let dialogEl, dialogContentEl;
12
+ /** @type {HTMLDialogElement | undefined} */
13
+ let dialogEl;
14
+ /** @type {HTMLElement | undefined} */
15
+ let dialogContentEl;
13
16
 
17
+ /**
18
+ * @param {HTMLDialogElement} el
19
+ * @returns {void}
20
+ */
14
21
  function closeDialog(el) {
15
22
  el.classList.remove(styles.on);
16
23
 
@@ -21,43 +28,55 @@ function closeDialog(el) {
21
28
 
22
29
  /**
23
30
  * modalPopup
24
- * Mostra un popup con il contenuto indicato
25
- * il contentuo può essere in forma di:
26
- * - array domBuilder oppure plain text oppure HTML (parametro `content`)
27
- * - contenuto caricato via Ajax (parametri `ajaxUrl` e `ajaxCallback`)
28
- * - iframe (parametro `iframeUrl`)
31
+ * Opens a popup dialog with the given content.
32
+ *
33
+ * Content can be provided as:
34
+ * - a domBuilder array, plain text, or HTML (via `content`)
35
+ * - content loaded via Ajax (via `ajaxUrl` and `ajaxCallback`)
36
+ * - an iframe (via `iframeUrl`)
29
37
  *
30
- * in presenza di più impostazioni in conflitto, viene scelta la prima opzione utile
31
- * nell'ordine di priorità riportato nella lista precedente.
38
+ * When multiple options conflict, the first applicable one in the list above takes precedence.
39
+ *
40
+ * @param {Object} params
41
+ * @param {string | null} [params.dialogExtraClassName=null] - Extra class added to the dialog element.
42
+ * @param {string | null} [params.contentExtraClassName=null] - Extra class added to the content wrapper.
43
+ * @param {string | null} [params.iframeUrl=null] - URL to load in an iframe.
44
+ * @param {string | any[] | null} [params.content=null] - Plain text, HTML, or a domBuilder array.
45
+ * @param {string | null} [params.ajaxUrl=null] - URL for Ajax content loading.
46
+ * @param {((data: *, el: Element) => void) | null} [params.ajaxCallback=null] - Called with the Ajax response and the content element.
47
+ * @param {((el: HTMLDialogElement) => void) | null} [params.closeCallback=null] - Called with the dialog element just before it is removed.
48
+ * @param {boolean} [params.addScrollbarPadding=false] - Adds right padding to compensate for the scrollbar.
49
+ * @param {string | null} [params.headerContent=null] - Header content: plain text or HTML.
50
+ * @param {string | null} [params.footerContent=null] - Footer content: plain text or HTML.
51
+ * @returns {HTMLDialogElement} The dialog element.
32
52
  */
33
-
34
53
  export function modalPopup({
35
54
 
36
55
  /** dialog extra class */
37
56
  dialogExtraClassName = null,
38
57
 
39
- /** extra classname added to dialogInner */
58
+ /** extra classname added to dialogInner */
40
59
  contentExtraClassName = null,
41
60
 
42
61
  /** iframe url */
43
62
  iframeUrl = null,
44
63
 
45
- /** content: plain text, html or domBuilder array */
64
+ /** content: plain text, html or domBuilder array */
46
65
  content = null,
47
66
 
48
- /** url ajax e relatuva funzione di callback.
49
- * Quest'ultima viene invocata con argomenti i dati restituiti dalla chiamata ajax
50
- * e con l''elemento contenitore (`ajaxCallback(data, dialogContentEl)`)
67
+ /** Ajax url and callback.
68
+ * The callback is invoked with the response data and the content container element
69
+ * (`ajaxCallback(data, dialogContentEl)`)
51
70
  */
52
71
  ajaxUrl = null,
53
72
  ajaxCallback = null,
54
73
 
55
- /** eventuale callback da invocare alla chiusra del dialog
56
- * Viene invocato con il le'elento dialog (`closeCallback(dialogEl)`)
57
- * NB: `dialogEl` viene eliminato subito dopo
74
+ /** optional callback invoked when the dialog closes;
75
+ * receives the dialog element (`closeCallback(dialogEl)`)
76
+ * NB: `dialogEl` is removed immediately after
58
77
  */
59
78
  closeCallback = null,
60
- addScrollbarPadding = false, // aggiunge un padding extra a destra per la barra di scorrimento
79
+ addScrollbarPadding = false, // adds extra right padding to compensate for the scrollbar
61
80
 
62
81
  /** header content: plain text or HTML */
63
82
  headerContent = null,
@@ -86,6 +105,9 @@ export function modalPopup({
86
105
  mode = 'iframe';
87
106
  }
88
107
 
108
+ // capture for use in the ajax callback closure (ajaxUrl is non-null when mode === 'ajax')
109
+ const safeAjaxUrl = /** @type {string} */ (ajaxUrl);
110
+
89
111
  domBuilder([
90
112
  {
91
113
  tag: 'dialog',
@@ -93,7 +115,7 @@ export function modalPopup({
93
115
  attrs: {
94
116
  'closedby': 'any'
95
117
  },
96
- callback: el => dialogEl = el,
118
+ callback: el => { dialogEl = /** @type {HTMLDialogElement} */ (el); },
97
119
  children: [
98
120
  {
99
121
  tag: 'button',
@@ -121,7 +143,7 @@ export function modalPopup({
121
143
  : null,
122
144
 
123
145
  children: [
124
- ...(mode === 'domBuilder'? content : []),
146
+ ...(mode === 'domBuilder'? /** @type {any[]} */ (content) : []),
125
147
  {
126
148
  condition: mode === 'iframe',
127
149
  className: styles.iframe,
@@ -138,15 +160,15 @@ export function modalPopup({
138
160
  try {
139
161
 
140
162
  (async () => {
141
- const response = await fetch(ajaxUrl),
163
+ const response = await fetch(safeAjaxUrl),
142
164
  data = await response.json();
143
165
  el.innerHTML = '';
144
- ajaxCallback(data, dialogContentEl);
166
+ ajaxCallback?.(data, dialogContentEl);
145
167
  })();
146
168
 
147
169
  } catch(err) {
148
170
  /* eslint-disable no-console */
149
- console.error(ajaxUrl);
171
+ console.error(safeAjaxUrl);
150
172
  console.error(err);
151
173
  /* eslint-enable no-console */
152
174
  }
@@ -165,15 +187,15 @@ export function modalPopup({
165
187
  }
166
188
  ], document.body);
167
189
 
168
- // snapshot: dialogEl potrebbe essere sovrascritto da una seconda chiamata a modalPopup
169
- const thisDialog = dialogEl;
190
+ // snapshot: dialogEl could be overwritten by a subsequent modalPopup call
191
+ const thisDialog = /** @type {HTMLDialogElement} */ (dialogEl);
170
192
 
171
- // listener sul pulsante di chiusura
172
- thisDialog.querySelector(`.${styles.closeButton}`).addEventListener('click', () => {
193
+ // close button listener
194
+ thisDialog.querySelector(`.${styles.closeButton}`)?.addEventListener('click', () => {
173
195
  closeDialog(thisDialog);
174
196
  }, false);
175
197
 
176
- // // Esc e clic sul backdrop
198
+ // // Esc and backdrop click
177
199
  // thisDialog.addEventListener('cancel', (e) => {
178
200
  // e.preventDefault();
179
201
  // closeDialog(thisDialog);
@@ -1,11 +1,23 @@
1
- import './overlay.css';
1
+ import './overlay.css';
2
2
 
3
+ /**
4
+ * Appends an overlay `div` to the given context element.
5
+ * @param {Element} [context=document.body]
6
+ * @param {boolean} [scroll_lock=false] - When true, locks body scroll via `overflow-hidden`.
7
+ * @returns {void}
8
+ */
3
9
  export function overlay(context = document.body, scroll_lock = false) {
4
10
  context.insertAdjacentHTML('beforeend', '<div class="overlay"/>');
5
11
  if(scroll_lock) {
6
12
  document.body.classList.add('overflow-hidden');
7
13
  }
8
14
  }
15
+
16
+ /**
17
+ * Removes the overlay element from the given context and re-enables body scroll.
18
+ * @param {Element} [context=document.body]
19
+ * @returns {void}
20
+ */
9
21
  export function removeOverlay(context = document.body) {
10
22
  context.querySelector(':scope > .overlay')?.remove();
11
23
  document.body.classList.remove('overflow-hidden');
@@ -1,5 +1,15 @@
1
- // import './sf-macro.css'; // da importare nel progetto
2
-
1
+ // import './sf-macro.css'; // import in the consuming project
2
+
3
+ /**
4
+ * Initialises Symfony macro collection fields (repeatable fieldsets).
5
+ * @param {Object} [options={}]
6
+ * @param {string} [options.wrapper_selector='.sf-macro-wrapper'] - Selector for the outer wrapper element.
7
+ * @param {string} [options.row_selector='.sf-macro-riga'] - Selector for each repeatable row.
8
+ * @param {((newRow: Element | null, addBtn: Element | null) => void) | null} [options.add_callback=null] - Called after a row is added; receives the new row and the add button.
9
+ * @param {(() => void) | null} [options.del_callback=null] - Called after a row is removed.
10
+ * @param {boolean} [options.insertAtTop=false] - When true, new rows are inserted at the top.
11
+ * @returns {void}
12
+ */
3
13
  export function sf_macro({
4
14
  wrapper_selector = '.sf-macro-wrapper',
5
15
  row_selector = '.sf-macro-riga',
@@ -9,19 +19,20 @@ export function sf_macro({
9
19
  }={}) {
10
20
 
11
21
  document.querySelectorAll(wrapper_selector).forEach(fset => {
12
- const macro_container = fset.querySelector('.sf-macro-container')
13
- ,macro_template = macro_container.dataset.template
14
- ;
22
+ const macro_container = /** @type {HTMLElement | null} */ (fset.querySelector('.sf-macro-container'));
23
+ if (!macro_container) return;
24
+ const macro_template = macro_container.dataset.template ?? '';
15
25
 
16
- // listeners
26
+ // event listeners
17
27
  fset.addEventListener('click', e => {
28
+ const target = /** @type {HTMLElement} */ (e.target);
18
29
 
19
- if(e.target.closest('.sf-macro-riga-add')) {
30
+ if(target.closest('.sf-macro-riga-add')) {
20
31
 
21
32
  const righe_macro = macro_container.querySelectorAll(row_selector).length;
22
33
 
23
34
  macro_container.insertAdjacentHTML(insertAtTop? 'afterbegin' : 'beforeend',
24
- macro_template.replace(/__indice\d?__/g, righe_macro + 1)
35
+ macro_template.replace(/__indice\d?__/g, String(righe_macro + 1))
25
36
  );
26
37
 
27
38
  if(add_callback && typeof add_callback === 'function') {
@@ -29,13 +40,13 @@ export function sf_macro({
29
40
  insertAtTop
30
41
  ? macro_container.querySelector('.sf-macro-riga:first-child')
31
42
  : macro_container.querySelector('.sf-macro-riga:last-child'),
32
- e.target.closest('.sf-macro-riga-add')
43
+ target.closest('.sf-macro-riga-add')
33
44
  );
34
45
  }
35
46
 
36
47
 
37
- } else if(e.target.closest('.sf-macro-close-btn')) {
38
- e.target.closest(row_selector).remove();
48
+ } else if(target.closest('.sf-macro-close-btn')) {
49
+ target.closest(row_selector)?.remove();
39
50
 
40
51
  if(del_callback && typeof del_callback === 'function') {
41
52
  del_callback();
@@ -43,5 +54,5 @@ export function sf_macro({
43
54
  }
44
55
 
45
56
  }, false);
46
- }); // end foreach wrapper_selector
57
+ });
47
58
  }
@@ -14,9 +14,19 @@ References
14
14
  */
15
15
 
16
16
  // TODO snackbar action
17
- // TODO gestione snackbar multiple (NO stacking)
18
-
19
-
17
+ // TODO handle multiple simultaneous snackbars (no stacking)
18
+
19
+ /**
20
+ * Displays a snackbar / toast notification using the Popover API.
21
+ * @param {string} message - The message to display.
22
+ * @param {Object} [options={}]
23
+ * @param {string | null} [options.status=null] - Status variant: `danger`, `warning`, `info`, or `success`.
24
+ * @param {number | false | null} [options.duration=4000] - Auto-close delay in ms; `false` or `null` disables auto-close.
25
+ * @param {boolean} [options.close_btn=true] - Whether to show the close button.
26
+ * @param {Function | null} [options.action=null] - Action callback.
27
+ * @param {string | null} [options.action_text=null] - Label for the action button.
28
+ * @returns {{ close: function(): void }}
29
+ */
20
30
  export function snackbar(message, options = {}){
21
31
 
22
32
  options = {
@@ -31,20 +41,24 @@ export function snackbar(message, options = {}){
31
41
  ...options
32
42
  };
33
43
 
34
- let _popover, _timeoutID, _isRemoving = false;
44
+ /** @type {HTMLElement | null} */
45
+ let _popover = null;
46
+ /** @type {ReturnType<typeof setTimeout> | undefined} */
47
+ let _timeoutID;
48
+ let _isRemoving = false;
35
49
 
36
50
  const removePopover = () => {
37
51
  if (_isRemoving || !_popover?.matches(':popover-open')) return;
38
- _isRemoving = true; // evita sovrapposizioni nel caso in cui removePopover sia chiamato più volte durante l'animazione di chiusura
52
+ _isRemoving = true; // prevents double-removal if called multiple times during the close animation
39
53
 
40
54
  if (_timeoutID) clearTimeout(_timeoutID);
41
55
 
42
- _popover.classList.add(styles.isHiding);
56
+ _popover?.classList.add(styles.isHiding);
43
57
 
44
- _popover.addEventListener('animationend', () => {
45
- _popover.hidePopover();
46
- _popover.remove();
47
- }, { once: true }); // `once` già evita duplicati sul listener, ma il flag protegge prima
58
+ _popover?.addEventListener('animationend', () => {
59
+ _popover?.hidePopover();
60
+ _popover?.remove();
61
+ }, { once: true }); // 'once' prevents duplicate listeners; the flag guards against calls before animation ends
48
62
  };
49
63
 
50
64
  _popover = domBuilder([
@@ -52,7 +66,7 @@ export function snackbar(message, options = {}){
52
66
  className: classnames(styles.snackbarOuter, options.status? styles[`status-${options.status}`] : null),
53
67
  attrs: {
54
68
  popover: 'manual',
55
- role: (options.action || ['danger','warning'].includes(options.status)) ? 'alert' : 'status'
69
+ role: (options.action || (options.status != null && ['danger','warning'].includes(options.status))) ? 'alert' : 'status'
56
70
  },
57
71
  children: [
58
72
  {
@@ -85,16 +99,17 @@ export function snackbar(message, options = {}){
85
99
  }
86
100
  ], document.body);
87
101
 
88
- _popover.showPopover();
102
+ _popover?.showPopover();
89
103
 
90
104
 
91
105
  if(options.duration) {
92
- _timeoutID = setTimeout(removePopover, options.duration);
106
+ const duration = options.duration; // const preserves narrowing inside closures
107
+ _timeoutID = setTimeout(removePopover, duration);
93
108
 
94
- // mette in pausa il timeout sull'hover
95
- _popover.addEventListener('mouseenter', () => clearTimeout(_timeoutID));
96
- _popover.addEventListener('mouseleave', () => {
97
- _timeoutID = setTimeout(removePopover, options.duration);
109
+ // pause the auto-close timeout on hover
110
+ _popover?.addEventListener('mouseenter', () => clearTimeout(_timeoutID));
111
+ _popover?.addEventListener('mouseleave', () => {
112
+ _timeoutID = setTimeout(removePopover, duration);
98
113
  });
99
114
 
100
115
  }
@@ -1,8 +1,12 @@
1
1
  import * as styles from './spinner.module.css';
2
2
 
3
- // NB la classe spinner è legata al tipo di spinner che si decide di usare, il cui file va incluso
4
- // TODO sistema per coinfigurare il tipo di spinner da utilizzare, per permettere l'uso di più tipi di spinner nello stesso progetto
3
+ // The spinner class depends on the spinner type in use; the corresponding CSS file must be included.
4
+ // TODO: system for configuring the spinner type to allow multiple spinner types in the same project
5
5
 
6
+ /**
7
+ * Returns the HTML markup for a loading spinner.
8
+ * @returns {string}
9
+ */
6
10
  export function spinner() {
7
11
  return `<div class="${styles.spinnerWrapper}"><div class="spinner">Loading...</div></div>`;
8
12
  }
@@ -1,4 +1,25 @@
1
1
 
2
+ /**
3
+ * @typedef {Object} UnsplashPhoto
4
+ * @property {string} id
5
+ * @property {string} color
6
+ * @property {number} width
7
+ * @property {number} height
8
+ * @property {string} alt_description
9
+ * @property {string} date
10
+ * @property {string} base_url
11
+ * @property {string} unsplash_url
12
+ * @property {string} author
13
+ * @property {string} author_profile
14
+ * @property {string} blur_hash
15
+ * @property {string} image_description
16
+ */
17
+
18
+ /**
19
+ * Fetches and normalises Unsplash photo data from the given URL.
20
+ * @param {{ unsplash_data_url: string | null }} settings
21
+ * @returns {Promise<UnsplashPhoto | undefined>}
22
+ */
2
23
  export async function fetchUnsplashData(settings) {
3
24
 
4
25
  const default_settings = {
@@ -13,11 +34,13 @@ export async function fetchUnsplashData(settings) {
13
34
  throw new Error( 'Unsplash data url must be defined' );
14
35
  }
15
36
 
37
+ const url = settings.unsplash_data_url; // const preserves narrowing inside the IIFE closure
38
+
16
39
  const imgData = await (async () => {
17
- const response = await fetch(settings.unsplash_data_url);
40
+ const response = await fetch(url);
18
41
  if (!response.ok) {
19
42
  /* eslint-disable no-console */
20
- console.error('Ajax error on: ' + settings.unsplash_data_url);
43
+ console.error('Ajax error on: ' + url);
21
44
  console.error(response);
22
45
  /* eslint-enable no-console */
23
46
  throw new Error( `Loading error: ${response.status}` );
@@ -1,3 +1,14 @@
1
+ /**
2
+ * @typedef {Object} Breakpoint
3
+ * @property {string} name - Breakpoint identifier.
4
+ * @property {string} mq - Media query string.
5
+ * @property {number} w - Image width in pixels.
6
+ * @property {number} h - Image height in pixels.
7
+ * @property {boolean} dpr2 - Whether to include a 2× DPR srcset entry.
8
+ * @property {string} [ar] - Aspect ratio (e.g. '16/9').
9
+ */
10
+
11
+ /** @type {Breakpoint[]} */
1
12
  export const breakpoints = [
2
13
  {
3
14
  name: 'xxl',
@@ -4,6 +4,21 @@ import { breakpoints } from './img-breakpoints.js';
4
4
  import { fetchUnsplashData } from './fetch-unsplash-data.js';
5
5
  import { decode } from 'blurhash';
6
6
 
7
+ /**
8
+ * Initialises an Unsplash full-page photo with blurhash placeholder, responsive picture element,
9
+ * and attribution overlay.
10
+ * @param {Object} settings
11
+ * @param {HTMLElement | null} settings.targetElement - Container element where the photo will be rendered.
12
+ * @param {string | null} [settings.className=null] - Extra class added to the container.
13
+ * @param {string | null} settings.unsplashDataUrl - URL returning Unsplash photo JSON.
14
+ * @param {string | null} settings.utmSource - UTM source value for attribution links.
15
+ * @param {string | null} settings.title - Main heading text.
16
+ * @param {string | null} [settings.text=null] - Optional body text.
17
+ * @param {string | null} [settings.backLink=null] - Optional back-link HTML.
18
+ * @param {boolean} [settings.hidePhotoLink=true] - When true, the Unsplash link is shown only on hover.
19
+ * @param {Record<string, string> | null} [settings.cssModulesObj=null] - CSS Modules class map; when null, class names are used as-is.
20
+ * @returns {void}
21
+ */
7
22
  export function unsplashPageJS(settings) {
8
23
 
9
24
  const default_settings = {
@@ -24,22 +39,26 @@ export function unsplashPageJS(settings) {
24
39
 
25
40
  try {
26
41
 
42
+ const settingsMap = /** @type {Record<string, unknown>} */ (settings);
27
43
  if(required_settings
28
- .map(i => settings[i])
44
+ .map(i => settingsMap[i])
29
45
  .filter(i => i !== null && i !== '').length !== required_settings.length) {
30
46
 
31
47
  throw new Error( `I parametri ${required_settings.map(i => `\`${i}\``).join(',')} sono obbligatori` );
32
48
  }
33
49
 
50
+ const targetElement = /** @type {HTMLElement} */ (settings.targetElement);
34
51
 
35
- const cssClass = classname => settings.cssModulesObj? settings.cssModulesObj[classname] : classname;
52
+ const cssClass = /** @param {string} classname */ classname =>
53
+ settings.cssModulesObj? settings.cssModulesObj[classname] : classname;
36
54
 
37
- settings.targetElement.innerHTML = `<div class="${cssClass('upContainer') + (settings.className? ` ${settings.className}` : '')}"></div>`;
38
- const container = settings.targetElement.querySelector('.' + cssClass('upContainer'));
55
+ targetElement.innerHTML = `<div class="${cssClass('upContainer') + (settings.className? ` ${settings.className}` : '')}"></div>`;
56
+ const container = /** @type {HTMLElement} */ (targetElement.querySelector('.' + cssClass('upContainer')));
39
57
  container.innerHTML = `<div class="${cssClass('upLoaderWrapper')}"><div class="${cssClass('upLoader')}"></div></div>`;
40
58
 
41
59
  fetchUnsplashData({unsplash_data_url: settings.unsplashDataUrl})
42
60
  .then(photo => {
61
+ if (!photo) return;
43
62
 
44
63
  const pixels = decode(photo.blur_hash, container.offsetWidth, container.offsetHeight);
45
64
 
@@ -48,9 +67,11 @@ export function unsplashPageJS(settings) {
48
67
  canvas.width = container.offsetWidth;
49
68
  canvas.height = container.offsetHeight;
50
69
  const ctx = canvas.getContext('2d');
51
- const imageData = ctx.createImageData(container.offsetWidth, container.offsetHeight);
52
- imageData.data.set(pixels);
53
- ctx.putImageData(imageData, 0, 0);
70
+ if (ctx) {
71
+ const imageData = ctx.createImageData(container.offsetWidth, container.offsetHeight);
72
+ imageData.data.set(pixels);
73
+ ctx.putImageData(imageData, 0, 0);
74
+ }
54
75
 
55
76
 
56
77
  container.insertAdjacentElement('afterbegin', canvas);
@@ -100,42 +121,45 @@ export function unsplashPageJS(settings) {
100
121
  '</picture>'
101
122
  ); // end insert <picture>
102
123
 
103
- const img = container.querySelector('.' + cssClass('unsplashPhoto'));
104
- img.onload = () => {
105
- container.querySelector('.' + cssClass('upLoaderWrapper'))?.remove();
106
- container.classList.add(cssClass('show'));
107
-
108
- container.insertAdjacentHTML('beforeend', `<div class="${cssClass('upMessageBox')}">
109
- <div class="${cssClass('upMessage')}">
110
- <h1>${settings.title}</h1>
111
- ${settings.text? `<p>${settings.text}</p>` : ''}
112
- ${settings.backLink ? `<p class="${cssClass('upBackLink')}">${settings.backLink}</p>` : ''}
113
-
114
- <div class="${cssClass('upArrowWrapper')}" role="button">
115
- <img src="${arrowIcon}" alt="Icona freccia">
124
+ const img = /** @type {HTMLImageElement | null} */ (container.querySelector('.' + cssClass('unsplashPhoto')));
125
+ if (img) {
126
+ img.onload = () => {
127
+ container.querySelector('.' + cssClass('upLoaderWrapper'))?.remove();
128
+ container.classList.add(cssClass('show'));
129
+
130
+ container.insertAdjacentHTML('beforeend', `<div class="${cssClass('upMessageBox')}">
131
+ <div class="${cssClass('upMessage')}">
132
+ <h1>${settings.title}</h1>
133
+ ${settings.text? `<p>${settings.text}</p>` : ''}
134
+ ${settings.backLink ? `<p class="${cssClass('upBackLink')}">${settings.backLink}</p>` : ''}
135
+
136
+ <div class="${cssClass('upArrowWrapper')}" role="button">
137
+ <img src="${arrowIcon}" alt="Icona freccia">
138
+ </div>
116
139
  </div>
117
- </div>
118
140
 
119
- <div class="${cssClass('upCredits')}">
120
- <em>${photo.image_description}</em>
121
- <span>Photo
122
- <a href="${`${photo.author_profile}?utmSource=${settings.utmSource}&utm_medium=referral`}">
123
- ${photo.author} / Unsplash
124
- </a>
125
- </span>
141
+ <div class="${cssClass('upCredits')}">
142
+ <em>${photo.image_description}</em>
143
+ <span>Photo
144
+ <a href="${`${photo.author_profile}?utmSource=${settings.utmSource}&utm_medium=referral`}">
145
+ ${photo.author} / Unsplash
146
+ </a>
147
+ </span>
148
+ </div>
126
149
  </div>
127
- </div>
128
- <div class="${cssClass('unsplashPhotoLink')}${settings.hidePhotoLink? ` ${cssClass('upShowOnHover')}` : ''}">
129
- <a href="${`${photo.unsplash_url}?utm_source=${settings.utmSource}&utm_medium=referral`}" target="_blank" rel="noopener noreferrer">
130
- <img src="${imageIcon}" alt="Icona immagine" />
131
- </a>
132
- </div>`);
133
-
134
- container.querySelector('.' + cssClass('upArrowWrapper')).addEventListener('click', e => {
135
- e.target.closest(`.${cssClass('upMessage')}`).classList.toggle(cssClass('hidden'));
136
- }, false);
137
-
138
- }; // end img.onload
150
+ <div class="${cssClass('unsplashPhotoLink')}${settings.hidePhotoLink? ` ${cssClass('upShowOnHover')}` : ''}">
151
+ <a href="${`${photo.unsplash_url}?utm_source=${settings.utmSource}&utm_medium=referral`}" target="_blank" rel="noopener noreferrer">
152
+ <img src="${imageIcon}" alt="Icona immagine" />
153
+ </a>
154
+ </div>`);
155
+
156
+ container.querySelector('.' + cssClass('upArrowWrapper'))?.addEventListener('click', e => {
157
+ const target = /** @type {HTMLElement} */ (e.target);
158
+ target.closest(`.${cssClass('upMessage')}`)?.classList.toggle(cssClass('hidden'));
159
+ }, false);
160
+
161
+ }; // end img.onload
162
+ }
139
163
 
140
164
  }); // end .then
141
165
 
@@ -1,6 +1,6 @@
1
1
  /*
2
2
  PERSONALIZZAZIONE
3
- custom properties (valori di default, vedi /design-tokens-src/components/unsplash-page.minimo.tokens.mjs):
3
+ custom properties (valori di default, vedi /design-tokens/_src/components/unsplash-page.minimo.tokens.mjs):
4
4
 
5
5
  Basic props:
6
6
  --up-bg-color: hsl(0deg 0% 0% / .7);
@@ -148,6 +148,10 @@ input::-webkit-calendar-picker-trigger-icon { /* solo in chromium, non in safari
148
148
  textarea.form-control {
149
149
  flex: 1;
150
150
  min-height: calc(3.5lh + var(--form-control-padding-block) * 2);
151
+
152
+ &.autosize {
153
+ field-sizing: content;
154
+ }
151
155
  }
152
156
 
153
157
 
@@ -1,14 +1,27 @@
1
+ /** @typedef {string | null | undefined | false | 0} ClassValue */
2
+
3
+ /**
4
+ * Joins CSS class names, filtering out falsy and non-string values.
5
+ * Accepts strings, falsy values, and (nested) arrays of the same.
6
+ * @param {...(ClassValue | ClassValue[])} args
7
+ * @returns {string}
8
+ */
1
9
  export function classnames(...args) {
2
10
  return args.flat().filter(x => {
3
11
  if (x && typeof x !== 'string') {
4
12
  // eslint-disable-next-line no-console
5
- console.error('[classnames] valore non stringa ignorato in ', ...args);
13
+ console.error('[classnames] non-string value ignored: ', ...args);
6
14
  return false;
7
15
  }
8
16
  return Boolean(x);
9
17
  }).join(' ');
10
18
  }
11
19
 
20
+ /**
21
+ * Like {@link classnames}, but returns `null` when the result is an empty string.
22
+ * @param {...(ClassValue | ClassValue[])} args
23
+ * @returns {string | null}
24
+ */
12
25
  export function classnamesNull(...args) {
13
26
  return classnames(...args) || null;
14
27
  }
@@ -0,0 +1,19 @@
1
+ import { stripTags } from './strip-tags.js';
2
+
3
+ /**
4
+ * Creates an id from a string (useful for generating automatic anchors from tag content).
5
+ * Strips HTML tags, normalizes accented characters, punctuation, and whitespace.
6
+ * @param {string | null | undefined} str
7
+ * @returns {string}
8
+ */
9
+ export function createId(str) {
10
+ return (stripTags(str) || '')
11
+ .toLowerCase()
12
+ .normalize('NFD')
13
+ .replace(/[\u0300-\u036f]/g, '')
14
+ .replace(/[\p{P}\p{S}]+/gu, '') // strips all Unicode punctuation and symbols
15
+ .replace(/\s+/g, '-')
16
+ .replace(/^-+|-+$/g, '')
17
+ .replace(/-+/g, '-')
18
+ .replace(/[^a-z0-9-]/g, '');
19
+ }