@massimo-cassandro/minimo 1.43.4 → 1.45.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.
@@ -0,0 +1,11 @@
1
+ # Custom media builder
2
+
3
+ Genera il file css delle @custom-media a partire da un modulo JS che esporta
4
+ (default) un oggetto { nome: media-query } e un un secondo oggetto breakpoint.js
5
+ per la condivisione dei breakpoiunt in js, vedi `src/custom-media.js`.
6
+
7
+ Uso da riga di comando (percorsi relativi alla directory corrente):
8
+
9
+ ```bash
10
+ node custom-media-builder.mjs [--src=./app/css/custom-media.js] [--dest=<src con estensione .css>]
11
+ ```
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+
3
+ /* eslint-disable no-console */
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import process from 'process';
7
+ import { pathToFileURL } from 'url';
8
+
9
+ /**
10
+ * Genera il file css delle @custom-media.
11
+ * Il file viene riscritto solo se il contenuto è cambiato,
12
+ * per non attivare inutilmente il watcher di webpack.
13
+ *
14
+ * @param {object} [options]
15
+ * @param {string} [options.src] modulo JS con l'export default delle custom media
16
+ * @param {string} [options.dest] file css da generare (default: file in `src` con estensione cambiata in `.css`)
17
+ * @returns {Promise<boolean>} `true` se il file è stato riscritto
18
+ *
19
+ * @example
20
+ * await buildCustomMedia({
21
+ * src: './app/css/custom-media.js',
22
+ * dest: './app/css/custom-media.css',
23
+ * });
24
+ */
25
+ export async function buildCustomMedia({
26
+ src = './app/css/custom-media.js',
27
+ dest = null,
28
+ } = {}) {
29
+
30
+ const srcPath = path.resolve(src),
31
+ destPath = path.resolve(dest ?? srcPath.replace(/\.m?js$/, '.css'));
32
+
33
+ const { default: customMedia } = await import(pathToFileURL(srcPath).href);
34
+
35
+ if (!customMedia || typeof customMedia !== 'object') {
36
+ throw new Error(`${src}: export default mancante o non valido`);
37
+ }
38
+
39
+ const rows = Object.entries(customMedia).map(([name, query]) => {
40
+ if (typeof query !== 'string' || !query.trim()) {
41
+ throw new Error(`${src}: valore non valido per "${name}"`);
42
+ }
43
+ return `@custom-media --${name.replace(/^--/, '')} ${query.trim()};`;
44
+ });
45
+
46
+ const content = '/* File generato da custom-media-builder.mjs a partire da ' +
47
+ `${path.basename(srcPath)}. Eventuali modifich andranno perdute */\n\n` +
48
+ rows.join('\n') + '\n';
49
+
50
+ if (fs.existsSync(destPath) && fs.readFileSync(destPath, 'utf8') === content) {
51
+ return false;
52
+ }
53
+
54
+ fs.writeFileSync(destPath, content);
55
+ return true;
56
+ }
57
+
58
+ // esecuzione da riga di comando
59
+ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
60
+ const args = Object.fromEntries(
61
+ process.argv.slice(2)
62
+ .map(arg => arg.match(/^--(src|dest)=(.+)$/))
63
+ .filter(Boolean)
64
+ .map(([, key, value]) => [key, value])
65
+ );
66
+
67
+ try {
68
+ const written = await buildCustomMedia(args);
69
+ console.log(written ? 'custom media: file aggiornato' : 'custom media: nessuna modifica');
70
+ } catch (err) {
71
+ console.error(err.message);
72
+ process.exit(1);
73
+ }
74
+ }
@@ -1,8 +1,8 @@
1
1
  {#
2
- Bootstrap 5 Symfony form layout
2
+ minimo form layout (ex bs5)
3
3
  =======================================
4
4
  Massimo Cassandro
5
- https://github.com/massimo-cassandro/symfony-bootstrap-form-theme
5
+ v. 2.1
6
6
  #}
7
7
 
8
8
  {#
@@ -112,11 +112,17 @@
112
112
  {% set params = {} %}
113
113
  {%- endif -%}
114
114
  {%- if params.container_attr is not defined %}
115
- {% set params = params|merge({container_attr: {}}) %}
115
+ {% set params = params|merge({container_attr: {} }) %}
116
116
  {%- endif -%}
117
117
 
118
118
  {% set params = params|merge({
119
- container_attr: params.container_attr|merge({ class: (params.container_attr.class|default('') ~ (compound|default()? ' form-collection-fset form-group' : ' form-group'))|trim })
119
+ container_attr: params.container_attr
120
+ |merge({
121
+ class: (
122
+ container_class|default('') ~ ' ' ~
123
+ params.container_attr.class|default('') ~
124
+ (compound|default()? ' form-collection-fset form-group' : ' form-group')
125
+ )|trim })
120
126
  }) %}
121
127
 
122
128
  {%- if disabled is defined and disabled %}
@@ -31,6 +31,7 @@
31
31
  "create-favicons": "npx create-favicons --dir=./app/favicons",
32
32
  "python server": "python3 -m http.server [[port8000]] # --directory __dirname__ # 8000 = default port",
33
33
  "node server": "node serve # richiede ‘npm i serve’",
34
+ "build custom media": "npx custom-media-builder --src=./app/css/custom-media.js",
34
35
  "build tokens": "npx buildTokens --config ./app/css/tokens-config.mjs",
35
36
  "check unresolved props": "npx checkUnresolvedProps --config ./app/css/tokens-config.mjs",
36
37
  "find orphan css classes": "npx findOrphanCssClasses --config ./app/css/orphan-css-classes.config.mjs",
@@ -458,6 +458,7 @@ done
458
458
  MINIMO_PKG_DIR=./node_modules/@massimo-cassandro/minimo
459
459
 
460
460
  force_cat "${MINIMO_PKG_DIR}/src/custom-properties.css" "${FRONTEND_INSTALL_PATH}/css/minimo-custom-properties.css"
461
+ force_cat "${MINIMO_PKG_DIR}/src/custom-media.js" "${FRONTEND_INSTALL_PATH}/css/custom-media.js"
461
462
  force_cat "${MINIMO_PKG_DIR}/src/custom-media.css" "${FRONTEND_INSTALL_PATH}/css/custom-media.css"
462
463
 
463
464
  # entry css principale del progetto: copia diretta di minimo.css
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massimo-cassandro/minimo",
3
- "version": "1.43.4",
3
+ "version": "1.45.0",
4
4
  "description": "Light css framework & utilities",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
@@ -34,6 +34,7 @@
34
34
  "upd@m": "dev-tools/upd@m/index.mjs",
35
35
  "svgIconsTools": "dev-tools/svg-icons-tools/index.mjs",
36
36
  "create-favicons": "dev-tools/create-favicons/index.mjs",
37
+ "custom-media-builder": "dev-tools/custom-media-builder/custom-media-builder.mjs",
37
38
  "starter-kit": "dev-tools/starter-kit/starter-install.sh"
38
39
  },
39
40
  "repository": {
@@ -47,6 +48,7 @@
47
48
  "build-margin-padding-css": "node ./scripts/build-margin-padding-css.mjs",
48
49
  "generate:types": "node ./scripts/generate-class-types.mjs",
49
50
  "prepublishOnly": "npm run generate:types",
51
+ "build custom media": "node ./dev-tools/custom-media-builder/custom-media-builder.mjs --src=./src/custom-media.js",
50
52
  "check unresolved props": "node ./dev-tools/design-tokens-builder/check-unresolved-custom-props.mjs --config ./design-tokens/tokens-config.mjs",
51
53
  "test": "node --test \"test/**/*.test.mjs\"",
52
54
  "test sideEffects tree-shaking": "node ./test/sideEffects-tree-shaking/run-test.mjs",
@@ -151,6 +151,7 @@ input[type='checkbox'] {
151
151
  display: block;
152
152
  padding-left: 1.5em;
153
153
  margin-block-start: var(--size-sm);
154
+ break-inside: avoid;
154
155
 
155
156
  .form-check-input {
156
157
  float: left;
@@ -1,40 +1,17 @@
1
- /* TODO valutare rinomina dei breakpoint in nomi semantici (es. dsk/lap/tab/mob,
2
- vedi scripts/build-margin-padding-css.mjs) per coerenza con la scala `sizes`,
3
- che usa la stessa nomenclatura xs/sm/md/lg/xl e genera ambiguità nelle
4
- utility responsive. Non banale: i tier attuali (xxlarge/xlarge/large/medium/
5
- small, con varianti -only/-down) sono numerosi e in parte sovrapposti (es.
6
- "large" = "small desktop") e non si riducono in modo pulito a poche
7
- categorie semantiche; inoltre il nome è usato in molti file (grid.css,
8
- display.css, visibility.css, text.css, sf-macro.css, json-table, oltre ai
9
- file margin-padding-*-media.css) ed è parte di un template pensato per
10
- essere personalizzato dall'utente: rinominarlo qui non aggiorna le copie
11
- già in uso nei progetti che lo hanno importato. */
12
-
13
- /*
14
- 90rem : 1440px xxlarge (large desktop)
15
- 80rem : 1280px xlarge (desktop)
16
- 64rem : 1024px large (small desktop)
17
- 47.5rem : 760px medium (tablet)
18
- 25rem : 400px small (phone)
19
-
20
- tablet alt:
21
- 53.75rem : 860px medium (tablet)
22
- */
1
+ /* File generato da custom-media-builder.mjs a partire da custom-media.js: NON MODIFICARE DIRETTAMENTE */
23
2
 
24
3
  @custom-media --media-xxlarge-viewport-up (width >= 90rem);
25
4
  @custom-media --media-xlarge-viewport-up (width >= 80rem);
26
5
  @custom-media --media-large-viewport-up (width >= 64rem);
27
6
  @custom-media --media-medium-viewport-up (width >= 47.5rem);
28
7
  @custom-media --media-small-viewport-up (width >= 25rem);
29
-
30
8
  @custom-media --media-xxlarge-viewport-only (width >= 90rem);
31
- @custom-media --media-xlarge-viewport-only (64rem < width <= 80rem);
32
- @custom-media --media-large-viewport-only (64rem < width <= 80rem);
33
- @custom-media --media-medium-viewport-only (47.5rem < width <= 64rem);
34
- @custom-media --media-small-viewport-only (width <= 47.5rem);
35
-
36
- @custom-media --media-xxlarge-viewport-down (max-width: 90rem);
37
- @custom-media --media-xlarge-viewport-down (max-width: 80rem);
38
- @custom-media --media-large-viewport-down (max-width: 64rem);
39
- @custom-media --media-medium-viewport-down (max-width: 47.5rem);
40
- @custom-media --media-small-viewport-down (max-width: 25rem);
9
+ @custom-media --media-xlarge-viewport-only (80rem <= width < 90rem);
10
+ @custom-media --media-large-viewport-only (64rem <= width < 80rem);
11
+ @custom-media --media-medium-viewport-only (47.5rem <= width < 64rem);
12
+ @custom-media --media-small-viewport-only (width < 47.5rem);
13
+ @custom-media --media-xxlarge-viewport-down (width < 90rem);
14
+ @custom-media --media-xlarge-viewport-down (width < 80rem);
15
+ @custom-media --media-large-viewport-down (width < 64rem);
16
+ @custom-media --media-medium-viewport-down (width < 47.5rem);
17
+ @custom-media --media-small-viewport-down (width < 25rem);
@@ -0,0 +1,72 @@
1
+ /*
2
+ Definizioni delle custom media del progetto: unica fonte dei breakpoint,
3
+ usata sia per generare custom-media.css (vedi custom-media-builder.mjs,
4
+ `npm run "build custom media"`) sia direttamente in JS.
5
+
6
+ NB: dopo ogni modifica rigenerare custom-media.css (la generazione è
7
+ comunque eseguita all'avvio delle build webpack DEV e PROD).
8
+
9
+ I valori sono duplicati nei `media` scritti a mano dei template Twig
10
+ (home.html.twig e home_incl/hero.incl.html.twig): vanno aggiornati a mano.
11
+
12
+ 90rem : 1440px xxlarge (large desktop)
13
+ 80rem : 1280px xlarge (desktop)
14
+ 64rem : 1024px large (small desktop)
15
+ 47.5rem : 760px medium (tablet)
16
+ 25rem : 400px small (phone)
17
+
18
+ tablet alt:
19
+ 53.75rem : 860px medium (tablet)
20
+ */
21
+
22
+ /* TODO valutare rinomina dei breakpoint in nomi semantici (es. dsk/lap/tab/mob,
23
+ vedi scripts/build-margin-padding-css.mjs) per coerenza con la scala `sizes`,
24
+ che usa la stessa nomenclatura xs/sm/md/lg/xl e genera ambiguità nelle
25
+ utility responsive. Non banale: i tier attuali (xxlarge/xlarge/large/medium/
26
+ small, con varianti -only/-down) sono numerosi e in parte sovrapposti (es.
27
+ "large" = "small desktop") e non si riducono in modo pulito a poche
28
+ categorie semantiche; inoltre il nome è usato in molti file (grid.css,
29
+ display.css, visibility.css, text.css, sf-macro.css, json-table, oltre ai
30
+ file margin-padding-*-media.css) ed è parte di un template pensato per
31
+ essere personalizzato dall'utente: rinominarlo qui non aggiorna le copie
32
+ già in uso nei progetti che lo hanno importato. */
33
+
34
+ export const breakpoints = {
35
+ xxlarge: '90rem',
36
+ xlarge: '80rem',
37
+ large: '64rem',
38
+ medium: '47.5rem',
39
+ small: '25rem',
40
+ };
41
+
42
+ const bp = breakpoints;
43
+
44
+ /**
45
+ * Custom media del progetto: chiave = nome della custom media (senza `--`),
46
+ * valore = media query.
47
+ *
48
+ * @example
49
+ * import customMedia from './custom-media.js';
50
+ * const mql = matchMedia(customMedia['media-medium-viewport-up']); // '(width >= 47.5rem)'
51
+ */
52
+ const customMedia = {
53
+ 'media-xxlarge-viewport-up': `(width >= ${bp.xxlarge})`,
54
+ 'media-xlarge-viewport-up': `(width >= ${bp.xlarge})`,
55
+ 'media-large-viewport-up': `(width >= ${bp.large})`,
56
+ 'media-medium-viewport-up': `(width >= ${bp.medium})`,
57
+ 'media-small-viewport-up': `(width >= ${bp.small})`,
58
+
59
+ 'media-xxlarge-viewport-only': `(width >= ${bp.xxlarge})`,
60
+ 'media-xlarge-viewport-only': `(${bp.xlarge} <= width < ${bp.xxlarge})`,
61
+ 'media-large-viewport-only': `(${bp.large} <= width < ${bp.xlarge})`,
62
+ 'media-medium-viewport-only': `(${bp.medium} <= width < ${bp.large})`,
63
+ 'media-small-viewport-only': `(width < ${bp.medium})`,
64
+
65
+ 'media-xxlarge-viewport-down': `(width < ${bp.xxlarge})`,
66
+ 'media-xlarge-viewport-down': `(width < ${bp.xlarge})`,
67
+ 'media-large-viewport-down': `(width < ${bp.large})`,
68
+ 'media-medium-viewport-down': `(width < ${bp.medium})`,
69
+ 'media-small-viewport-down': `(width < ${bp.small})`,
70
+ };
71
+
72
+ export default customMedia;
@@ -1,7 +1,7 @@
1
1
  // @ts-check
2
2
  /*! minimo - Better Text with places */
3
3
 
4
- import { betterText, NBSP_AFTER_PARTICLE_REGEX } from './better-text.js';
4
+ import { betterText, NBSP_AFTER_PARTICLE_REGEX, PARTICELLE } from './better-text.js';
5
5
 
6
6
  const places = [
7
7
  // EUROPA
@@ -467,6 +467,19 @@ function buildPlacesMatcher(list) {
467
467
 
468
468
  const defaultMatcher = buildPlacesMatcher(places);
469
469
 
470
+ /*
471
+ Corrisponde agli spazi (normali o non separabili) NON seguiti da una particella:
472
+ usata con l'opzione `addNbsp` per unire le parole dei nomi composti, lasciando
473
+ separabile lo spazio prima di una particella (es. "Lago di Como" → spazio normale
474
+ tra "Lago" e "di", mentre quello tra "di" e "Como" è già gestito da betterText())
475
+ */
476
+ const SPACE_BEFORE_NON_PARTICLE_REGEX = new RegExp(
477
+ '[ \\u00A0]+(?!(?:' +
478
+ PARTICELLE.map(p => p.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&')).join('|') +
479
+ ')(?![\\p{L}\\p{N}]))',
480
+ 'giu'
481
+ );
482
+
470
483
  /**
471
484
  * Versione di betterText() che normalizza anche le occorrenze di luoghi noti
472
485
  * (paesi, città, fiumi, monti, località turistiche) presenti nel testo:
@@ -480,18 +493,33 @@ const defaultMatcher = buildPlacesMatcher(places);
480
493
  * ovunque compaiano.
481
494
  *
482
495
  * @param {string} str - stringa da elaborare
483
- * @param {string[]} [custom_words] - elenco di parole di cui va mantenuta la capitalizzazione esatta (es. `['iPhone', 'macOS']`) (default: [])
484
- * @param {string[]} [extra_places] - luoghi aggiuntivi, uniti all'elenco predefinito (default: [])
496
+ * @param {object} [options] - opzioni (default: {})
497
+ * @param {string[]} [options.custom_words] - elenco di parole di cui va mantenuta la capitalizzazione esatta (es. `['iPhone', 'macOS']`) (default: [])
498
+ * @param {string[]} [options.extra_places] - luoghi aggiuntivi, uniti all'elenco predefinito (default: [])
499
+ * @param {boolean} [options.addNbsp] - se `true`, nei nomi di luogo composti da più parole gli spazi vengono
500
+ * sostituiti da spazi non separabili, tranne quelli che precedono una particella
501
+ * (es. in "Monte Bianco" lo spazio diventa non separabile, in "Lago di Como" solo quello dopo "di") (default: false)
485
502
  * @returns {string} stringa elaborata, oppure stringa vuota se l'input è falsy
486
503
  *
487
504
  * @example
488
505
  * betterTextWithPlaces("vacanza a new york e nel lago di como ,l'avana");
489
506
  * // → "vacanza a New York e nel Lago di Como, L’Avana"
490
507
  *
491
- * betterTextWithPlaces('gita a monte cervino', ['iPhone'], ['Monte Cervino']);
508
+ * betterTextWithPlaces('gita a monte cervino', { custom_words: ['iPhone'], extra_places: ['Monte Cervino'] });
492
509
  * // → 'gita a Monte Cervino'
510
+ *
511
+ * // opzioni con i valori di default
512
+ * betterTextWithPlaces('monte bianco e lago di como', {
513
+ * custom_words: [],
514
+ * extra_places: [],
515
+ * addNbsp: false
516
+ * });
517
+ * // → 'Monte Bianco e Lago di Como' (spazio non separabile solo dopo "di")
518
+ *
519
+ * betterTextWithPlaces('monte bianco e lago di como', { addNbsp: true });
520
+ * // → 'Monte Bianco e Lago di Como' (spazi non separabili tra "Monte" e "Bianco" e dopo "di")
493
521
  */
494
- export function betterTextWithPlaces(str, custom_words = [], extra_places = []) {
522
+ export function betterTextWithPlaces(str, { custom_words = [], extra_places = [], addNbsp = false } = {}) {
495
523
 
496
524
  str = betterText(str, custom_words);
497
525
 
@@ -503,7 +531,12 @@ export function betterTextWithPlaces(str, custom_words = [], extra_places = [])
503
531
  ? buildPlacesMatcher([...places, ...extra_places])
504
532
  : defaultMatcher;
505
533
 
506
- return matcher
507
- ? str.replace(matcher.regex, match => matcher.canonical.get(placeKey(match)) ?? match)
508
- : str;
534
+ if (!matcher) {
535
+ return str;
536
+ }
537
+
538
+ return str.replace(matcher.regex, match => {
539
+ const place = matcher.canonical.get(placeKey(match)) ?? match;
540
+ return addNbsp ? place.replace(SPACE_BEFORE_NON_PARTICLE_REGEX, '\u00A0') : place;
541
+ });
509
542
  }
@@ -9,7 +9,7 @@
9
9
  che nulla di tutto ciò dipende dagli argomenti della funzione e in precedenza veniva
10
10
  ricostruito a ogni singola chiamata di betterText().
11
11
  */
12
- const PARTICELLE = [
12
+ export const PARTICELLE = [
13
13
  'e', 'ed',
14
14
  'di', 'a', 'da', 'in', 'per', 'con', 'su', 'per', 'tra', 'fra',
15
15
  'il', 'lo', 'la', 'i', 'gli', 'le',