@massimo-cassandro/minimo 1.39.2 → 1.40.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 (29) hide show
  1. package/dev-tools/create-favicons/src/default-params.mjs +5 -5
  2. package/dev-tools/design-tokens-builder/tokens-config-sample.mjs +1 -1
  3. package/dev-tools/find-orphan-css-classes/find-orphan-css-classes.mjs +451 -0
  4. package/dev-tools/find-orphan-css-classes/orphan-css-classes.config.mjs +44 -0
  5. package/dev-tools/starter-kit/source_files/app/css/fonts.css +2 -2
  6. package/dev-tools/starter-kit/source_files/root/__project__.code-workspace +4 -0
  7. package/dev-tools/starter-kit/source_files/root/_editorconfig +10 -1
  8. package/dev-tools/starter-kit/source_files/root/_gitignore +1 -0
  9. package/dev-tools/starter-kit/source_files/root/_sf/controller/UnsplashController.php +2 -1
  10. package/dev-tools/starter-kit/source_files/root/_sf/symfony-webpack-setup.md +1 -1
  11. package/dev-tools/starter-kit/source_files/root/_sf/templates/_main-tpl.html.twig +9 -2
  12. package/dev-tools/starter-kit/source_files/root/_sf/templates/bundles/TwigBundle/Exception/error.html.twig +5 -1
  13. package/dev-tools/starter-kit/source_files/root/_sf/templates/home.html.twig +17 -0
  14. package/dev-tools/starter-kit/source_files/root/package-tpl.json +5 -4
  15. package/dev-tools/starter-kit/source_files/root/webpack-config-modules/custom-props-purgecss-plugin.mjs +30 -6
  16. package/dev-tools/starter-kit/source_files/root/webpack.config.mjs +140 -58
  17. package/dev-tools/starter-kit/starter-install.sh +24 -13
  18. package/dev-tools/starter-kit/webpack-config-upg.md +99 -5
  19. package/package.json +2 -1
  20. package/src/components/unsplash-page/unsplash-page.module.css +6 -5
  21. package/src/minimo.css +3 -6
  22. package/types/classes.d.ts +0 -525
  23. package/dev-tools/starter-kit/source_files/app/api/.gitkeep +0 -0
  24. package/dev-tools/starter-kit/source_files/app/src/components/.gitkeep +0 -0
  25. package/dev-tools/starter-kit/source_files/app/src/js/.gitkeep +0 -0
  26. package/dev-tools/starter-kit/source_files/app/src/pagine/.gitkeep +0 -0
  27. package/dev-tools/starter-kit/source_files/app/src/web-components/.gitkeep +0 -0
  28. package/src/css/margin-padding-bootstrap-media.css +0 -1845
  29. package/src/css/margin-padding-bootstrap.css +0 -465
@@ -15,7 +15,7 @@ export const default_params = {
15
15
 
16
16
  // directory output (percorso relativo alla dir di lavoro)
17
17
  // se la dir non esiste, viene creata
18
- output_dir: './output',
18
+ output_dir: './output', // symfony -> '../../public',
19
19
 
20
20
 
21
21
  // WEBMANIFEST
@@ -43,12 +43,12 @@ export const default_params = {
43
43
  // nome del file snippet, compresa l'estensione
44
44
  // se questo parametro è null e anche `snippet_target_file` lo è, lo snippet non viene generato
45
45
  // se il linguaggio è ejs il nome è sempre 'favicons.incl.ejs' e snippet_name viene ignorato
46
- snippet_name: 'favicons.html',
46
+ snippet_name: 'favicons.html', // symfony -> 'favicons.incl.html.twig',
47
47
 
48
48
  // path (relativo a questo file) in cui salvare lo snippet
49
49
  // se null, viene utilizzato lo stesso percorso indicato in `output_dir`
50
50
  // se la dir non esiste, viene creata
51
- snippet_path: null,
51
+ snippet_path: null, // symfony -> '../../templates/_incl',
52
52
 
53
53
  // path del file in cui scrivere le righe dello snippet
54
54
  // questa impostazione è alternativa alle due precedenti e, se presente, queste
@@ -62,7 +62,7 @@ export const default_params = {
62
62
 
63
63
  // linguaggio da utilizzare per la sintassi dello snippet html
64
64
  // html, pug, ejs
65
- snippet_language: 'ejs',
65
+ snippet_language: 'ejs', // symfony: 'html',
66
66
 
67
67
  // template per la costruzione dello snippet.
68
68
  // si tratta di una stringa (anche su più righe) in cui deve essere presente
@@ -78,7 +78,7 @@ export const default_params = {
78
78
 
79
79
  // se true aggiunge allo snippet una query string per ovviare ad eventuali
80
80
  // problemi di caching del browser
81
- add_cache_buster: false,
81
+ add_cache_buster: false, // symfony: true,
82
82
 
83
83
  //****************************************************************************
84
84
 
@@ -114,7 +114,7 @@ const config = {
114
114
  // your project tokens — e.g. remaps a Figma/Penpot-exported `primary`
115
115
  // color to minimo's own `primary.100` naming
116
116
  {
117
- src: './project-tokens/*.{js,mjs,jsonc,json}',
117
+ src: './design-tokens/*.{js,mjs,jsonc,json}',
118
118
  prefix: 'my-project',
119
119
  transform: { primary: 'primary.100' },
120
120
  }
@@ -0,0 +1,451 @@
1
+ #!/usr/bin/env node
2
+
3
+
4
+ /*
5
+ * find-orphan-css-classes.mjs
6
+ *
7
+ * Analizza i file sorgente (html, js, php, ecc.) ed elenca le classi CSS
8
+ * utilizzate che non sono definite nei file CSS indicati.
9
+ * Per i file js/jsx controlla anche le classi dei CSS modules
10
+ * (styles.xxx, ${styles.xxx}, styles['xxx']), verificandole rispetto al
11
+ * file .module.css importato.
12
+ *
13
+ * Nessuna dipendenza esterna, richiede Node >= 18.
14
+ *
15
+ * Il report viene scritto nel file `orphan-classes.md`, nella stessa directory
16
+ * del file di configurazione, con link cliccabili alle occorrenze. Se non ci
17
+ * sono classi mancanti il file viene eliminato.
18
+ *
19
+ * Uso:
20
+ * node find-orphan-css-classes.mjs --config orphan-css-classes.config.mjs
21
+ * node find-orphan-css-classes.mjs -c orphan-css-classes.config.mjs --json
22
+ *
23
+ * Opzioni:
24
+ * -c, --config file di configurazione JavaScript (.mjs consigliato, con export default)
25
+ * --json output JSON su stdout (nessun file scritto)
26
+ * -h, --help mostra questo messaggio
27
+ *
28
+ * Configurazione (i percorsi relativi sono risolti rispetto alla directory
29
+ * del file di configurazione):
30
+ * src (obbligatorio) file o directory da analizzare
31
+ * css (obbligatorio) file o directory contenenti i CSS
32
+ * ext estensioni dei sorgenti
33
+ * cssExt estensioni dei file CSS
34
+ * excludeDir nomi di directory da saltare (default: node_modules, .git, dist, vendor)
35
+ * excludeCssFiles percorsi di file CSS (o CSS modules) da non considerare
36
+ * nel confronto (default: [])
37
+ * ignore espressioni regolari (RegExp o stringhe) di classi da ignorare
38
+ * cssModules {
39
+ * enabled attiva il controllo dei CSS modules (default: true)
40
+ * ext estensioni dei sorgenti in cui cercare i CSS modules (default: js, jsx)
41
+ * pattern espressione regolare (RegExp o stringa) che identifica i file module
42
+ * camelCase se true, styles.myClass corrisponde anche a .my-class
43
+ * }
44
+ *
45
+ * Codice di uscita: 0 = nessuna classe mancante, 1 = trovate classi mancanti,
46
+ * 2 = errore di configurazione.
47
+ */
48
+
49
+ import fs from 'node:fs';
50
+ import path from 'node:path';
51
+ import { pathToFileURL } from 'node:url';
52
+ import { parseArgs } from 'node:util';
53
+
54
+ const DEFAULTS = {
55
+ ext: ['html', 'htm', 'js', 'mjs', 'jsx', 'ts', 'tsx', 'php', 'vue', 'twig'],
56
+ cssExt: ['css'],
57
+ excludeDir: ['node_modules', '.git', 'dist', 'vendor'],
58
+ excludeCssFiles: [],
59
+ ignore: [],
60
+ cssModules: {
61
+ enabled: true,
62
+ ext: ['js', 'jsx'],
63
+ pattern: '\\.module\\.(css|scss|sass|less)$',
64
+ camelCase: false
65
+ }
66
+ };
67
+
68
+ const REPORT_FILE = 'orphan-classes.md';
69
+
70
+ const DYN = '\u0000'; /* segnaposto per le parti dinamiche (PHP, template literal, ecc.) */
71
+
72
+ /* classe CSS in un selettore: .nome, con eventuali caratteri di escape */
73
+ const CSS_CLASS_RE = /\.(-?(?:[_a-zA-Z]|\\.)(?:[\w-]|\\.)*)/g;
74
+
75
+ /* class="..." / className='...' / class: `...` */
76
+ const CLASS_ATTR_RE = /\b(?:class|className)\s*[=:]\s*(["'`])((?:(?!\1)[\s\S])*?)\1/g;
77
+
78
+ /* el.classList.add('a', 'b') ecc. */
79
+ const CLASSLIST_RE = /\.classList\.(?:add|remove|toggle|contains|replace)\(([^)]*)\)/g;
80
+ const STRING_RE = /(["'`])((?:(?!\1).)*?)\1/g;
81
+
82
+ /* querySelector('.a .b'), closest('.a'), matches('.a') */
83
+ const SELECTOR_RE = /\b(querySelectorAll|querySelector|closest|matches)\(\s*(["'`])((?:(?!\2).)*?)\2/g;
84
+
85
+ /* getElementsByClassName('a b') */
86
+ const BY_CLASS_RE = /\bgetElementsByClassName\(\s*(["'`])((?:(?!\1).)*?)\1/g;
87
+
88
+ /*
89
+ * Import di un CSS module:
90
+ * import styles from './x.module.css'
91
+ * import * as styles from './x.module.css'
92
+ * import styles, { foo } from './x.module.css'
93
+ * const styles = require('./x.module.css')
94
+ */
95
+ const IMPORT_RE = /\bimport\s+(?:\*\s+as\s+)?([A-Za-z_$][\w$]*)\s*(?:,\s*\{[^}]*\})?\s*from\s*(["'])([^"']+)\2/g;
96
+ const REQUIRE_RE = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*require\(\s*(["'])([^"']+)\2\s*\)/g;
97
+
98
+ /* ---------- utilità ---------- */
99
+
100
+ function unescapeCss(name) {
101
+ return name.replace(/\\(.)/g, '$1');
102
+ }
103
+
104
+ function escapeRegExp(s) {
105
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
106
+ }
107
+
108
+ function camelize(s) {
109
+ return s.replace(/[-_]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ''));
110
+ }
111
+
112
+ function maskDynamic(value) {
113
+ return value
114
+ .replace(/<\?[\s\S]*?\?>/g, DYN)
115
+ .replace(/\{\{[\s\S]*?\}\}/g, DYN)
116
+ .replace(/\{%[\s\S]*?%\}/g, DYN)
117
+ .replace(/\$\{[^}]*\}/g, DYN);
118
+ }
119
+
120
+ /*
121
+ * Divide un valore di attributo in singole classi.
122
+ * I token che contengono parti dinamiche (es. "btn-<?= $x ?>") vengono
123
+ * scartati, perché il nome reale della classe non è noto.
124
+ */
125
+ function splitTokens(value) {
126
+ return maskDynamic(value)
127
+ .split(/\s+/)
128
+ .filter(t => t && !t.includes(DYN) && !/[<>"'`]/.test(t));
129
+ }
130
+
131
+ function normalizeExt(list) {
132
+ return new Set(list.map(e => e.replace(/^\./, '').toLowerCase()));
133
+ }
134
+
135
+ function collectFiles(entries, extensions, exclude) {
136
+ const out = new Set();
137
+
138
+ const visit = (p, explicit) => {
139
+ let stat;
140
+ try {
141
+ stat = fs.statSync(p);
142
+ } catch {
143
+ console.error(`Percorso non trovato: ${p}`);
144
+ return;
145
+ }
146
+
147
+ if (stat.isDirectory()) {
148
+ for (const entry of fs.readdirSync(p, { withFileTypes: true })) {
149
+ if (exclude.has(entry.name)) continue;
150
+ visit(path.join(p, entry.name), false);
151
+ }
152
+ } else if (explicit || extensions.has(path.extname(p).slice(1).toLowerCase())) {
153
+ out.add(path.resolve(p));
154
+ }
155
+ };
156
+
157
+ entries.forEach(p => visit(p, true));
158
+ return [...out];
159
+ }
160
+
161
+ function buildLineIndex(text) {
162
+ const idx = [0];
163
+ for (let i = 0; i < text.length; i++) {
164
+ if (text.charCodeAt(i) === 10) idx.push(i + 1);
165
+ }
166
+ return idx;
167
+ }
168
+
169
+ function lineAt(idx, pos) {
170
+ let lo = 0;
171
+ let hi = idx.length - 1;
172
+ while (lo < hi) {
173
+ const mid = (lo + hi + 1) >> 1;
174
+ if (idx[mid] <= pos) lo = mid;
175
+ else hi = mid - 1;
176
+ }
177
+ return lo + 1;
178
+ }
179
+
180
+ /* ---------- configurazione ---------- */
181
+
182
+ async function loadConfig(configPath) {
183
+ const abs = path.resolve(configPath);
184
+ const mod = await import(pathToFileURL(abs).href);
185
+ const raw = mod.default ?? mod;
186
+
187
+ for (const key of ['src', 'css']) {
188
+ if (!Array.isArray(raw[key]) || !raw[key].length) {
189
+ throw new Error(`Nel file di configurazione manca la lista "${key}"`);
190
+ }
191
+ }
192
+
193
+ const baseDir = path.dirname(abs);
194
+ const toAbs = list => list.map(p => path.resolve(baseDir, p));
195
+ const modules = { ...DEFAULTS.cssModules, ...(raw.cssModules ?? {}) };
196
+
197
+ return {
198
+ src: toAbs(raw.src),
199
+ css: toAbs(raw.css),
200
+ ext: normalizeExt(raw.ext ?? DEFAULTS.ext),
201
+ cssExt: normalizeExt(raw.cssExt ?? DEFAULTS.cssExt),
202
+ excludeDir: new Set(raw.excludeDir ?? DEFAULTS.excludeDir),
203
+ excludeCssFiles: new Set(toAbs(raw.excludeCssFiles ?? DEFAULTS.excludeCssFiles)),
204
+ baseDir,
205
+ ignore: (raw.ignore ?? DEFAULTS.ignore).map(s => new RegExp(s)),
206
+ cssModules: {
207
+ enabled: Boolean(modules.enabled),
208
+ ext: normalizeExt(modules.ext),
209
+ pattern: new RegExp(modules.pattern),
210
+ camelCase: Boolean(modules.camelCase)
211
+ }
212
+ };
213
+ }
214
+
215
+ /* ---------- estrazione classi dai CSS ---------- */
216
+
217
+ function extractCssClasses(css) {
218
+ const clean = css
219
+ .replace(/\/\*[\s\S]*?\*\//g, '') /* commenti */
220
+ .replace(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'/g, '""') /* stringhe */
221
+ .replace(/url\([^)]*\)/gi, 'url()');
222
+
223
+ const classes = new Set();
224
+ const preludeRe = /([^{}]+)\{/g; /* testo che precede ogni "{" */
225
+ let m;
226
+
227
+ while ((m = preludeRe.exec(clean))) {
228
+ /* con il CSS nesting il testo può includere dichiarazioni precedenti */
229
+ const prelude = m[1].slice(m[1].lastIndexOf(';') + 1).trim();
230
+ if (!prelude || prelude.startsWith('@')) continue; /* at-rule: @media, @font-face, ecc. */
231
+
232
+ for (const c of prelude.matchAll(CSS_CLASS_RE)) {
233
+ classes.add(unescapeCss(c[1]));
234
+ }
235
+ }
236
+
237
+ return classes;
238
+ }
239
+
240
+ /* ---------- estrazione classi dai sorgenti ---------- */
241
+
242
+ function* extractUsages(text) {
243
+ for (const m of text.matchAll(CLASS_ATTR_RE)) {
244
+ for (const cls of splitTokens(m[2])) yield { cls, index: m.index };
245
+ }
246
+
247
+ for (const m of text.matchAll(CLASSLIST_RE)) {
248
+ for (const lit of m[1].matchAll(STRING_RE)) {
249
+ for (const cls of splitTokens(lit[2])) yield { cls, index: m.index };
250
+ }
251
+ }
252
+
253
+ for (const m of text.matchAll(BY_CLASS_RE)) {
254
+ for (const cls of splitTokens(m[2])) yield { cls, index: m.index };
255
+ }
256
+
257
+ for (const m of text.matchAll(SELECTOR_RE)) {
258
+ const selector = maskDynamic(m[3]);
259
+ for (const c of selector.matchAll(CSS_CLASS_RE)) {
260
+ /* se la classe è seguita da una parte dinamica il nome è incompleto */
261
+ if (selector[c.index + c[0].length] === DYN) continue;
262
+ yield { cls: unescapeCss(c[1]), index: m.index };
263
+ }
264
+ }
265
+ }
266
+
267
+ /*
268
+ * Classi dei CSS modules: individua le variabili a cui è assegnato un file
269
+ * module (qualunque nome, non solo "styles") e cerca nome.xxx / nome['xxx'].
270
+ * Il caso ${styles.xxx} è coperto da nome.xxx.
271
+ * "module" è il percorso assoluto del file importato, o null se non risolvibile
272
+ * (es. alias di webpack).
273
+ */
274
+ function* extractModuleUsages(text, file, pattern) {
275
+ const bindings = new Map();
276
+
277
+ for (const re of [IMPORT_RE, REQUIRE_RE]) {
278
+ for (const m of text.matchAll(re)) {
279
+ const name = m[1];
280
+ const spec = m[3];
281
+ if (!pattern.test(spec)) continue;
282
+
283
+ let resolved = null;
284
+ if (spec.startsWith('.')) {
285
+ const candidate = path.resolve(path.dirname(file), spec);
286
+ if (fs.existsSync(candidate)) resolved = candidate;
287
+ }
288
+ bindings.set(name, resolved);
289
+ }
290
+ }
291
+
292
+ for (const [name, resolved] of bindings) {
293
+ const id = escapeRegExp(name);
294
+ const dotRe = new RegExp(`(?<![\\w$.])${id}\\.([A-Za-z_$][\\w$]*)`, 'g');
295
+ const bracketRe = new RegExp(`(?<![\\w$.])${id}\\[\\s*(["'])([^"']+)\\1\\s*\\]`, 'g');
296
+
297
+ for (const m of text.matchAll(dotRe)) {
298
+ yield { cls: m[1], index: m.index, isModule: true, module: resolved };
299
+ }
300
+ for (const m of text.matchAll(bracketRe)) {
301
+ yield { cls: m[2], index: m.index, isModule: true, module: resolved };
302
+ }
303
+ }
304
+ }
305
+
306
+ /* ---------- main ---------- */
307
+
308
+ const { values } = parseArgs({
309
+ options: {
310
+ config: { type: 'string', short: 'c' },
311
+ json: { type: 'boolean', default: false },
312
+ help: { type: 'boolean', short: 'h', default: false }
313
+ }
314
+ });
315
+
316
+ if (values.help) {
317
+ const header = fs.readFileSync(new URL(import.meta.url), 'utf8').match(/\/\*[\s\S]*?\*\//)[0];
318
+ console.log(header);
319
+ process.exit(0);
320
+ }
321
+
322
+ if (!values.config) {
323
+ console.error('Specificare il file di configurazione con --config <file> (usare --help per i dettagli).');
324
+ process.exit(2);
325
+ }
326
+
327
+ let cfg;
328
+ try {
329
+ cfg = await loadConfig(values.config);
330
+ } catch (err) {
331
+ console.error(`Errore nel file di configurazione: ${err.message}`);
332
+ process.exit(2);
333
+ }
334
+
335
+ const camel = cfg.cssModules.camelCase;
336
+
337
+ /* con camelCase attivo ogni classe è disponibile anche in forma camelCase */
338
+ function withCamel(set) {
339
+ if (camel) for (const c of [...set]) set.add(camelize(c));
340
+ return set;
341
+ }
342
+
343
+ const allCssFiles = collectFiles(cfg.css, cfg.cssExt, cfg.excludeDir);
344
+ const cssFiles = allCssFiles.filter(f => !cfg.excludeCssFiles.has(f));
345
+ const srcFiles = collectFiles(cfg.src, cfg.ext, cfg.excludeDir).filter(f => !allCssFiles.includes(f));
346
+
347
+ if (!cssFiles.length) {
348
+ console.error('Nessun file CSS trovato.');
349
+ process.exit(2);
350
+ }
351
+
352
+ /*
353
+ * I file module presenti nella lista CSS non contribuiscono alle classi
354
+ * "globali" (sono raggiungibili solo tramite import), ma servono come
355
+ * riferimento di riserva quando l'import non è risolvibile.
356
+ */
357
+ const globalClasses = new Set();
358
+ const allModuleClasses = new Set();
359
+ const moduleCache = new Map();
360
+
361
+ function getModuleClasses(file) {
362
+ if (!moduleCache.has(file)) {
363
+ moduleCache.set(file, withCamel(extractCssClasses(fs.readFileSync(file, 'utf8'))));
364
+ }
365
+ return moduleCache.get(file);
366
+ }
367
+
368
+ for (const file of cssFiles) {
369
+ if (cfg.cssModules.enabled && cfg.cssModules.pattern.test(file)) {
370
+ for (const cls of getModuleClasses(file)) allModuleClasses.add(cls);
371
+ } else {
372
+ for (const cls of extractCssClasses(fs.readFileSync(file, 'utf8'))) globalClasses.add(cls);
373
+ }
374
+ }
375
+
376
+ const fallbackClasses = new Set([...globalClasses, ...allModuleClasses]);
377
+
378
+ function isDefined(usage) {
379
+ /* usages of an excluded CSS module are not checked */
380
+ if (usage.module && cfg.excludeCssFiles.has(usage.module)) return true;
381
+ if (!usage.isModule) return globalClasses.has(usage.cls);
382
+ const set = usage.module ? getModuleClasses(usage.module) : fallbackClasses;
383
+ return set.has(usage.cls);
384
+ }
385
+
386
+ const missing = new Map(); /* classe -> [{ file, line, module? }] */
387
+
388
+ for (const file of srcFiles) {
389
+ const text = fs.readFileSync(file, 'utf8');
390
+ const lineIndex = buildLineIndex(text);
391
+ const rel = path.relative(cfg.baseDir, file);
392
+ const ext = path.extname(file).slice(1).toLowerCase();
393
+
394
+ const usages = [...extractUsages(text)];
395
+ if (cfg.cssModules.enabled && cfg.cssModules.ext.has(ext)) {
396
+ usages.push(...extractModuleUsages(text, file, cfg.cssModules.pattern));
397
+ }
398
+
399
+ for (const usage of usages) {
400
+ if (isDefined(usage) || cfg.ignore.some(re => re.test(usage.cls))) continue;
401
+
402
+ const entry = { file: rel, line: lineAt(lineIndex, usage.index) };
403
+ if (usage.isModule) {
404
+ entry.module = usage.module ? path.relative(cfg.baseDir, usage.module) : 'non risolto';
405
+ }
406
+
407
+ const list = missing.get(usage.cls) ?? [];
408
+ if (!list.some(e => e.file === entry.file && e.line === entry.line && e.module === entry.module)) {
409
+ list.push(entry);
410
+ }
411
+ missing.set(usage.cls, list);
412
+ }
413
+ }
414
+
415
+ const sorted = [...missing.entries()].sort((a, b) => a[0].localeCompare(b[0]));
416
+
417
+ if (values.json) {
418
+ console.log(JSON.stringify({
419
+ cssFiles: cssFiles.length,
420
+ sourceFiles: srcFiles.length,
421
+ definedClasses: globalClasses.size,
422
+ missing: Object.fromEntries(sorted)
423
+ }, null, 2));
424
+ } else {
425
+ const reportPath = path.resolve(cfg.baseDir, REPORT_FILE);
426
+
427
+ if (sorted.length) {
428
+ /* flat list sorted by file and line, with a clickable link to each occurrence */
429
+ const items = sorted
430
+ .flatMap(([cls, locations]) => locations.map(loc => ({ cls, ...loc })))
431
+ .sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
432
+
433
+ const lines = items.map(({ cls, file, line, module }) => {
434
+ const href = `${file.split(path.sep).map(encodeURIComponent).join('/')}#L${line}`;
435
+ return `* [${path.basename(file)}:${line}](${href}) -> \`.${cls}\``
436
+ + (module ? ` (CSS module: ${module})` : '');
437
+ });
438
+
439
+ fs.writeFileSync(reportPath, `## Missing CSS classes\n\n${lines.join('\n')}\n`, 'utf-8');
440
+ } else if (fs.existsSync(reportPath)) {
441
+ fs.unlinkSync(reportPath);
442
+ }
443
+
444
+ console.log(
445
+ `File CSS: ${cssFiles.length} (${globalClasses.size} classi globali) | File analizzati: ${srcFiles.length}\n`
446
+ + `Classi mancanti: ${sorted.length}`
447
+ + (sorted.length ? ` -> ${path.relative(process.cwd(), reportPath)}` : '')
448
+ );
449
+ }
450
+
451
+ process.exit(sorted.length ? 1 : 0);
@@ -0,0 +1,44 @@
1
+ /*
2
+ * Configurazione per find-prphan-css-classes.mjs
3
+ * I percorsi relativi sono risolti rispetto alla directory di questo file.
4
+ *
5
+ * Config Da Copiare Nella Root Del Progetto
6
+ */
7
+
8
+ const minimo_path = '../../node_modules/@massimo-cassandro/minimo';
9
+
10
+ export default {
11
+ /* file o directory da analizzare */
12
+ src: ['../src', '../../templates'],
13
+
14
+ /* file o directory contenenti i CSS */
15
+ css: ['./', '../src', `${minimo_path}/src`],
16
+
17
+ /* estensioni dei sorgenti */
18
+ ext: ['html', 'js', 'jsx', 'php'],
19
+
20
+ /* estensioni dei file CSS */
21
+ cssExt: ['css'],
22
+
23
+ /* directory da saltare */
24
+ excludeDir: [/* 'node_modules', */'.git', 'dist', 'vendor'],
25
+
26
+ /* file CSS (o CSS modules) da non considerare nel confronto (default: []) */
27
+ excludeCssFiles: [],
28
+
29
+ /* classi da ignorare (RegExp o stringhe) */
30
+ ignore: [/^js-/, /^is-/],
31
+
32
+ cssModules: {
33
+ enabled: true,
34
+
35
+ /* sorgenti in cui cercare l'uso dei CSS modules */
36
+ ext: ['js', 'jsx'],
37
+
38
+ /* file considerati CSS modules */
39
+ pattern: /\.module\.css$/,
40
+
41
+ /* se true, styles.myClass corrisponde anche a .my-class */
42
+ camelCase: false
43
+ }
44
+ };
@@ -3,5 +3,5 @@
3
3
 
4
4
  */
5
5
 
6
- @import '@fontsource-variable/rubik/wght.css';
7
- @import '@fontsource-variable/rubik/wght-italic.css';
6
+ /* @import '@fontsource-variable/rubik/wght.css';
7
+ @import '@fontsource-variable/rubik/wght-italic.css'; */
@@ -41,6 +41,10 @@
41
41
  {
42
42
  "filePath": "node_modules/open-props/open-props.style-dictionary-tokens.json",
43
43
  "group": "Default"
44
+ },
45
+ {
46
+ "filePath": "node_modules/@massimo-cassandro/minimo/src/css",
47
+ "group": "Default"
44
48
  }
45
49
  ],
46
50
 
@@ -11,7 +11,16 @@ indent_style = space
11
11
  insert_final_newline = true
12
12
  trim_trailing_whitespace = true
13
13
 
14
+ # [*.yaml]
15
+ # indent_size = 4
16
+
17
+ # [*.php]
18
+ # indent_size = 4
19
+
20
+ # [{compose.yaml,compose.*.yaml}]
21
+ # indent_size = 2
22
+
14
23
  [*.md]
15
- max_line_length = off
24
+ max_line_length = 80
16
25
  trim_trailing_whitespace = false
17
26
 
@@ -22,3 +22,4 @@ node_modules
22
22
  _private
23
23
  public/_dev
24
24
  unresolved-unused-props.md
25
+ orphan-classes.md
@@ -22,7 +22,8 @@ class UnsplashController extends AbstractController
22
22
  $orientation = 'landscape';
23
23
  }
24
24
 
25
- $unsplash_access_key = 'xxxxxxx';
25
+ $unsplash_access_key = 'xxxxxxx'; // vedi minimo/_private/unsplash-access-keys.md
26
+
26
27
  $collections_ids = '3660951'; // comma separated list
27
28
  $count = null;
28
29
 
@@ -34,7 +34,7 @@ twig:
34
34
  file_name_pattern: '*.twig'
35
35
 
36
36
  form_themes:
37
- - '_tpl/incl/bs5-form-layout.html.twig'
37
+ - '_incl/minimo-form-layout.html.twig'
38
38
  paths:
39
39
  '%kernel.project_dir%/public/%env(ASSETS_DIR)%': assets_path
40
40
  '%kernel.project_dir%/public': public_path
@@ -4,17 +4,22 @@
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6
6
  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
7
- <title>{%- block title -%}{%- endblock %} | Marketing</title>
7
+ <title>{%- block title -%}{%- endblock %} | xxxxx</title>
8
8
  <meta name="robots" content="noindex, nofollow, noarchive">
9
9
  {%- include '_incl/favicons.incl.html.twig'-%}
10
+
11
+ <link rel="preload" href="{{ asset('fonts/xxxxx.woff2') }}" as="font" type="font/woff2" crossorigin>
12
+
10
13
  {{- '' -}}
14
+ {#- shared.js, runtime.js e shared.css servono solo con `useSharedChunk: true` in webpack.config.mjs:
15
+ con `false` rimuovere tutte le righe che li referenziano (qui sotto e a fine body) -#}
11
16
  <link rel="preload" href="{{ asset('index.js') }}" as="script">
12
17
  <link rel="preload" href="{{ asset('shared.js') }}" as="script">
13
18
  <link rel="preload" href="{{ asset('runtime.js') }}" as="script">
14
19
  {{- '' -}}
15
20
  <link rel="preload" href="{{ asset('shared.css') }}" as="style">
16
21
  <link rel="preload" href="{{ asset('index.css') }}" as="style">
17
- {# shared.css va linkato PRIMA del css della entry (ordine della cascata) #}
22
+ {# shared.css va inserito PRIMA del css della entry (ordine della cascata) #}
18
23
  <link rel="stylesheet" href="{{ asset('shared.css') }}" type="text/css" media="all">
19
24
  <link rel="stylesheet" href="{{ asset('index.css') }}" type="text/css" media="all">
20
25
  <style>
@@ -24,6 +29,8 @@
24
29
  </head>
25
30
  <body>
26
31
 
32
+ {%- block body -%}{%- endblock -%}
33
+
27
34
  <script src="{{ asset('runtime.js') }}"></script>
28
35
  <script src="{{ asset('shared.js') }}"></script>
29
36
  <script src="{{ asset('index.js') }}"></script>
@@ -1,6 +1,6 @@
1
1
  {#- https://symfony.com/doc/current/controller/error_pages.html#testing-error-pages-during-development -#}
2
2
  <!DOCTYPE html>
3
- <html lang="it" data-err-code="{{ status_code }}" data-err-text="{{ status_text}}">
3
+ <html lang="it" class="uspg" data-err-code="{{ status_code }}" data-err-text="{{ status_text}}">
4
4
  <head>
5
5
  <meta charset="utf-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
@@ -8,6 +8,10 @@
8
8
  <title>Errore {{ status_code }}</title>
9
9
  <meta name="robots" content="noindex, nofollow, noarchive">
10
10
  {%- include '_incl/favicons.incl.html.twig'-%}
11
+ {#- shared.css, shared.js e runtime.js servono solo con `useSharedChunk: true` in webpack.config.mjs:
12
+ con `false` rimuovere tutte le righe che li referenziano -#}
13
+ <link rel="preload" href="{{ asset('shared.css') }}" as="style">
14
+ <link rel="stylesheet" href="{{ asset('shared.css') }}" media="all">
11
15
  <link rel="preload" href="{{ asset('index.css') }}" as="style">
12
16
  <link rel="stylesheet" href="{{ asset('index.css') }}" media="all">
13
17
  <link rel="preload" href="{{ asset('error-pages.css') }}" as="style">
@@ -0,0 +1,17 @@
1
+ {% extends '_main-tpl.html.twig' %}
2
+
3
+ {% block title %}xxxx{% endblock %}
4
+ {% block head %}
5
+
6
+ <link rel="preload" href="{{ asset('__page__.css') }}" as="style">
7
+ <link rel="stylesheet" href="{{ asset('__page__.css') }}" media="all">
8
+ <link rel="preload" href="{{ asset('__page__.js') }}" as="script">
9
+ {% endblock %}
10
+ {% block js %}
11
+ <script src="{{ asset('__page__.js') }}"></script>
12
+ {% endblock %}
13
+
14
+ {% block body %}
15
+ home
16
+ <div id="root"><div class="spinner"></div></div>
17
+ {% endblock %}