@massimo-cassandro/minimo 1.29.5 → 1.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/charts/index.js CHANGED
@@ -4,9 +4,9 @@ import { MinimoCharts } from './src/helpers/core.js';
4
4
  import { bars } from './src/bars.js';
5
5
  import { goal } from './src/goal.js';
6
6
  import { lines } from './src/lines.js';
7
+ import { hbars } from './src/hbars.js';
7
8
  // import { ratingDisplay } from './src/rating-display/index.js';
8
9
  // import { pie } from './src/charts/pie.js';
9
- // import { hbars } from './src/charts/hbars.js';
10
10
  // import { signalBars } from './src/charts/signal-bars.js';
11
11
  // import { radialBars } from './src/charts/radial-bars.js';
12
12
  // import { settimanaleMensile } from './src/charts/settimanale-mensile.js';
@@ -17,6 +17,7 @@ import { lines } from './src/lines.js';
17
17
  export const barsChart = (chartInstance, params) => bars.call(chartInstance, params);
18
18
  export const goalChart = (chartInstance, params) => goal.call(chartInstance, params);
19
19
  export const linesChart = (chartInstance, params) => lines.call(chartInstance, params);
20
+ export const hBarsChart = (chartInstance, params) => hbars.call(chartInstance, params);
20
21
  // export const ratingDisplayChart = (chartInstance, params) => ratingDisplay.call(chartInstance, params);
21
22
 
22
23
  export { MinimoCharts };
@@ -101,9 +101,14 @@ const default_params = {
101
101
  xAxisTitleAttrs: {...default_font_attrs, size: 18},
102
102
  xAxisTitleFontFilePath: null,
103
103
 
104
- /** valori minimi e massimi per l'asse Y, se null vengono calcolati automaticamente */
104
+ /**
105
+ valori minimi e massimi per l'asse Y, se null vengono calcolati automaticamente.
106
+ Il calcolo automatico di min_value usa 0 come soglia massima, per far sì che le
107
+ barre partano da 0 quando i dati sono tutti positivi, estendendo il minimo verso
108
+ il basso solo se sono presenti valori negativi
109
+ */
105
110
  max_value: null,
106
- min_value: 0,
111
+ min_value: null,
107
112
 
108
113
  /**
109
114
  classi e attributi dei vari elementi.
@@ -173,6 +178,11 @@ export async function bars(params) {
173
178
  throw 'Tutti i valori dei sub-array di `values` devono essere numerici';
174
179
  }
175
180
 
181
+ // calcolo automatico di min_value: 0 se i dati sono tutti positivi,
182
+ // altrimenti il valore minimo effettivo (per includere le barre negative nell'area del grafico)
183
+ if(params.min_value == null) {
184
+ params.min_value = Math.min(0, ...flatValues.filter(v => v != null));
185
+ }
176
186
 
177
187
  // container
178
188
  [, params.width, params.height] = parseContainer({ container: params.container, width: params.width, height: params.height });
@@ -237,9 +247,11 @@ export async function bars(params) {
237
247
  max_value,
238
248
  min_value,
239
249
  chart_area,
250
+ stepY_count,
240
251
  stepY_value,
241
252
  stepY_size,
242
- stepX_size
253
+ stepX_size,
254
+ zero_y
243
255
  } = parseStdAxisOpts({
244
256
  max_value: params.max_value,
245
257
  min_value: params.min_value,
@@ -266,10 +278,12 @@ export async function bars(params) {
266
278
  max_value,
267
279
  min_value,
268
280
  chart_area,
281
+ stepY_count,
269
282
  stepY_value,
270
283
  stepY_size,
271
284
  stepX_size,
272
- stepX_count
285
+ stepX_count,
286
+ zero_y
273
287
  }
274
288
  });
275
289
 
@@ -281,7 +295,9 @@ export async function bars(params) {
281
295
 
282
296
  // calcolo larghezza barre
283
297
  const barsWidth = (stepX_size - params.seriesGap - (params.barsGap * (params.values.length - 1))) / params.values.length
284
- ,barsCornerRadius = Math.min(params.barsCornerRadius, barsWidth / 2) // il reggio dell'angolo non può essere più grandedella barra stessa
298
+ // il raggio dell'angolo non può essere più grande della barra stessa, né negativo
299
+ // (barsWidth può risultare negativo se seriesGap/barsGap non lasciano spazio sufficiente per tutte le serie)
300
+ ,barsCornerRadius = Math.max(0, Math.min(params.barsCornerRadius, barsWidth / 2))
285
301
  ;
286
302
 
287
303
 
@@ -303,9 +319,15 @@ export async function bars(params) {
303
319
  const isLast = idx === thisStepValues.length - 1;
304
320
 
305
321
  if (itemValue != null) {
306
- // calcolo coordinata Y e altezza assoluta della barra
307
- const barY = chart_area.top + chart_area.height - (((itemValue - min_value) * chart_area.height) / (max_value - min_value)),
308
- barH = chart_area.bottom - barY,
322
+ // valori negativi: la barra scende dalla linea dello zero verso il basso,
323
+ // invece di crescere dal basso verso l'alto
324
+ const isNegative = itemValue < 0,
325
+ // calcolo coordinata Y del valore e altezza assoluta della barra,
326
+ // usando come baseline la linea dello zero (zero_y) invece del fondo del grafico
327
+ itemY = chart_area.top + chart_area.height - (((itemValue - min_value) * chart_area.height) / (max_value - min_value)),
328
+ barTop = isNegative? zero_y : itemY,
329
+ barBottom = isNegative? itemY : zero_y,
330
+ barH = barBottom - barTop,
309
331
  parteVerticaleBarra = barH - barsCornerRadius;
310
332
  let path;
311
333
 
@@ -315,20 +337,36 @@ export async function bars(params) {
315
337
 
316
338
  const altezzaCornerRadius = barH;
317
339
 
318
- path = `M${barX},${chart_area.bottom}` +
319
- `q0,-${altezzaCornerRadius} ${barsCornerRadius},-${altezzaCornerRadius}` +
320
- `h${barsWidth - barsCornerRadius * 2}` +
321
- `q${barsCornerRadius},0 ${barsCornerRadius},${altezzaCornerRadius}` +
322
- 'z';
340
+ path = isNegative
341
+ // angoli arrotondati in basso (lato lontano dallo zero), spigolo vivo in alto
342
+ ? `M${barX},${barTop}` +
343
+ `q0,${altezzaCornerRadius} ${barsCornerRadius},${altezzaCornerRadius}` +
344
+ `h${barsWidth - barsCornerRadius * 2}` +
345
+ `q${barsCornerRadius},0 ${barsCornerRadius},-${altezzaCornerRadius}` +
346
+ 'z'
347
+ // angoli arrotondati in alto (lato lontano dallo zero), spigolo vivo in basso
348
+ : `M${barX},${barBottom}` +
349
+ `q0,-${altezzaCornerRadius} ${barsCornerRadius},-${altezzaCornerRadius}` +
350
+ `h${barsWidth - barsCornerRadius * 2}` +
351
+ `q${barsCornerRadius},0 ${barsCornerRadius},${altezzaCornerRadius}` +
352
+ 'z';
323
353
 
324
354
  } else {
325
- path = `M${barX},${chart_area.bottom}` +
326
- `v-${parteVerticaleBarra}` +
327
- `q0,-${barsCornerRadius} ${barsCornerRadius},-${barsCornerRadius}` +
328
- `h${barsWidth - barsCornerRadius * 2}` +
329
- `q${barsCornerRadius},0 ${barsCornerRadius},${barsCornerRadius}` +
330
- `v${parteVerticaleBarra}` +
331
- 'z';
355
+ path = isNegative
356
+ ? `M${barX},${barTop}` +
357
+ `v${parteVerticaleBarra}` +
358
+ `q0,${barsCornerRadius} ${barsCornerRadius},${barsCornerRadius}` +
359
+ `h${barsWidth - barsCornerRadius * 2}` +
360
+ `q${barsCornerRadius},0 ${barsCornerRadius},-${barsCornerRadius}` +
361
+ `v-${parteVerticaleBarra}` +
362
+ 'z'
363
+ : `M${barX},${barBottom}` +
364
+ `v-${parteVerticaleBarra}` +
365
+ `q0,-${barsCornerRadius} ${barsCornerRadius},-${barsCornerRadius}` +
366
+ `h${barsWidth - barsCornerRadius * 2}` +
367
+ `q${barsCornerRadius},0 ${barsCornerRadius},${barsCornerRadius}` +
368
+ `v${parteVerticaleBarra}` +
369
+ 'z';
332
370
  }
333
371
 
334
372
  barsGroup.path(path)
@@ -0,0 +1,419 @@
1
+ // barre orizzontali — analogo di bars.js con assi invertiti:
2
+ // categorie sull'asse verticale (a sinistra), valori sull'asse orizzontale (in basso)
3
+
4
+ import { parseStdAxisOpts } from './helpers/parse-std-axis-opts.js';
5
+ import { parseContainer } from './helpers/chart-utils.js';
6
+ import { cartesianAxis } from './helpers/cartesian-axis.js';
7
+ import { legenda } from './helpers/legenda.js';
8
+
9
+ const default_lines_attrs = {
10
+ stroke: '#999',
11
+ 'stroke-width': 1,
12
+ 'shape-rendering': 'crispEdges',
13
+ 'stroke-linecap': 'round',
14
+ fill: 'none'
15
+ };
16
+
17
+ const default_font_attrs = {
18
+ // family: 'sans-serif',
19
+ size: 14,
20
+ // stretch: 'normal',
21
+ // style: 'normal',
22
+ // variant: 'normal',
23
+ weight: 400,
24
+ fill: '#999'
25
+ };
26
+
27
+ const default_params = {
28
+
29
+ /** mostra info utili per il debug */
30
+ debug: false,
31
+
32
+ /** container (selettore o elemento DOM), se null viene restituito il codice SVG */
33
+ container: null,
34
+
35
+ /**
36
+ larghezza e altezza del grafico (px),
37
+ Se il container è presente e i parametro `width` e `height` sono `null`,
38
+ vengono utilizzate le dimensioni del container.
39
+ Se uno dei due valori non è impostato o ricavabile, viene generato un errore.
40
+ NB: il padding del container viene considerato nelle dimensioni del grafico,
41
+ è preferibile non impostarlo.
42
+ */
43
+ width: null, // null || <value>
44
+ height: 300, // <value> || null
45
+
46
+ /** attributi facoltativi per l'svg (classe, ecc) */
47
+ svgAttrs: {},
48
+
49
+ /**
50
+ valori numerici per la definizione delle barre
51
+ è un array di array, in cui ogni subarray contiene i valori di una singola serie di barre
52
+ */
53
+ values: [],
54
+
55
+ /** spazio tra una barra e l'altra, all'interno dello stesso gruppo di categoria */
56
+ barsGap: 5,
57
+
58
+ /** spazio tra un gruppo di categoria e l'altro */
59
+ seriesGap: 10,
60
+
61
+ /** bars corner radius (lato esterno, lontano dallo zero) */
62
+ barsCornerRadius: 6,
63
+
64
+ /**
65
+ etichette di categoria, una per ogni gruppo di barre. Sono posizionate sull'asse verticale (a sinistra)
66
+ */
67
+ xLabels: [],
68
+
69
+ /** spazio extra attorno al bordo */
70
+ padding: 10,
71
+
72
+ /** suddivisioni asse dei valori (in basso) */
73
+ stepY_count: 4,
74
+
75
+ /** px, altezza o larghezza ticks (secondo l'orientamento) */
76
+ ticksLength: 8,
77
+
78
+ /**
79
+ opzioni per legenda come da componente legenda sono impostati automaticamente)
80
+ NB: l'elemento `items` viene generato dinamicamente:
81
+ `shape` è sempre impostato su line
82
+ `attrs` è ricavato dagli attributi delle linee dati
83
+
84
+ Per utilizzare le impostazioni di default, è sufficiente impostare i valori `items.text`
85
+ */
86
+ legenda: {},
87
+
88
+ /**
89
+ larghezza area etichette di categoria (asse verticale, a sinistra)
90
+ */
91
+ yAxisLabelWidth: 60,
92
+
93
+ /** altezza area etichette dell'asse dei valori (in basso) */
94
+ xAxisLabelHeight: 50,
95
+
96
+ /** titoli assi: yAxisTitle → asse categorie (a sinistra), xAxisTitle → asse valori (in basso) */
97
+ yAxisTitle: null,
98
+ yAxisTitleAttrs: {...default_font_attrs, size: 18},
99
+ yAxisTitleFontFilePath: null,
100
+
101
+ xAxisTitle: null,
102
+ xAxisTitleAttrs: {...default_font_attrs, size: 18},
103
+ xAxisTitleFontFilePath: null,
104
+
105
+ /**
106
+ valori minimi e massimi per l'asse dei valori, se null vengono calcolati automaticamente.
107
+ Il calcolo automatico di min_value usa 0 come soglia massima, per far sì che le
108
+ barre partano da 0 quando i dati sono tutti positivi, estendendo il minimo verso
109
+ sinistra solo se sono presenti valori negativi
110
+ */
111
+ max_value: null,
112
+ min_value: null,
113
+
114
+ /**
115
+ classi e attributi dei vari elementi.
116
+ Per gli attributi, ogni variabile è un oggetto di attributi svg.js, es =
117
+
118
+ {
119
+ stroke: '#414141',
120
+ 'stroke-width': 1,
121
+ 'shape-rendering': 'crispEdges',
122
+ class: 'xxxx'
123
+ }
124
+
125
+ */
126
+ axisAttrs: {...default_lines_attrs, 'stroke-width': 2},
127
+ ticksAttrs: default_lines_attrs,
128
+
129
+ /**
130
+ griglia: showXgrid → griglia orizzontale (asse categorie), showYgrid → griglia verticale (asse valori)
131
+ */
132
+ showXgrid: false,
133
+ showYgrid: true,
134
+ gridLinesAttrs: default_lines_attrs,
135
+
136
+ labelXAttrs: default_font_attrs, // etichette di categoria (a sinistra)
137
+ labelXFontFilePath: null, // percorso font per textToPath
138
+
139
+ labelYAttrs: default_font_attrs, // etichette dell'asse dei valori (in basso)
140
+ labelYFontFilePath: null, // percorso font per textToPath
141
+
142
+
143
+
144
+ /** colori delle barre */
145
+ data_colors: null,
146
+
147
+
148
+ /**
149
+ Funzione per formattare le etichette dell'asse dei valori (in basso).
150
+ Null per nessuna eleborazione.
151
+ La funzione di default assume che il valore sia di tipo numerico
152
+ */
153
+ labelYFormatter: etiValue => Math.ceil(etiValue / 1e3)
154
+ .toLocaleString('it-IT', {
155
+ minimumFractionDigits: 0,
156
+ maximumFractionDigits: 0
157
+ }) +
158
+ (etiValue > 0? '\u2009K' : ''), // \u2009: thin space (https://www.compart.com/en/unicode/U+2009)
159
+
160
+ };
161
+ export async function hbars(params) {
162
+
163
+ const chartInstance = this;
164
+
165
+ params = {...default_params, ...params};
166
+ params.data_colors??= chartInstance.defaults.colors;
167
+
168
+ try {
169
+
170
+ // controllo values
171
+ params.values.forEach(subarray => {
172
+ if(!Array.isArray(subarray)) {
173
+ throw 'L’elemento `values` deve essere un array di soli array';
174
+ }
175
+ });
176
+
177
+ const flatValues = params.values.flat(); // utilizzato anche in seguito
178
+ if(flatValues.filter(v => isNaN(v)).length) {
179
+ throw 'Tutti i valori dei sub-array di `values` devono essere numerici';
180
+ }
181
+
182
+ // calcolo automatico di min_value: 0 se i dati sono tutti positivi,
183
+ // altrimenti il valore minimo effettivo (per includere le barre negative nell'area del grafico)
184
+ if(params.min_value == null) {
185
+ params.min_value = Math.min(0, ...flatValues.filter(v => v != null));
186
+ }
187
+
188
+ // container
189
+ [, params.width, params.height] = parseContainer({ container: params.container, width: params.width, height: params.height });
190
+
191
+
192
+ if(!params.width || !params.height) {
193
+ throw `width e/o height mancanti: width: ${params.width}, height: ${params.height}`;
194
+ }
195
+
196
+ // =>> legenda
197
+ // costruzione delle opzioni per la legenda
198
+ const legenda_params = {
199
+
200
+ ...{
201
+ /** altezza legenda */
202
+ height: 20,
203
+
204
+ /** spazio tra gli elementi della legenda */
205
+ elements_gap: 25,
206
+
207
+ /** dimensioni delle forme della legenda (NB: solo tondi) */
208
+ circles_radius: 20,
209
+
210
+ /** attributi svg.js dei font del testo della legenda (null utilizza default) */
211
+ fontAttrs: {
212
+ fill: '#333',
213
+ 'font-size': 14,
214
+ 'font-weight': 400
215
+ },
216
+ fontFilePath: null,
217
+ },
218
+
219
+ ...params.legenda
220
+ };
221
+
222
+
223
+ // legenda.items (elementi della legenda)
224
+ legenda_params.items = params.legenda.items.map((item, idx) => {
225
+ return {
226
+ ...item,
227
+ shape: 'circle',
228
+ attrs: {
229
+ ...(item.attrs?? {}),
230
+ fill: params.data_colors[idx]?? '#666',
231
+ }
232
+ };
233
+ });
234
+
235
+
236
+ // numero di step dell'asse delle categorie
237
+ const stepX_count = params.values.reduce((prev, curr) => Math.max(prev, curr.length), 0) + 1;
238
+
239
+ const svgCanvas = chartInstance.createSvgCanvas(params.container, params.debug)
240
+ .viewbox(0, 0, params.width, params.height)
241
+ .attr({...params.svgAttrs});
242
+
243
+
244
+ // pre-elaborazione e calcolo di alcuni elementi (chartArea)
245
+ // nuove variabili definite da parseStdAxisOpts (orientation:'horizontal':
246
+ // stepY_* si riferiscono all'asse dei valori (X), stepX_* all'asse delle categorie (Y))
247
+ const {
248
+ max_value,
249
+ min_value,
250
+ chart_area,
251
+ stepY_count,
252
+ stepY_value,
253
+ stepY_size,
254
+ stepX_size,
255
+ zero_y: zero_x
256
+ } = parseStdAxisOpts({
257
+ orientation: 'horizontal',
258
+ max_value: params.max_value,
259
+ min_value: params.min_value,
260
+ values: flatValues,
261
+ legenda: legenda_params,
262
+ padding: params.padding,
263
+ width: params.width,
264
+ height: params.height,
265
+ eti_asseX_height: params.xAxisLabelHeight,
266
+ eti_asseY_width: params.yAxisLabelWidth,
267
+ stepY_count: params.stepY_count,
268
+ stepX_count: stepX_count,
269
+ ticksLength: params.ticksLength,
270
+ }); // elaborazione standard
271
+
272
+ // =>> costruzione assi e griglia.
273
+ // Gli argomenti di cartesianAxis sono uguali a quelli di hbars
274
+ // con l'aggiunta di quelli calcolati da parseStdAxisOpts
275
+ // non tutti sono necessari ma vengono passati lo stesso per comodità
276
+ const cartesianAxisGroup = await cartesianAxis(chartInstance, {
277
+ ...params,
278
+ orientation: 'horizontal',
279
+ xLabelsOnTicks: false, // posiziona l'etichetta di categoria al centro di ogni step
280
+ ...{
281
+ max_value,
282
+ min_value,
283
+ chart_area,
284
+ stepY_count,
285
+ stepY_value,
286
+ stepY_size,
287
+ stepX_size,
288
+ stepX_count,
289
+ zero_y: zero_x
290
+ }
291
+ });
292
+
293
+ svgCanvas.add(cartesianAxisGroup);
294
+
295
+ // *********************************
296
+ // =>> disegno barre
297
+ // *********************************
298
+
299
+ // calcolo altezza (spessore) barre
300
+ const barsHeight = (stepX_size - params.seriesGap - (params.barsGap * (params.values.length - 1))) / params.values.length
301
+ // il raggio dell'angolo non può essere più grande dello spessore della barra stessa, né negativo
302
+ // (barsHeight può risultare negativo se seriesGap/barsGap non lasciano spazio sufficiente per tutte le serie,
303
+ // frequente qui perché l'asse delle categorie è vincolato all'altezza del grafico, spesso più stretta della larghezza)
304
+ ,barsCornerRadius = Math.max(0, Math.min(params.barsCornerRadius, barsHeight / 2))
305
+ ;
306
+
307
+
308
+ const barsGroup = svgCanvas.group()
309
+ .attr({ 'data-debug-info': params.debug? 'Gruppo barre' : null });
310
+
311
+ let barY = chart_area.top + params.seriesGap / 2;
312
+
313
+ for (let thisStepIdx = 0; thisStepIdx < stepX_count; thisStepIdx++) {
314
+
315
+ const thisStepValues = params.values.reduce((result, curr) => {
316
+ result.push(curr[thisStepIdx]?? null);
317
+ return result;
318
+ }, []);
319
+
320
+ // stampa di ogni barra della serie
321
+ for (let idx = 0; idx < thisStepValues.length; idx++) {
322
+ const itemValue = thisStepValues[idx];
323
+ const isLast = idx === thisStepValues.length - 1;
324
+
325
+ if (itemValue != null) {
326
+ // valori negativi: la barra si estende dalla linea dello zero verso sinistra,
327
+ // invece di crescere da sinistra verso destra
328
+ const isNegative = itemValue < 0,
329
+ // calcolo coordinata X del valore, usando come baseline la linea dello zero (zero_x)
330
+ // invece del margine sinistro del grafico. Nessuna inversione: l'asse X cresce verso destra
331
+ itemX = chart_area.left + (((itemValue - min_value) * chart_area.width) / (max_value - min_value)),
332
+ barLeft = isNegative? itemX : zero_x,
333
+ barRight = isNegative? zero_x : itemX,
334
+ barW = barRight - barLeft,
335
+ parteOrizzontaleBarra = barW - barsCornerRadius;
336
+ let path;
337
+
338
+ if(parteOrizzontaleBarra > 0) {
339
+
340
+ if(barW <= barsCornerRadius) {
341
+
342
+ const larghezzaCornerRadius = barW;
343
+
344
+ path = isNegative
345
+ // angoli arrotondati a sinistra (lato lontano dallo zero), spigolo vivo a destra
346
+ ? `M${zero_x},${barY}` +
347
+ `q-${larghezzaCornerRadius},0 -${larghezzaCornerRadius},${barsCornerRadius}` +
348
+ `v${barsHeight - barsCornerRadius * 2}` +
349
+ `q0,${barsCornerRadius} ${larghezzaCornerRadius},${barsCornerRadius}` +
350
+ 'z'
351
+ // angoli arrotondati a destra (lato lontano dallo zero), spigolo vivo a sinistra
352
+ : `M${zero_x},${barY}` +
353
+ `q${larghezzaCornerRadius},0 ${larghezzaCornerRadius},${barsCornerRadius}` +
354
+ `v${barsHeight - barsCornerRadius * 2}` +
355
+ `q0,${barsCornerRadius} -${larghezzaCornerRadius},${barsCornerRadius}` +
356
+ 'z';
357
+
358
+ } else {
359
+ path = isNegative
360
+ ? `M${zero_x},${barY}` +
361
+ `h-${parteOrizzontaleBarra}` +
362
+ `q-${barsCornerRadius},0 -${barsCornerRadius},${barsCornerRadius}` +
363
+ `v${barsHeight - barsCornerRadius * 2}` +
364
+ `q0,${barsCornerRadius} ${barsCornerRadius},${barsCornerRadius}` +
365
+ `h${parteOrizzontaleBarra}` +
366
+ 'z'
367
+ : `M${zero_x},${barY}` +
368
+ `h${parteOrizzontaleBarra}` +
369
+ `q${barsCornerRadius},0 ${barsCornerRadius},${barsCornerRadius}` +
370
+ `v${barsHeight - barsCornerRadius * 2}` +
371
+ `q0,${barsCornerRadius} -${barsCornerRadius},${barsCornerRadius}` +
372
+ `h-${parteOrizzontaleBarra}` +
373
+ 'z';
374
+ }
375
+
376
+ barsGroup.path(path)
377
+ .attr({
378
+ fill: params.data_colors[idx]?? '#666',
379
+ 'data-debug-info': params.debug? `value: ${itemValue}` : null
380
+ });
381
+ }
382
+ }
383
+
384
+ barY += barsHeight + (isLast ? 0 : params.barsGap);
385
+ }
386
+
387
+ barY += params.seriesGap;
388
+ }
389
+
390
+
391
+
392
+ // *********************************
393
+ // =>> Legenda
394
+ // *********************************
395
+ const legendaEl = await legenda(chartInstance, legenda_params);
396
+ legendaEl.attr({ 'data-cfg.debug-info': params.debug? 'Legenda' : null });
397
+ const legendaBbox = legendaEl.bbox();
398
+
399
+ legendaEl.dmove(
400
+ (params.width - legendaBbox.width) / 2,
401
+ params.padding
402
+ );
403
+ legendaEl.y(params.padding);
404
+
405
+ legendaEl.x((params.width - legendaBbox.width) / 2);
406
+
407
+ svgCanvas.add(legendaEl);
408
+
409
+
410
+ if(!params.container) {
411
+ return svgCanvas.svg();
412
+ }
413
+
414
+
415
+ } catch(e) {
416
+ console.error( 'MinimoCharts / hbars →', e ); // eslint-disable-line
417
+ }
418
+
419
+ }
@@ -4,7 +4,18 @@
4
4
  import { G } from '@svgdotjs/svg.js';
5
5
  import { createTextEl } from './create-text-element.js';
6
6
 
7
- export async function cartesianAxis(chartInstance, {
7
+ /**
8
+ dispatcher: orientation 'vertical' (barre/linee verticali, default) o 'horizontal' (barre orizzontali).
9
+ Gli argomenti sono gli stessi per entrambi gli orientamenti (vedi cartesianAxisVertical),
10
+ interpretati diversamente da ciascun ramo (vedi commenti in cartesianAxisHorizontal)
11
+ */
12
+ export async function cartesianAxis(chartInstance, params) {
13
+ return params.orientation === 'horizontal'
14
+ ? cartesianAxisHorizontal(chartInstance, params)
15
+ : cartesianAxisVertical(chartInstance, params);
16
+ }
17
+
18
+ async function cartesianAxisVertical(chartInstance, {
8
19
  debug = false,
9
20
  chart_area = {},
10
21
  padding = 0,
@@ -20,6 +31,7 @@ export async function cartesianAxis(chartInstance, {
20
31
  stepY_size,
21
32
  stepY_value,
22
33
  min_value,
34
+ zero_y,
23
35
  stepX_size,
24
36
  stepX_count,
25
37
  ticksLength,
@@ -106,7 +118,8 @@ export async function cartesianAxis(chartInstance, {
106
118
  );
107
119
 
108
120
  // =>> griglia orizzontale (ortogonale all'asse Y)
109
- if(showYgrid && stepIdx > 0) { // la prima linea coincide con l'asse X
121
+ // viene omessa solo la linea che coincide con l'asse X (la linea dello zero)
122
+ if(showYgrid && Math.abs(y - zero_y) > 0.01) {
110
123
  yGridGroup.line(
111
124
  chart_area.left, y,
112
125
  chart_area.right, y
@@ -147,8 +160,8 @@ export async function cartesianAxis(chartInstance, {
147
160
  xAxisGroup
148
161
  .attr({ 'data-debug-info': debug? 'Gruppo Asse X' : null })
149
162
  .line(
150
- chart_area.left, chart_area.bottom,
151
- chart_area.right, chart_area.bottom
163
+ chart_area.left, zero_y,
164
+ chart_area.right, zero_y
152
165
  )
153
166
  .attr({
154
167
  'data-debug-info': debug? 'Asse X' : null,
@@ -252,3 +265,248 @@ export async function cartesianAxis(chartInstance, {
252
265
  console.error( 'MinimoCharts → cartesianAxis', e ); // eslint-disable-line
253
266
  }
254
267
  }
268
+
269
+ /**
270
+ Variante orizzontale (barre orizzontali): rispetto a cartesianAxisVertical i ruoli sono invertiti:
271
+ - l'asse dei VALORI (numerico, formattato con labelYFormatter) è fisso in basso (come l'asse X
272
+ di cartesianAxisVertical), con griglia verticale. La linea dello zero (baseline delle barre)
273
+ è una linea verticale che si sposta in base al segno dei dati (zero_y qui è una coordinata X)
274
+ - l'asse delle CATEGORIE (testuale, da xLabels) è fisso a sinistra (come l'asse Y di
275
+ cartesianAxisVertical), con griglia orizzontale
276
+ stepY_count/stepY_size/stepY_value/zero_y e stepX_size/stepX_count sono gli stessi campi
277
+ restituiti da parseStdAxisOpts con orientation:'horizontal' (vedi commenti lì)
278
+ */
279
+ async function cartesianAxisHorizontal(chartInstance, {
280
+ debug = false,
281
+ chart_area = {},
282
+ padding = 0,
283
+ width,
284
+ height,
285
+ axisAttrs = {},
286
+ ticksAttrs = {},
287
+ labelYAttrs = {},
288
+ labelYFontFilePath = null,
289
+ labelXAttrs = {},
290
+ labelXFontFilePath = null,
291
+ stepY_count,
292
+ stepY_size,
293
+ stepY_value,
294
+ min_value,
295
+ zero_y: zero_x, // rinominato per chiarezza: qui è una coordinata X
296
+ stepX_size,
297
+ stepX_count,
298
+ ticksLength,
299
+ showXgrid = true, // griglia orizzontale (asse categorie)
300
+ showYgrid = true, // griglia verticale (asse valori)
301
+ gridLinesAttrs = {},
302
+
303
+ yAxisTitle = null, // titolo asse categorie (a sinistra)
304
+ yAxisTitleAttrs = {},
305
+ yAxisTitleFontFilePath = null,
306
+
307
+ xAxisTitle = null, // titolo asse valori (in basso)
308
+ xAxisTitleAttrs = {},
309
+ xAxisTitleFontFilePath = null,
310
+
311
+ xLabels, // array con le etichette di categoria, mostrate sull'asse verticale (a sinistra)
312
+ xLabelsOnTicks = true,
313
+ labelYFormatter = null, // funzione, formatta le etichette numeriche dell'asse valori (in basso)
314
+ }) {
315
+
316
+ try {
317
+
318
+ const cartesianAxisGroup = new G();
319
+ cartesianAxisGroup.attr({ 'data-debug-info': debug? 'Gruppo Base Assi Cartesiani (orizzontale) ed elementi correlati' : null });
320
+
321
+ // =>> ASSE VALORI (in basso, fisso — analogo all'asse X di cartesianAxisVertical)
322
+ // *****************************************
323
+ const valueAxisGroup = cartesianAxisGroup.group();
324
+ valueAxisGroup
325
+ .attr({ 'data-debug-info': debug? 'Gruppo Asse Valori' : null })
326
+ .line(
327
+ chart_area.left, chart_area.bottom,
328
+ chart_area.right, chart_area.bottom
329
+ )
330
+ .attr({
331
+ 'data-debug-info': debug? 'Asse Valori' : null,
332
+ ...axisAttrs
333
+ });
334
+
335
+ // =>> ticks ed etichette asse valori (da sinistra a destra)
336
+ const ticksValueGroup = valueAxisGroup.group().attr({
337
+ 'data-debug-info': debug? 'Gruppo ticks Asse Valori' : null,
338
+ });
339
+ const labelsValueGroup = valueAxisGroup.group().attr({
340
+ 'data-debug-info': debug? 'Gruppo etichette Asse Valori' : null,
341
+ });
342
+
343
+ // gruppo per griglia asse valori (verticale, ortogonale all'asse)
344
+ let valueGridGroup = null;
345
+ if(showYgrid) {
346
+ valueGridGroup = cartesianAxisGroup.group()
347
+ .attr({ 'data-debug-info': debug? 'Gruppo Griglia Asse Valori' : null });
348
+ }
349
+
350
+ for await (const stepIdx of Array.from(Array(stepY_count + 1).keys())) {
351
+
352
+ const x = chart_area.left + (stepY_size * stepIdx)
353
+ ,label = labelYFormatter(min_value + stepY_value * stepIdx);
354
+
355
+ // =>> ticks asse valori
356
+ ticksValueGroup
357
+ .line(
358
+ x, chart_area.bottom,
359
+ x, chart_area.bottom + ticksLength
360
+ )
361
+ .attr(ticksAttrs);
362
+
363
+ // =>> etichette asse valori
364
+ const labelEl = await createTextEl(chartInstance, {
365
+ text: label,
366
+ textFill: labelYAttrs.fill?? '#000',
367
+ fontAttr: labelYAttrs,
368
+ fontFilePath: labelYFontFilePath,
369
+ parentElement: labelsValueGroup
370
+ }),
371
+ label_bbox = labelEl.bbox();
372
+
373
+ labelEl.move(
374
+ x - label_bbox.width / 2,
375
+ chart_area.bottom + ticksLength + 2 // 2 è un distanziatore tra l'etichetta e la linea
376
+ );
377
+
378
+ // =>> griglia verticale (ortogonale all'asse valori)
379
+ // viene omessa solo la linea che coincide con l'asse categorie (la linea dello zero)
380
+ if(showYgrid && Math.abs(x - zero_x) > 0.01) {
381
+ valueGridGroup.line(
382
+ x, chart_area.top,
383
+ x, chart_area.bottom
384
+ ).attr(gridLinesAttrs);
385
+ }
386
+
387
+ } // end for await stepIdx
388
+
389
+ // =>> titolo asse valori (in basso)
390
+ if(xAxisTitle) {
391
+ const titleEl = await createTextEl (chartInstance, {
392
+ text: xAxisTitle,
393
+ textFill: xAxisTitleAttrs.fill?? '#000',
394
+ fontAttr: xAxisTitleAttrs,
395
+ fontFilePath: xAxisTitleFontFilePath,
396
+ parentElement: valueAxisGroup
397
+ }),
398
+ title_bbox = titleEl.bbox();
399
+
400
+ titleEl.move(
401
+ chart_area.left + (chart_area.width - title_bbox.width) / 2,
402
+ height - padding - title_bbox.height
403
+ );
404
+ }
405
+
406
+
407
+
408
+ // =>> ASSE CATEGORIE (a sinistra — analogo all'asse Y di cartesianAxisVertical;
409
+ // la linea dell'asse è quella dello zero, si sposta in base al segno dei dati)
410
+ // *****************************************
411
+ const categoryAxisGroup = cartesianAxisGroup.group();
412
+ categoryAxisGroup
413
+ .attr({ 'data-debug-info': debug? 'Gruppo Asse Categorie' : null })
414
+ .line(
415
+ zero_x, chart_area.top,
416
+ zero_x, chart_area.bottom
417
+ )
418
+ .attr({
419
+ 'data-debug-info': debug? 'Asse Categorie' : null,
420
+ ...axisAttrs
421
+ });
422
+
423
+ // =>> ticks ed etichette asse categorie (dall'alto verso il basso)
424
+ const ticksCategoryGroup = categoryAxisGroup.group().attr({
425
+ 'data-debug-info': debug? 'Gruppo ticks Asse Categorie' : null,
426
+ });
427
+ const labelsCategoryGroup = categoryAxisGroup.group().attr({
428
+ 'data-debug-info': debug? 'Gruppo etichette Asse Categorie' : null,
429
+ });
430
+
431
+ // coordinata x per il posizionamento delle etichette (a sinistra dei ticks, fisse su chart_area.left)
432
+ const labelX = chart_area.left - ticksLength - 2; // 2 è un distanziatore tra l'etichetta e la linea
433
+
434
+ // gruppo per griglia asse categorie (orizzontale, ortogonale all'asse)
435
+ // il gruppo viene sempre creato, perché anche se non si stampa la griglia viene
436
+ // cmq aggiunta l'ultima linea in basso
437
+ const categoryGridGroup = cartesianAxisGroup.group()
438
+ .attr({ 'data-debug-info': debug? 'Gruppo Griglia Asse Categorie' : null });
439
+
440
+ for (let i = 0; i <= stepX_count - 1; i++) {
441
+
442
+ const thisLabel = (Array.isArray(xLabels) ? xLabels[i] : undefined) ?? '',
443
+ labelIdx = i,
444
+ isLastStep = labelIdx === stepX_count - 1,
445
+ y = chart_area.top + (stepX_size * labelIdx);
446
+
447
+ // =>> ticks categorie
448
+ ticksCategoryGroup
449
+ .line(
450
+ chart_area.left - ticksLength, y,
451
+ chart_area.left, y
452
+ )
453
+ .attr(ticksAttrs);
454
+
455
+ // =>> etichette categorie
456
+ const labelEl = await createTextEl (chartInstance, {
457
+ text: thisLabel,
458
+ textFill: labelXAttrs.fill?? '#000',
459
+ fontAttr: labelXAttrs,
460
+ fontFilePath: labelXFontFilePath,
461
+ parentElement: labelsCategoryGroup
462
+ }),
463
+ label_bbox = labelEl.bbox();
464
+
465
+ // posizionamento (allineate a destra sulla linea dei ticks)
466
+ labelEl.move(
467
+ labelX - label_bbox.width,
468
+ y - label_bbox.height / 2 + (xLabelsOnTicks? 0 : stepX_size / 2)
469
+ );
470
+
471
+ // =>> griglia orizzontale
472
+ // la prima linea coincide con l'asse valori (in basso, essendo la prima categoria in alto)
473
+ // l'ultima linea in basso viene stampata sempre
474
+ if((showXgrid && labelIdx > 0) || isLastStep ) {
475
+ categoryGridGroup.line(
476
+ chart_area.left, y,
477
+ chart_area.right, y
478
+ ).attr(gridLinesAttrs);
479
+ }
480
+
481
+ } // end for thisLabel
482
+
483
+ // =>> titolo asse categorie (a sinistra, ruotato)
484
+ if(yAxisTitle) {
485
+ const titleGroup = categoryAxisGroup.group(); // per la rotazione
486
+ titleGroup.attr({
487
+ 'data-debug-info': debug? 'Titolo asse Categorie' : null,
488
+ });
489
+
490
+ const titleEl = await createTextEl(chartInstance, {
491
+ text: yAxisTitle,
492
+ textFill: yAxisTitleAttrs.fill?? '#000',
493
+ fontAttr: yAxisTitleAttrs,
494
+ fontFilePath: yAxisTitleFontFilePath,
495
+ parentElement: titleGroup
496
+ });
497
+
498
+ titleEl.rotate(-90);
499
+ const title_bbox = titleGroup.bbox();
500
+
501
+ titleGroup.move(
502
+ padding,
503
+ (chart_area.bottom - title_bbox.height) / 2
504
+ );
505
+ }
506
+
507
+ return cartesianAxisGroup;
508
+
509
+ } catch(e) {
510
+ console.error( 'MinimoCharts → cartesianAxis (horizontal)', e ); // eslint-disable-line
511
+ }
512
+ }
@@ -33,6 +33,14 @@ export function parseStdAxisOpts(params = {} ) {
33
33
  stepX_count: null, // non può essere null, va verificato prima
34
34
  ticksLength: 0,
35
35
 
36
+ /**
37
+ 'vertical' (barre/linee verticali, default): l'asse dei valori è l'asse Y, l'asse delle categorie è l'asse X.
38
+ 'horizontal' (barre orizzontali): l'asse dei valori è l'asse X, l'asse delle categorie è l'asse Y.
39
+ Influisce solo sul calcolo geometrico di stepY_size/zero_y/stepX_size (vedi sotto),
40
+ non sul calcolo "nice zero" di min_value/max_value/stepY_count, che è indipendente dall'orientamento
41
+ */
42
+ orientation: 'vertical',
43
+
36
44
  ...params
37
45
  };
38
46
 
@@ -62,6 +70,23 @@ export function parseStdAxisOpts(params = {} ) {
62
70
  parsed.min_value = params.min_value;
63
71
  }
64
72
 
73
+ // se i valori attraversano lo zero (min negativo, max positivo), min_value/max_value/stepY_count
74
+ // vengono ricalcolati in modo che tutti gli step della griglia abbiano la stessa dimensione
75
+ // e che lo zero coincida esattamente con una delle suddivisioni (invece di cadere a metà tra due step)
76
+ let stepY_count = params.stepY_count;
77
+
78
+ if(parsed.min_value < 0 && parsed.max_value > 0) {
79
+ const rawStep = (parsed.max_value - parsed.min_value) / stepY_count,
80
+ positive_steps = Math.max(1, Math.round(parsed.max_value / rawStep)),
81
+ step = parsed.max_value / positive_steps,
82
+ negative_steps = Math.max(1, Math.ceil(Math.abs(parsed.min_value) / step));
83
+
84
+ parsed.max_value = positive_steps * step;
85
+ parsed.min_value = -negative_steps * step;
86
+ stepY_count = positive_steps + negative_steps;
87
+ }
88
+ parsed.stepY_count = stepY_count;
89
+
65
90
  const legenda_height = params.legenda?.height?? 0;
66
91
  const gap_legenda = legenda_height? 10 : 0; // spazio tra legenda e grafico
67
92
 
@@ -78,16 +103,32 @@ export function parseStdAxisOpts(params = {} ) {
78
103
  parsed.chart_area.width = parsed.chart_area.right - parsed.chart_area.left;
79
104
  parsed.chart_area.height = parsed.chart_area.bottom - parsed.chart_area.top;
80
105
 
81
- // valore di ogni step asse y
82
- // (usato per il calcolo delle etichette dell'asse Y)
83
- parsed.stepY_value = (parsed.max_value - parsed.min_value) / params.stepY_count;
84
-
85
- // lunghezza di ogni step asse y
86
- parsed.stepY_size = parsed.chart_area.height / params.stepY_count;
87
-
88
-
89
- parsed.stepX_size = parsed.chart_area.width / (params.stepX_count - 1);
90
-
106
+ // valore di ogni step dell'asse dei valori
107
+ // (usato per il calcolo delle etichette)
108
+ parsed.stepY_value = (parsed.max_value - parsed.min_value) / stepY_count;
109
+
110
+ const isHorizontal = params.orientation === 'horizontal';
111
+
112
+ // lunghezza di ogni step dell'asse dei valori e dell'asse delle categorie:
113
+ // con orientamento verticale l'asse dei valori è l'asse Y (chart_area.height) e quello delle
114
+ // categorie è l'asse X (chart_area.width); con orientamento orizzontale i ruoli sono invertiti
115
+ parsed.stepY_size = (isHorizontal? parsed.chart_area.width : parsed.chart_area.height) / stepY_count;
116
+ parsed.stepX_size = (isHorizontal? parsed.chart_area.height : parsed.chart_area.width) / (params.stepX_count - 1);
117
+
118
+ // coordinata corrispondente al valore 0 (asse Y con orientamento verticale, asse X con orientamento orizzontale)
119
+ // utile quando min_value è negativo: la baseline delle barre viene disegnata in corrispondenza
120
+ // dello zero invece che del valore minimo
121
+ let raw_zero_y;
122
+ if(isHorizontal) {
123
+ // l'asse X cresce naturalmente verso destra (nessuna inversione necessaria, a differenza dell'asse Y)
124
+ raw_zero_y = parsed.chart_area.left +
125
+ (((0 - parsed.min_value) * parsed.chart_area.width) / (parsed.max_value - parsed.min_value));
126
+ parsed.zero_y = Math.max(parsed.chart_area.left, Math.min(parsed.chart_area.right, raw_zero_y));
127
+ } else {
128
+ raw_zero_y = parsed.chart_area.top + parsed.chart_area.height -
129
+ (((0 - parsed.min_value) * parsed.chart_area.height) / (parsed.max_value - parsed.min_value));
130
+ parsed.zero_y = Math.min(parsed.chart_area.bottom, Math.max(parsed.chart_area.top, raw_zero_y));
131
+ }
91
132
 
92
133
  return parsed;
93
134
 
@@ -277,9 +277,11 @@ export async function lines(params) {
277
277
  max_value,
278
278
  min_value,
279
279
  chart_area,
280
+ stepY_count,
280
281
  stepY_value,
281
282
  stepY_size,
282
- stepX_size
283
+ stepX_size,
284
+ zero_y
283
285
  } = parseStdAxisOpts({
284
286
  max_value: params.max_value,
285
287
  min_value: params.min_value,
@@ -305,10 +307,12 @@ export async function lines(params) {
305
307
  max_value,
306
308
  min_value,
307
309
  chart_area,
310
+ stepY_count,
308
311
  stepY_value,
309
312
  stepY_size,
310
313
  stepX_size,
311
- stepX_count
314
+ stepX_count,
315
+ zero_y
312
316
  }
313
317
  });
314
318
 
@@ -25,7 +25,7 @@ const config = {
25
25
  stylelintConfigPath: '../../stylelint.config.mjs',
26
26
 
27
27
  // Output directory for the CSS file generated by Style Dictionary
28
- buildPath: '../src/css',
28
+ buildPath: './',
29
29
 
30
30
  // Output filename for the generated CSS file
31
31
  destFile: 'custom-properties.css',
@@ -10,9 +10,9 @@ A collection of standalone Node.js CLI utilities to help manage a web project's
10
10
 
11
11
  ## starter-kit
12
12
 
13
- `starter-kit/starter-install.sh` bootstraps a new frontend project: it copies the starter config files (`package.json`, `.gitignore`, `jsconfig.json`, eslint/stylelint configs, webpack config and modules, ...) and installs all the required dev dependencies.
13
+ `starter-kit/starter-install.sh` bootstraps a new frontend project: it copies the starter config files (`package.json`, `.gitignore`, `jsconfig.json`, eslint/stylelint configs, webpack config and modules, ...) into the project root and the frontend files (webpack template, `CLAUDE.md`, `_root_htaccess`/`_root_robots.txt`, ...) into a fixed `./app` folder, then installs all the required dev dependencies.
14
14
 
15
- The script **requires zsh** (it exits with an error if launched with `sh` or `bash`). Existing files are never overwritten: root config files are copied with a `NEW-` prefix, while `package.json` and the `*.code-workspace` file are merged (template keys are appended at the end, prefixed with `_`).
15
+ The script **requires zsh** (it exits with an error if launched with `sh` or `bash`). Existing files are never overwritten: they are copied alongside with a `NEW-` prefix (to be integrated manually or removed). If the `./app` folder already exists, the whole frontend file set is copied into `./NEW-app` instead.
16
16
 
17
17
  ### If minimo is already installed
18
18
 
@@ -22,7 +22,7 @@ The script is exposed as a `bin` of the package:
22
22
  npx starter-kit
23
23
  ```
24
24
 
25
- Since the installation is fully overwrite-safe, the script can also be **re-run at any time to update** an existing setup (e.g. to refresh the webpack modules or reinstall the dev dependencies): existing files are skipped or copied with the `NEW-` prefix, and the `package.json` / `*.code-workspace` merges are idempotent (running the script again neither duplicates nor overwrites anything).
25
+ Since the installation is fully overwrite-safe, the script can also be **re-run at any time to update** an existing setup (e.g. to refresh the webpack modules or reinstall the dev dependencies): existing files are copied with the `NEW-` prefix, so running the script again never duplicates or overwrites anything.
26
26
 
27
27
  ### Without installing minimo
28
28
 
@@ -34,14 +34,6 @@ zsh package/dev-tools/starter-kit/starter-install.sh
34
34
  rm -rf package
35
35
  ```
36
36
 
37
- To test the installation skipping all `npm install` commands, use the `--no-npm` parameter:
38
-
39
- ```bash
40
- curl -sL "$(npm view @massimo-cassandro/minimo dist.tarball)" | tar xz package/dev-tools/starter-kit
41
- zsh package/dev-tools/starter-kit/starter-install.sh --no-npm
42
- rm -rf package
43
- ```
44
-
45
37
  ## SVG adn favicons tools
46
38
 
47
39
  ```bash
@@ -3,8 +3,9 @@
3
3
  # IMPORTANTE: questo script è idempotente e deve restare tale.
4
4
  # Può essere rilanciato più volte sullo stesso progetto (anche via `npx starter-kit`
5
5
  # per aggiornare un setup esistente) senza sovrascrivere nulla né creare duplicati:
6
- # i file esistenti vengono saltati o copiati con prefisso NEW- (o _ per
7
- # package.json e *.code-workspace).
6
+ # i file esistenti vengono copiati con prefisso NEW- (da integrare manualmente
7
+ # o rimuovere); la cartella di installazione del frontend, fissa su ./app, se
8
+ # già esistente viene sostituita con ./NEW-app.
8
9
  # Ogni modifica futura deve conservare questa proprietà.
9
10
 
10
11
  RED='\033[0;31m'
@@ -12,6 +13,8 @@ YELLOW='\033[0;33m'
12
13
  GREEN='\033[0;32m'
13
14
  NC='\033[0m' # No Color
14
15
 
16
+ DEBUG=FALSE
17
+
15
18
  # Questo script richiede zsh: se lanciato con sh/bash blocca l'esecuzione
16
19
  if [ -z "$ZSH_VERSION" ]; then
17
20
  printf "${RED}Errore: lanciare con 'zsh starter-install.sh', non con sh/bash.${NC}\n" >&2
@@ -146,6 +149,11 @@ devDependencies=(
146
149
  # @principalstudio/html-webpack-inject-preload file-loader
147
150
  )
148
151
 
152
+ if [ "$DEBUG" = "TRUE" ]; then
153
+ dependencies=( @massimo-cassandro/minimo )
154
+ devDependencies=()
155
+ fi
156
+
149
157
  for pkg in "${dependencies[@]}"; do
150
158
  npm i -S "$pkg"
151
159
  done
@@ -170,9 +178,9 @@ echo -e "${GREEN}...MINIMO FILES${NC}"
170
178
  # installato in node_modules (stessa versione che userà il progetto)
171
179
  MINIMO_PKG_DIR=./node_modules/@massimo-cassandro/minimo
172
180
 
173
- safe_cat "${MINIMO_PKG_DIR}/src/custom-properties.css" "${FRONTEND_INSTALL_PATH}/css/${FILE}"
174
- safe_cat "${MINIMO_PKG_DIR}/src/custom-media.css" "${FRONTEND_INSTALL_PATH}/css/${FILE}"
175
- safe_cat "${MINIMO_PKG_DIR}/src/fonts.css" "${FRONTEND_INSTALL_PATH}/css/${FILE}"
181
+ safe_cat "${MINIMO_PKG_DIR}/src/custom-properties.css" "${FRONTEND_INSTALL_PATH}/css/custom-properties.css"
182
+ safe_cat "${MINIMO_PKG_DIR}/src/custom-media.css" "${FRONTEND_INSTALL_PATH}/css/custom-media.css"
183
+ safe_cat "${MINIMO_PKG_DIR}/src/fonts.css" "${FRONTEND_INSTALL_PATH}/css/fonts.css"
176
184
 
177
185
  # config di build-tokens: collocato accanto al css generato (custom-properties.css).
178
186
  safe_cat "${MINIMO_PKG_DIR}/design-tokens/tokens-config-sample.mjs" "${FRONTEND_INSTALL_PATH}/css/tokens-config.mjs"
@@ -195,5 +203,6 @@ safe_cat "${SOURCE_FILES_DIR}/app_index.js" "${FRONTEND_INSTALL_PATH}/index.js"
195
203
  safe_cat "${SOURCE_FILES_DIR}/layout-critical.css" "${FRONTEND_INSTALL_PATH}/css/layout-critical.css"
196
204
 
197
205
 
206
+
198
207
  set +C
199
208
  echo -e "\n\n${GREEN}*** END ***${NC}"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massimo-cassandro/minimo",
3
- "version": "1.29.5",
3
+ "version": "1.30.0",
4
4
  "description": "Light css framework & utilities",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -105,6 +105,11 @@ label:has(.form-control) {
105
105
  }
106
106
  }
107
107
 
108
+ .form-control[type='number'] {
109
+ font-feature-settings: 'lnum', 'tnum';
110
+ font-variant-numeric: lining-nums tabular-nums;
111
+ }
112
+
108
113
  /* da bootstrap */
109
114
  select.form-control {
110
115