@massimo-cassandro/minimo 1.15.0 → 1.16.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.
@@ -6,9 +6,31 @@ A collection of standalone Node.js CLI utilities to help manage a web project's
6
6
  * [Svg Icons Tools](./svg-icons-tools/README.md) — optimizes SVG icons and combines/converts them into symbols, JSX, or SCSS variables
7
7
  * [Create favicons](./create-favicons/README.md) — generates the full favicon set (SVG, PNG, ICO, webmanifest, HTML snippet) from a source image
8
8
  * [upd@m](./upd@m/README.md) — updates all `@massimo-cassandro/*` packages installed in `node_modules`
9
- * **webpack-setup** — script for installing minimo environment
9
+ * [webpack-setup](#webpack-setup) — script for installing minimo environment
10
10
 
11
- ## Usage of SVG adn favicons tools
11
+ ## webpack-setup
12
+
13
+ `webpack-setup/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.
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 `_`).
16
+
17
+ ### If minimo is already installed
18
+
19
+ ```bash
20
+ zsh node_modules/@massimo-cassandro/minimo/dev-tools/webpack-setup/starter-install.sh
21
+ ```
22
+
23
+ ### Without installing minimo
24
+
25
+ To bootstrap a project **before** (or without) installing the whole package, download only the `webpack-setup` folder from the npm tarball and run the script from there:
26
+
27
+ ```bash
28
+ curl -sL "$(npm view @massimo-cassandro/minimo dist.tarball)" | tar xz package/dev-tools/webpack-setup
29
+ zsh package/dev-tools/webpack-setup/starter-install.sh
30
+ rm -rf package
31
+ ```
32
+
33
+ ## SVG adn favicons tools
12
34
 
13
35
  ```bash
14
36
  ## SVG Icons Tools
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+ // Merge JSONC: aggiunge in fondo al file di destinazione le chiavi di primo
3
+ // livello del template, prefissate con '_', preservando commenti e
4
+ // formattazione di entrambi i file.
5
+ // Uso: node merge-jsonc.cjs <template> <destinazione>
6
+
7
+ const fs = require('fs');
8
+
9
+ // Analizza un testo JSONC ignorando stringhe e commenti: individua
10
+ // l'oggetto principale, le virgole a profondità 1 e la posizione
11
+ // dell'ultimo carattere significativo (non spazio, non commento)
12
+ function scan(text) {
13
+ let depth = 0;
14
+ let open = -1;
15
+ let close = -1;
16
+ let last = -1;
17
+ const commas = [];
18
+ let i = 0;
19
+ while (i < text.length) {
20
+ const c = text[i];
21
+ const next = text[i + 1];
22
+ if (c === '"') {
23
+ i++;
24
+ while (i < text.length && text[i] !== '"') {
25
+ if (text[i] === '\\') i++;
26
+ i++;
27
+ }
28
+ last = i;
29
+ } else if (c === '/' && next === '/') {
30
+ while (i < text.length && text[i] !== '\n') i++;
31
+ continue;
32
+ } else if (c === '/' && next === '*') {
33
+ i += 2;
34
+ while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i++;
35
+ i++;
36
+ } else {
37
+ if (c === '{' || c === '[') {
38
+ if (depth === 0 && open === -1 && c === '{') open = i;
39
+ depth++;
40
+ } else if (c === '}' || c === ']') {
41
+ depth--;
42
+ if (depth === 0 && close === -1) close = i;
43
+ } else if (c === ',' && depth === 1) {
44
+ commas.push(i);
45
+ }
46
+ if (!/\s/.test(c)) last = i;
47
+ }
48
+ i++;
49
+ }
50
+ return { open, close, commas, last };
51
+ }
52
+
53
+ // Posizione della prima doppia virgoletta fuori dai commenti
54
+ function firstQuotePos(text) {
55
+ let i = 0;
56
+ while (i < text.length) {
57
+ const c = text[i];
58
+ const next = text[i + 1];
59
+ if (c === '/' && next === '/') {
60
+ while (i < text.length && text[i] !== '\n') i++;
61
+ } else if (c === '/' && next === '*') {
62
+ i += 2;
63
+ while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i++;
64
+ i += 2;
65
+ } else if (c === '"') {
66
+ return i;
67
+ } else {
68
+ i++;
69
+ }
70
+ }
71
+ return -1;
72
+ }
73
+
74
+ const [tplPath, destPath] = process.argv.slice(2);
75
+
76
+ try {
77
+ const tpl = fs.readFileSync(tplPath, 'utf8');
78
+ const dest = fs.readFileSync(destPath, 'utf8');
79
+ const t = scan(tpl);
80
+ const d = scan(dest);
81
+ if (t.open < 0 || t.close < 0) throw new Error(tplPath + ': oggetto JSON non trovato');
82
+ if (d.open < 0 || d.close < 0) throw new Error(destPath + ': oggetto JSON non trovato');
83
+
84
+ // voci di primo livello del template (spezzate sulle virgole a profondità 1)
85
+ const body = tpl.slice(t.open + 1, t.close);
86
+ const offset = t.open + 1;
87
+ const bounds = t.commas.map((p) => p - offset).concat(body.length);
88
+ const entries = [];
89
+ let prev = 0;
90
+ for (const b of bounds) {
91
+ entries.push(body.slice(prev, b));
92
+ prev = b + 1;
93
+ }
94
+
95
+ // prefissa con '_' la chiave di ogni voce (voci di soli commenti/spazi escluse)
96
+ const renamed = [];
97
+ for (const entry of entries) {
98
+ const cleaned = entry.replace(/^\s+/, '').replace(/\s+$/, '');
99
+ const q = firstQuotePos(cleaned);
100
+ if (q < 0) continue;
101
+ renamed.push('\n ' + cleaned.slice(0, q + 1) + '_' + cleaned.slice(q + 1));
102
+ }
103
+ if (renamed.length === 0) process.exit(0);
104
+
105
+ // aggiunge le voci prima della chiusura dell'oggetto principale della
106
+ // destinazione, inserendo una virgola dopo l'ultima voce esistente se serve
107
+ const dBody = dest.slice(d.open + 1, d.close);
108
+ const b = scan(dBody);
109
+ const needsComma = b.last >= 0 && dBody[b.last] !== ',';
110
+ const insertAt = d.open + 1 + b.last + 1;
111
+ const out = needsComma
112
+ ? dest.slice(0, insertAt) + ',' + dest.slice(insertAt, d.close) + renamed.join(',') + '\n' + dest.slice(d.close)
113
+ : dest.slice(0, d.close) + renamed.join(',') + '\n' + dest.slice(d.close);
114
+
115
+ fs.writeFileSync(destPath, out);
116
+ } catch (err) {
117
+ console.error('merge-jsonc: ' + err.message);
118
+ process.exit(1);
119
+ }
@@ -1,5 +1,16 @@
1
1
  #!/bin/zsh
2
2
 
3
+ RED='\033[0;31m'
4
+ YELLOW='\033[0;33m'
5
+ GREEN='\033[0;32m'
6
+ NC='\033[0m' # No Color
7
+
8
+ # Questo script richiede zsh: se lanciato con sh/bash blocca l'esecuzione
9
+ if [ -z "$ZSH_VERSION" ]; then
10
+ printf "${RED}Errore: lanciare con 'zsh starter-install.sh', non con sh/bash.${NC}\n" >&2
11
+ exit 1
12
+ fi
13
+
3
14
  # Configurazione variabili
4
15
  # BASE_URL="https://raw.githubusercontent.com/massimo-cassandro/minimo/refs/heads/main"
5
16
 
@@ -16,23 +27,31 @@ TEMPLATES_DIR="${BASE_URL}/templates"
16
27
  # WEBPACK_SOURCE_DIR="$BASE_URL/webpack-setup"
17
28
  WEBPACK_SOURCE_DIR="$BASE_URL"
18
29
 
19
- RED='\033[0;31m'
20
- YELLOW='\033[0;33m'
21
- GREEN='\033[0;32m'
22
- NC='\033[0m' # No Color
23
-
24
30
  # protezione contro la sovrascrittura
25
31
  set -C
26
32
 
27
- # Array per tracciare le copie bloccati
33
+ # Array per tracciare i file già esistenti
28
34
  BLOCKED_FILES=()
29
35
 
30
- # Wrapper: copia solo se il file non esiste, altrimenti registra il blocco
36
+ # Wrapper: copia solo se il file non esiste, altrimenti registra il blocco.
37
+ # Con terzo argomento "new", se la destinazione esiste già la copia viene
38
+ # comunque creata con prefisso NEW- (da integrare manualmente o rimuovere)
31
39
  safe_cat() {
32
40
  local src="$1"
33
41
  local dest="$2"
42
+ local mode="$3"
43
+ if [ ! -f "$src" ]; then
44
+ echo -e "${RED}Sorgente mancante: $src${NC}"
45
+ return 1
46
+ fi
34
47
  if [ -e "$dest" ]; then
35
- BLOCKED_FILES+=("$dest")
48
+ if [ "$mode" = "new" ]; then
49
+ local new_dest="$(dirname "$dest")/NEW-$(basename "$dest")"
50
+ cat "$src" >| "$new_dest"
51
+ BLOCKED_FILES+=("$dest → copiato come $new_dest")
52
+ else
53
+ BLOCKED_FILES+=("$dest")
54
+ fi
36
55
  else
37
56
  cat "$src" > "$dest"
38
57
  fi
@@ -41,16 +60,26 @@ safe_cat() {
41
60
  echo -e "${GREEN}...package.json${NC}"
42
61
  if [ ! -f package.json ]; then
43
62
  safe_cat "${TEMPLATES_DIR}/package-tpl.json" package.json
63
+ elif [ ! -f "${TEMPLATES_DIR}/package-tpl.json" ]; then
64
+ echo -e "${RED}Sorgente mancante: ${TEMPLATES_DIR}/package-tpl.json${NC}"
44
65
  else
45
- echo -e "${GREEN}package.json already exists${NC}"
66
+ # package.json esistente: merge con il template, aggiungendo in fondo
67
+ # le chiavi del template prefissate con '_' (ordine originale preservato)
68
+ node -e '
69
+ const fs = require("fs");
70
+ const existing = JSON.parse(fs.readFileSync("package.json", "utf8"));
71
+ const tpl = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
72
+ for (const [key, value] of Object.entries(tpl)) {
73
+ existing["_" + key] = value;
74
+ }
75
+ fs.writeFileSync("package.json", JSON.stringify(existing, null, 2) + "\n");
76
+ ' "${TEMPLATES_DIR}/package-tpl.json"
77
+ echo -e "${YELLOW}package.json esistente: chiavi del template aggiunte in fondo con prefisso '_' (da integrare o rimuovere)${NC}"
78
+ BLOCKED_FILES+=("package.json → merge: chiavi template aggiunte con prefisso \`_\`")
46
79
  fi
47
80
 
48
81
  echo -e "${GREEN}...gitignore${NC}"
49
- if [ ! -f .gitignore ]; then
50
- safe_cat "${TEMPLATES_DIR}/_gitignore" .gitignore
51
- else
52
- echo -e "${GREEN}.gitignore already exists${NC}"
53
- fi
82
+ safe_cat "${TEMPLATES_DIR}/_gitignore" .gitignore new
54
83
 
55
84
  echo -e "\n${GREEN}Where do you want to install the frontend configuration?${NC}"
56
85
  echo "1) Root directory"
@@ -73,20 +102,39 @@ fi
73
102
 
74
103
  echo -e "\n${GREEN}...config files & utilities${NC}"
75
104
 
76
- safe_cat "${TEMPLATES_DIR}/_browserslistrc" .browserslistrc
77
- safe_cat "${TEMPLATES_DIR}/_editorconfig" .editorconfig
78
- safe_cat "${TEMPLATES_DIR}/_prettierrc" .prettierrc
79
- safe_cat "${TEMPLATES_DIR}/jsconfig.json" jsconfig.json
80
- safe_cat "${TEMPLATES_DIR}/__project__.code-workspace" __project__.code-workspace
105
+ safe_cat "${TEMPLATES_DIR}/_browserslistrc" .browserslistrc new
106
+ safe_cat "${TEMPLATES_DIR}/_editorconfig" .editorconfig new
107
+ safe_cat "${TEMPLATES_DIR}/_prettierrc" .prettierrc new
108
+ safe_cat "${TEMPLATES_DIR}/jsconfig.json" jsconfig.json new
109
+
110
+ # code-workspace: se un file .code-workspace esiste già (qualunque sia il nome),
111
+ # le chiavi del template vengono aggiunte in fondo con prefisso '_'
112
+ WORKSPACE_TPL="${TEMPLATES_DIR}/__project__.code-workspace"
113
+ WORKSPACE_FILES=( *.code-workspace(N) )
114
+ if [ ${#WORKSPACE_FILES[@]} -gt 1 ]; then
115
+ echo -e "${RED}Trovati più file .code-workspace (${WORKSPACE_FILES[*]}): merge non eseguito${NC}"
116
+ elif [ ${#WORKSPACE_FILES[@]} -eq 0 ]; then
117
+ safe_cat "$WORKSPACE_TPL" __project__.code-workspace
118
+ elif [ ! -f "$WORKSPACE_TPL" ]; then
119
+ echo -e "${RED}Sorgente mancante: ${WORKSPACE_TPL}${NC}"
120
+ else
121
+ WORKSPACE_FILE="${WORKSPACE_FILES[1]}"
122
+ if node "${BASE_URL}/merge-jsonc.cjs" "$WORKSPACE_TPL" "$WORKSPACE_FILE"; then
123
+ echo -e "${YELLOW}${WORKSPACE_FILE} esistente: chiavi del template aggiunte in fondo con prefisso '_' (da integrare o rimuovere)${NC}"
124
+ BLOCKED_FILES+=("${WORKSPACE_FILE} → merge: chiavi template aggiunte con prefisso \`_\`")
125
+ else
126
+ echo -e "${RED}Merge di ${WORKSPACE_FILE} non riuscito${NC}"
127
+ fi
128
+ fi
81
129
 
82
130
 
83
131
  echo -e "\n${GREEN}...eslint${NC}"
84
132
  npm i -D @massimo-cassandro/eslint-config
85
- safe_cat "${TEMPLATES_DIR}/eslint.config.mjs" eslint.config.mjs
133
+ safe_cat "${TEMPLATES_DIR}/eslint.config.mjs" eslint.config.mjs new
86
134
 
87
135
  echo -e "\n${GREEN}...stylelint${NC}"
88
136
  npm i -D @massimo-cassandro/stylelint-config
89
- safe_cat "${TEMPLATES_DIR}/stylelint.config.mjs" stylelint.config.mjs
137
+ safe_cat "${TEMPLATES_DIR}/stylelint.config.mjs" stylelint.config.mjs new
90
138
 
91
139
  echo -e "\n${GREEN}...webpack${NC}"
92
140
  npm i -D webpack-cli webpack-dev-server webpack-manifest-plugin webpack
@@ -117,15 +165,19 @@ FILES=(
117
165
  'postcss.config.mjs'
118
166
  )
119
167
 
120
- if [ ! -d "$WEBPACK_LOCAL_DIR" ]; then
121
- mkdir -p "$WEBPACK_LOCAL_DIR"
168
+ # se la cartella esiste già, i moduli vengono copiati in NEW-webpack-modules
169
+ # (da integrare manualmente o rimuovere)
170
+ if [ -d "$WEBPACK_LOCAL_DIR" ]; then
171
+ NEW_WEBPACK_LOCAL_DIR="$(dirname "$WEBPACK_LOCAL_DIR")/NEW-$(basename "$WEBPACK_LOCAL_DIR")"
172
+ BLOCKED_FILES+=("$WEBPACK_LOCAL_DIR → copiata come $NEW_WEBPACK_LOCAL_DIR")
173
+ WEBPACK_LOCAL_DIR="$NEW_WEBPACK_LOCAL_DIR"
174
+ fi
122
175
 
123
- for FILE in "${FILES[@]}"; do
124
- # echo "Scaricamento di $FILE..."
125
- safe_cat "${WEBPACK_MODULES_REMOTE_URL}/${FILE}" "${WEBPACK_LOCAL_DIR}/${FILE}"
126
- done
176
+ mkdir -p "$WEBPACK_LOCAL_DIR"
127
177
 
128
- fi
178
+ for FILE in "${FILES[@]}"; do
179
+ safe_cat "${WEBPACK_MODULES_REMOTE_URL}/${FILE}" "${WEBPACK_LOCAL_DIR}/${FILE}"
180
+ done
129
181
 
130
182
  echo -e "\n${GREEN}Creating a template of your frontend application's folder structured${NC}"
131
183
 
@@ -136,9 +188,9 @@ mkdir -p error-pages imgs icons src src/css favicons design-tokens
136
188
  cd ..
137
189
  set +C
138
190
 
139
- # Riepilogo finale dei file bloccati
191
+ # Riepilogo finale dei file già esistenti
140
192
  if [ ${#BLOCKED_FILES[@]} -gt 0 ]; then
141
- echo -e "\n${YELLOW}*** File non copiati (già esistenti): ***${NC}"
193
+ echo -e "\n${YELLOW}*** File già esistenti (non copiati o copiati con prefisso NEW-): ***${NC}"
142
194
  for F in "${BLOCKED_FILES[@]}"; do
143
195
  echo -e "${YELLOW} ✗ $F${NC}"
144
196
  done
@@ -32,6 +32,6 @@
32
32
  "build tokens": "npx buildTokens --config ./path/to/tokens-config.mjs",
33
33
  "check unresolved props": "npx checkUnresolvedProps --config ./path/to/tokens-config.mjs"
34
34
  },
35
- "dependencies": {},
36
- "devDependencies": {}
35
+ "devDependencies": {},
36
+ "dependencies": {}
37
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massimo-cassandro/minimo",
3
- "version": "1.15.0",
3
+ "version": "1.16.0",
4
4
  "description": "Light css framework & utilities",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -15,6 +15,7 @@ References
15
15
 
16
16
  // TODO snackbar action
17
17
  // TODO handle multiple simultaneous snackbars (no stacking)
18
+ // TODO test con prefers-reduced-motion
18
19
 
19
20
  /**
20
21
  * Displays a snackbar / toast notification using the Popover API.
@@ -53,12 +54,22 @@ export function snackbar(message, options = {}){
53
54
 
54
55
  if (_timeoutID) clearTimeout(_timeoutID);
55
56
 
56
- _popover?.classList.add(styles.isHiding);
57
-
58
- _popover?.addEventListener('animationend', () => {
57
+ const remove = () => {
59
58
  _popover?.hidePopover();
60
59
  _popover?.remove();
61
- }, { once: true }); // 'once' prevents duplicate listeners; the flag guards against calls before animation ends
60
+ };
61
+
62
+ _popover?.classList.add(styles.isHiding);
63
+
64
+ // with `prefers-reduced-motion` (or any override disabling animations) no animation
65
+ // runs and `animationend` would never fire: in that case remove immediately
66
+ if (_popover?.getAnimations().length) {
67
+ // 'once' prevents duplicate listeners; the flag guards against calls before animation ends
68
+ _popover.addEventListener('animationend', remove, { once: true });
69
+ }
70
+ else {
71
+ remove();
72
+ }
62
73
  };
63
74
 
64
75
  _popover = domBuilder([