mypgs 1.6.0 → 2.1.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.
Files changed (125) hide show
  1. package/AGENTS.md +2 -2
  2. package/README.md +25 -25
  3. package/assets/javascript/components/_accordion.js +2 -2
  4. package/assets/javascript/components/_modals.js +159 -136
  5. package/assets/javascript/components/_notifications.js +19 -9
  6. package/assets/javascript/components/_search.js +2 -2
  7. package/assets/javascript/components/_slides.js +66 -32
  8. package/assets/javascript/components/_stepTabs.js +4 -4
  9. package/assets/javascript/components/_steps.js +2 -2
  10. package/assets/javascript/components/_summary.js +2 -2
  11. package/assets/javascript/functions/_formValidate.js +97 -53
  12. package/assets/javascript/functions/_scrollY.js +10 -10
  13. package/assets/javascript/layout/_header.js +3 -0
  14. package/assets/scss/base/_html.scss +2 -2
  15. package/assets/scss/base/_variables.scss +2 -2
  16. package/assets/scss/components/_accordion.scss +0 -2
  17. package/assets/scss/components/_button.scss +1 -1
  18. package/assets/scss/components/_form.scss +2 -18
  19. package/assets/scss/components/_formAddon.scss +18 -0
  20. package/assets/scss/components/_modals.scss +5 -3
  21. package/assets/scss/components/_notification.scss +23 -30
  22. package/assets/scss/components/_search.scss +29 -23
  23. package/assets/scss/components/_slides.scss +0 -1
  24. package/assets/scss/components/_tooltip.scss +0 -1
  25. package/assets/scss/index.scss +1 -0
  26. package/assets/scss/layout/_footer.scss +0 -2
  27. package/assets/scss/layout/_header.scss +9 -1
  28. package/assets/scss/layout/_pageShell.scss +24 -17
  29. package/assets/scss/mixin/_mx-button.scss +30 -76
  30. package/assets/scss/mixin/_mx-card.scss +11 -9
  31. package/assets/scss/mixin/_mx-form.scss +10 -12
  32. package/assets/scss/mixin/{_mx-form-addon.scss → _mx-formAddon.scss} +23 -26
  33. package/assets/scss/mixin/_mx-hover.scss +75 -0
  34. package/assets/scss/mixin/_settings.scss +2 -1
  35. package/assets/scss/mixin/mixin.scss +2 -1
  36. package/{templates/html → demo}/demo.html +1 -1
  37. package/demo/demo.js +241 -0
  38. package/dist/css/index.css +1317 -1674
  39. package/dist/css/index.css.map +1 -1
  40. package/dist/css/index.min.css +1 -1
  41. package/dist/javascript/index.js +356 -255
  42. package/dist/javascript/index.js.map +1 -1
  43. package/dist/javascript/index.min.js +1 -1
  44. package/docs/componenti-e-markup.md +32 -32
  45. package/docs/components/accordion.md +61 -0
  46. package/docs/components/badges.md +67 -0
  47. package/docs/components/breadcumbs.md +43 -0
  48. package/docs/components/button.md +59 -0
  49. package/docs/components/card.md +50 -0
  50. package/docs/components/dropdown.md +135 -0
  51. package/docs/components/form.md +136 -0
  52. package/docs/components/formAddon.md +79 -0
  53. package/docs/components/logo.md +36 -0
  54. package/docs/components/menu.md +90 -0
  55. package/docs/components/modal.md +131 -0
  56. package/docs/components/notification.md +81 -0
  57. package/docs/components/search.md +118 -0
  58. package/docs/components/slides.md +85 -0
  59. package/docs/components/stepTabs.md +93 -0
  60. package/docs/components/steps.md +56 -0
  61. package/docs/components/summary.md +49 -0
  62. package/docs/components/table.md +42 -0
  63. package/docs/components/tooltip.md +53 -0
  64. package/docs/convenzioni.md +11 -11
  65. package/docs/export-e-sviluppo.md +3 -3
  66. package/docs/helper-javascript.md +73 -8
  67. package/docs/layout/body.md +41 -0
  68. package/docs/layout/flex.md +116 -0
  69. package/docs/layout/footer.md +71 -0
  70. package/docs/layout/grid.md +116 -0
  71. package/docs/layout/header.md +116 -0
  72. package/docs/layout/pageShell.md +94 -0
  73. package/docs/layout/section.md +69 -0
  74. package/docs/patterns/cookieConsent.md +89 -0
  75. package/docs/utilizzo-css-scss.md +8 -8
  76. package/package.json +2 -1
  77. package/scripts/generate-component-docs.js +557 -0
  78. package/templates/html/components/accordion.html +36 -7
  79. package/templates/html/components/badges.html +28 -5
  80. package/templates/html/components/breadcumbs.html +15 -1
  81. package/templates/html/components/button.html +28 -6
  82. package/templates/html/components/card.html +28 -8
  83. package/templates/html/components/dropdown.html +42 -8
  84. package/templates/html/components/form.html +110 -9
  85. package/templates/html/components/formAddon.html +72 -0
  86. package/templates/html/components/logo.html +22 -1
  87. package/templates/html/components/menu.html +33 -6
  88. package/templates/html/components/modal.html +58 -14
  89. package/templates/html/components/notification.html +68 -32
  90. package/templates/html/components/search.html +85 -8
  91. package/templates/html/components/slides.html +46 -3
  92. package/templates/html/components/stepTabs.html +55 -8
  93. package/templates/html/components/steps.html +24 -0
  94. package/templates/html/components/summary.html +30 -3
  95. package/templates/html/components/table.html +14 -2
  96. package/templates/html/components/tooltip.html +35 -0
  97. package/templates/html/layout/body.html +20 -6
  98. package/templates/html/layout/flex.html +53 -33
  99. package/templates/html/layout/footer.html +32 -4
  100. package/templates/html/layout/grid.html +53 -33
  101. package/templates/html/layout/header.html +47 -6
  102. package/templates/html/layout/pageShell.html +19 -0
  103. package/templates/html/layout/section.html +41 -19
  104. package/templates/html/patterns/cookieConsent.html +44 -13
  105. package/templates/react/components/accordion.jsx +4 -4
  106. package/templates/react/components/breadcumbs.jsx +1 -1
  107. package/templates/react/components/button.jsx +5 -5
  108. package/templates/react/components/card.jsx +4 -4
  109. package/templates/react/components/dropdown.jsx +8 -8
  110. package/templates/react/components/form.jsx +5 -5
  111. package/templates/react/components/modal.jsx +14 -14
  112. package/templates/react/components/notification.jsx +7 -7
  113. package/templates/react/components/search.jsx +5 -5
  114. package/templates/react/components/slides.jsx +3 -3
  115. package/templates/react/components/stepTabs.jsx +8 -8
  116. package/templates/react/components/summary.jsx +3 -3
  117. package/templates/react/components/table.jsx +2 -2
  118. package/templates/react/layout/flex.jsx +8 -8
  119. package/templates/react/layout/grid.jsx +8 -8
  120. package/templates/react/layout/section.jsx +12 -12
  121. package/templates/react/patterns/cookieConsent.jsx +8 -8
  122. package/templates/react/patterns/footer.jsx +4 -4
  123. package/templates/react/patterns/header.jsx +3 -3
  124. package/templates/html/demo.js +0 -130
  125. /package/{templates/html → demo}/demo.css +0 -0
@@ -0,0 +1,557 @@
1
+ #!/usr/bin/env node
2
+
3
+ "use strict";
4
+
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+
8
+ const PROJECT_ROOT = path.resolve(__dirname, "..");
9
+ const TEMPLATE_ROOT = path.join(PROJECT_ROOT, "templates", "html");
10
+ const DOCS_ROOT = path.join(PROJECT_ROOT, "docs");
11
+ const MANAGED_DOC_DIRECTORIES = ["components", "layout", "patterns"]
12
+ .map(directory => path.join(DOCS_ROOT, directory));
13
+ const SOURCE_ROOTS = [
14
+ path.join(PROJECT_ROOT, "assets", "javascript"),
15
+ path.join(PROJECT_ROOT, "assets", "scss"),
16
+ ];
17
+
18
+ const TAG_ORDER = ["title", "description", "pgs", "pgs-option", "pgs-state", "api", "related", "return"];
19
+ const LIST_TAGS = new Set(["pgs", "pgs-option", "pgs-state", "api", "related"]);
20
+ const REQUIRED_TAGS = ["title", "description", "pgs"];
21
+ const GENERATED_MARKER = /^<!-- (?:Automatically generated from (templates\/html\/.+\.html)\. Edit \1 and run npm run docs:generate again\.|File generato automaticamente da (templates\/html\/.+\.html)\. Modificare \2 e rieseguire npm run docs:generate\.) -->$/;
22
+
23
+ function toPosix(value) {
24
+ return value.split(path.sep).join("/");
25
+ }
26
+
27
+ function relativeToProject(value) {
28
+ return toPosix(path.relative(PROJECT_ROOT, value));
29
+ }
30
+
31
+ function getOutputPath(template) {
32
+ const relativeTemplate = path.relative(TEMPLATE_ROOT, template);
33
+ return path.join(DOCS_ROOT, relativeTemplate.replace(/\.html$/i, ".md"));
34
+ }
35
+
36
+ function normalizeEol(value) {
37
+ return value.replace(/\r\n?/g, "\n");
38
+ }
39
+
40
+ function walkFiles(root, predicate) {
41
+ if (!fs.existsSync(root)) return [];
42
+
43
+ const files = [];
44
+ const visit = directory => {
45
+ fs.readdirSync(directory, { withFileTypes: true })
46
+ .sort((a, b) => a.name.localeCompare(b.name, "en"))
47
+ .forEach(entry => {
48
+ const absolute = path.join(directory, entry.name);
49
+ if (entry.isDirectory()) visit(absolute);
50
+ else if (entry.isFile() && predicate(absolute)) files.push(absolute);
51
+ });
52
+ };
53
+
54
+ visit(root);
55
+ return files.sort((a, b) => toPosix(a).localeCompare(toPosix(b), "en"));
56
+ }
57
+
58
+ function createError(file, message, suggestion, section, value) {
59
+ return { file, message, suggestion, section, value };
60
+ }
61
+
62
+ function parseDocumentationBlock(file, source) {
63
+ const errors = [];
64
+ const relativeFile = relativeToProject(file);
65
+ const hasBom = source.charCodeAt(0) === 0xFEFF;
66
+ const content = hasBom ? source.slice(1) : source;
67
+ const blockPattern = /\/\*\*[\s\S]*?\*\//g;
68
+ const taggedBlocks = [...content.matchAll(blockPattern)]
69
+ .filter(match => /@(title|description|pgs(?:-option|-state)?|api|related|return)\b/.test(match[0]));
70
+ const initialHtmlComment = content.match(/^<!--[\t\r\n ]*(\/\*\*[\s\S]*?\*\/)[\t\r\n ]*-->/);
71
+
72
+ if (taggedBlocks.length === 0) {
73
+ errors.push(createError(relativeFile, "Commento strutturato iniziale mancante.", "Aggiungi un blocco <!-- /** ... */ --> come primo contenuto del file."));
74
+ return { errors };
75
+ }
76
+
77
+ if (taggedBlocks.length > 1) {
78
+ errors.push(createError(relativeFile, `Trovati ${taggedBlocks.length} commenti strutturati.`, "Mantieni un solo blocco di documentazione iniziale."));
79
+ }
80
+
81
+ const first = taggedBlocks[0];
82
+ const hasValidHtmlWrapper = initialHtmlComment && initialHtmlComment[1] === first[0];
83
+ if (!hasValidHtmlWrapper) {
84
+ errors.push(createError(
85
+ relativeFile,
86
+ "Il commento strutturato non è un commento HTML iniziale valido.",
87
+ "Posizionalo all'inizio e racchiudi il blocco /** ... */ tra <!-- e -->, così il browser non lo mostra nella pagina.",
88
+ ));
89
+ }
90
+
91
+ const lines = normalizeEol(first[0]).split("\n");
92
+ if (lines[0].trim() !== "/**" || lines.at(-1).trim() !== "*/") {
93
+ errors.push(createError(relativeFile, "Delimitatori del commento strutturato non validi.", "Usa /** su una riga e */ su una riga separata."));
94
+ return { errors };
95
+ }
96
+
97
+ const bodyLines = [];
98
+ for (let index = 1; index < lines.length - 1; index += 1) {
99
+ const match = lines[index].match(/^\s*\*(?: ?(.*))?$/);
100
+ if (!match) {
101
+ errors.push(createError(relativeFile, `Riga ${index + 1} del commento non valida.`, "Prefissa ogni riga interna con *.", "commento"));
102
+ continue;
103
+ }
104
+ bodyLines.push(match[1] || "");
105
+ }
106
+
107
+ const data = {
108
+ title: "",
109
+ description: "",
110
+ pgs: [],
111
+ "pgs-option": [],
112
+ "pgs-state": [],
113
+ api: [],
114
+ related: [],
115
+ return: "",
116
+ };
117
+ const seenTags = new Set();
118
+ let activeTag = null;
119
+ let previousOrder = -1;
120
+
121
+ bodyLines.forEach((rawLine, lineIndex) => {
122
+ const line = rawLine.trim();
123
+ if (!line) return;
124
+
125
+ const tagMatch = line.match(/^@([a-z-]+)(?:\s+(.*))?$/);
126
+ if (tagMatch) {
127
+ const tag = tagMatch[1];
128
+ const value = (tagMatch[2] || "").trim();
129
+ const order = TAG_ORDER.indexOf(tag);
130
+
131
+ if (order === -1) {
132
+ errors.push(createError(relativeFile, `Tag sconosciuto @${tag}.`, "Usa solamente i tag previsti dal formato.", `@${tag}`));
133
+ activeTag = null;
134
+ return;
135
+ }
136
+ if (seenTags.has(tag)) {
137
+ errors.push(createError(relativeFile, `Sezione @${tag} duplicata.`, "Mantieni una sola sezione per tag.", `@${tag}`));
138
+ }
139
+ if (order < previousOrder) {
140
+ errors.push(createError(relativeFile, `La sezione @${tag} non rispetta l'ordine canonico.`, `Usa l'ordine: ${TAG_ORDER.map(item => `@${item}`).join(", ")}.`, `@${tag}`));
141
+ }
142
+
143
+ seenTags.add(tag);
144
+ previousOrder = Math.max(previousOrder, order);
145
+ activeTag = tag;
146
+
147
+ if (LIST_TAGS.has(tag)) {
148
+ if (value) errors.push(createError(relativeFile, `La sezione @${tag} deve iniziare su una riga senza contenuto.`, "Sposta le voci nelle righe successive con il formato - valore: descrizione.", `@${tag}`));
149
+ } else {
150
+ if (!value) errors.push(createError(relativeFile, `Il tag @${tag} è vuoto.`, "Aggiungi il testo sulla stessa riga del tag.", `@${tag}`));
151
+ data[tag] = value;
152
+ activeTag = null;
153
+ }
154
+ return;
155
+ }
156
+
157
+ if (!activeTag || !LIST_TAGS.has(activeTag)) {
158
+ errors.push(createError(relativeFile, `Contenuto non associato a un tag alla riga ${lineIndex + 2}.`, "Inserisci il contenuto in una sezione riconosciuta."));
159
+ return;
160
+ }
161
+
162
+ const itemMatch = line.match(/^-\s+([^:]+):\s+(.+)$/);
163
+ if (!itemMatch) {
164
+ errors.push(createError(relativeFile, `Voce non valida nella sezione @${activeTag}: "${line}".`, "Usa il formato - valore: descrizione.", `@${activeTag}`));
165
+ return;
166
+ }
167
+
168
+ const key = itemMatch[1].trim();
169
+ const description = itemMatch[2].trim();
170
+ if (!key || !description) {
171
+ errors.push(createError(relativeFile, `Voce incompleta nella sezione @${activeTag}.`, "Specifica sia il valore sia la descrizione.", `@${activeTag}`, key));
172
+ return;
173
+ }
174
+ if (data[activeTag].some(item => item.key === key)) {
175
+ errors.push(createError(relativeFile, `Valore duplicato "${key}" nella sezione @${activeTag}.`, "Rimuovi la voce duplicata.", `@${activeTag}`, key));
176
+ return;
177
+ }
178
+ if (activeTag === "pgs-option" && /[\s[\]]/.test(key)) {
179
+ errors.push(createError(relativeFile, `La voce @pgs-option "${key}" non è una chiave valida.`, "Documenta la chiave senza payload, per esempio position invece di position[top left].", "@pgs-option", key));
180
+ return;
181
+ }
182
+ data[activeTag].push({ key, description });
183
+ });
184
+
185
+ REQUIRED_TAGS.forEach(tag => {
186
+ const value = data[tag];
187
+ if (!seenTags.has(tag) || (Array.isArray(value) ? value.length === 0 : !value)) {
188
+ errors.push(createError(relativeFile, `Sezione obbligatoria @${tag} mancante o vuota.`, `Aggiungi una sezione @${tag} valida.`, `@${tag}`));
189
+ }
190
+ });
191
+
192
+ LIST_TAGS.forEach(tag => {
193
+ if (seenTags.has(tag) && data[tag].length === 0) {
194
+ errors.push(createError(relativeFile, `Sezione @${tag} dichiarata ma vuota.`, `Aggiungi almeno una voce oppure rimuovi la sezione @${tag}.`, `@${tag}`));
195
+ }
196
+ });
197
+
198
+ const documentationContainer = hasValidHtmlWrapper ? initialHtmlComment[0] : first[0];
199
+ const markupStart = hasValidHtmlWrapper ? documentationContainer.length : first.index + documentationContainer.length;
200
+ const markup = normalizeEol(content.slice(markupStart)).trim();
201
+ if (!markup) {
202
+ errors.push(createError(relativeFile, "Contenuto HTML mancante sotto il commento.", "Mantieni l'esempio HTML ufficiale dopo il blocco di documentazione."));
203
+ }
204
+
205
+ return { errors, data, markup, block: documentationContainer, hasBom };
206
+ }
207
+
208
+ function tokenizeOptions(value) {
209
+ return value.match(/[^\s[\]]+(?:\[[^\]]*\])?/g) || [];
210
+ }
211
+
212
+ function splitOption(value) {
213
+ const match = value.match(/^([^\s[\]]+)(?:\[([^\]]*)\])?$/);
214
+ return match ? { key: match[1], payload: match[2] } : { key: "", payload: undefined };
215
+ }
216
+
217
+ function extractAttributes(markup) {
218
+ const activeMarkup = markup.replace(/<!--[\s\S]*?-->/g, "");
219
+ const result = { pgs: [], options: [], states: [] };
220
+ const pattern = /\b(pgs(?:-option|-state)?)\s*=\s*(["'])([\s\S]*?)\2/g;
221
+
222
+ for (const match of activeMarkup.matchAll(pattern)) {
223
+ const attribute = match[1];
224
+ const value = match[3];
225
+ if (attribute === "pgs-option") result.options.push(...tokenizeOptions(value));
226
+ else if (attribute === "pgs-state") result.states.push(...value.trim().split(/\s+/).filter(Boolean));
227
+ else result.pgs.push(...value.trim().split(/\s+/).filter(Boolean));
228
+ }
229
+
230
+ Object.keys(result).forEach(key => {
231
+ result[key] = [...new Set(result[key])];
232
+ });
233
+ return result;
234
+ }
235
+
236
+ function stripSourceComments(value) {
237
+ return value
238
+ .replace(/\/\*[\s\S]*?\*\//g, "")
239
+ .replace(/(^|[^:])\/\/.*$/gm, "$1");
240
+ }
241
+
242
+ function normalizeName(value) {
243
+ return value.replace(/^_/, "").replace(/[^a-z0-9]/gi, "").toLowerCase();
244
+ }
245
+
246
+ function singularName(value) {
247
+ return value.endsWith("s") ? value.slice(0, -1) : value;
248
+ }
249
+
250
+ function escapeRegExp(value) {
251
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
252
+ }
253
+
254
+ function containsExactToken(source, token) {
255
+ const escaped = escapeRegExp(token);
256
+ return new RegExp(`(^|[^A-Za-z0-9_-])${escaped}(?=$|[^A-Za-z0-9_-])`).test(source);
257
+ }
258
+
259
+ function containsPgsReference(source, token) {
260
+ const escaped = escapeRegExp(token);
261
+ const patterns = [
262
+ new RegExp(`\\[pgs[^\\]]*(?:["']${escaped}["']|[~=]${escaped})(?=[\\]\\s])`),
263
+ new RegExp(`\\.(?:querySelector|querySelectorAll|contains|add|remove|toggle)\\(\\s*["']${escaped}["']`),
264
+ new RegExp(`pgs\\s*=\\s*["'][^"']*(?:^|\\s)${escaped}(?=\\s|["'])`),
265
+ ];
266
+ return patterns.some(pattern => pattern.test(source));
267
+ }
268
+
269
+ function loadSources() {
270
+ return SOURCE_ROOTS.flatMap(root => walkFiles(root, file => /\.(?:js|scss)$/.test(file)))
271
+ .map(file => ({
272
+ file,
273
+ relative: relativeToProject(file),
274
+ name: normalizeName(path.basename(file, path.extname(file))),
275
+ content: stripSourceComments(normalizeEol(fs.readFileSync(file, "utf8"))),
276
+ }));
277
+ }
278
+
279
+ function associateSources(template, documentation, sources) {
280
+ const basename = normalizeName(path.basename(template, ".html"));
281
+ const singularBasename = singularName(basename);
282
+ const primaryTokens = documentation.pgs.map(item => item.key);
283
+
284
+ const nameMatches = sources.filter(source => {
285
+ const singularSource = singularName(source.name);
286
+ return source.name === basename
287
+ || singularSource === singularBasename
288
+ || source.name.startsWith(basename)
289
+ || basename.startsWith(source.name);
290
+ });
291
+
292
+ if (nameMatches.length > 0) return nameMatches;
293
+ return sources.filter(source => primaryTokens.some(token => containsPgsReference(source.content, token)));
294
+ }
295
+
296
+ function extractSourceFacts(sources) {
297
+ const facts = { options: new Set(), states: new Set() };
298
+
299
+ sources.forEach(source => {
300
+ const content = source.content;
301
+ const optionPatterns = [
302
+ /\.option\.(?:contains|getValueBrackets|setValueBrackets)\(\s*["']([^"']+)["']\s*(?=[,)])/g,
303
+ /pgs-option\s*[~*^$|]?=\s*["']([^"'\[\]\s]+)(?:\[[^\]]*\])?["']/g,
304
+ ];
305
+ const statePatterns = [
306
+ /\.state\.(?:add|remove|toggle|contains)\(\s*["']([^"']+)["']\s*(?=[,)])/g,
307
+ /pgs-state\s*[~*^$|]?=\s*["']([^"'\s]+)["']/g,
308
+ ];
309
+
310
+ optionPatterns.forEach(pattern => {
311
+ for (const match of content.matchAll(pattern)) facts.options.add(splitOption(match[1]).key);
312
+ });
313
+ statePatterns.forEach(pattern => {
314
+ for (const match of content.matchAll(pattern)) facts.states.add(match[1]);
315
+ });
316
+ });
317
+
318
+ facts.options.delete("");
319
+ facts.states.delete("");
320
+ return facts;
321
+ }
322
+
323
+ function validatePosition(file, option, errors) {
324
+ if (option.payload === undefined) return;
325
+ const parts = option.payload.trim().toLowerCase().split(/\s+/).filter(Boolean);
326
+ const side = parts[0];
327
+ const align = parts[1];
328
+ const allowed = {
329
+ top: new Set(["left", "center", "right"]),
330
+ bottom: new Set(["left", "center", "right"]),
331
+ left: new Set(["top", "center", "bottom"]),
332
+ right: new Set(["top", "center", "bottom"]),
333
+ };
334
+
335
+ if (parts.length !== 2 || !allowed[side]?.has(align)) {
336
+ errors.push(createError(file, `Payload non valido per pgs-option position: "${option.payload}".`, "Usa una coppia lato/allineamento compatibile, per esempio position[bottom center].", "@pgs-option", "position"));
337
+ }
338
+ }
339
+
340
+ function validateTemplate(template, parsed, sources, allSourceContent) {
341
+ const errors = [...parsed.errors];
342
+ if (!parsed.data || !parsed.markup) return errors;
343
+
344
+ const file = relativeToProject(template);
345
+ const documentation = parsed.data;
346
+ const attributes = extractAttributes(parsed.markup);
347
+ const documentedPgs = new Set(documentation.pgs.map(item => item.key));
348
+ const documentedRelated = new Set(documentation.related.map(item => item.key));
349
+ const documentedOptions = new Set(documentation["pgs-option"].map(item => item.key));
350
+ const documentedStates = new Set(documentation["pgs-state"].map(item => item.key));
351
+ const associatedSources = associateSources(template, documentation, sources);
352
+ const associatedFacts = extractSourceFacts(associatedSources);
353
+
354
+ attributes.pgs.forEach(token => {
355
+ if (!documentedPgs.has(token) && !documentedRelated.has(token)) {
356
+ errors.push(createError(file, `Valore pgs non documentato: "${token}".`, "Aggiungilo a @pgs oppure a @related.", "@pgs", token));
357
+ }
358
+ });
359
+
360
+ documentedPgs.forEach(token => {
361
+ if (documentedRelated.has(token)) {
362
+ errors.push(createError(file, `Il valore "${token}" compare sia in @pgs sia in @related.`, "Documentalo in una sola sezione.", "@pgs", token));
363
+ }
364
+ });
365
+
366
+ [...documentedPgs, ...documentedRelated].forEach(token => {
367
+ if (!attributes.pgs.includes(token) && !containsExactToken(allSourceContent, token)) {
368
+ errors.push(createError(file, `Valore documentato non trovato nel template, JavaScript o SCSS: "${token}".`, "Correggi il nome oppure rimuovi la voce non implementata.", documentedPgs.has(token) ? "@pgs" : "@related", token));
369
+ }
370
+ });
371
+
372
+ attributes.options.forEach(rawOption => {
373
+ const option = splitOption(rawOption);
374
+ if (!option.key) {
375
+ errors.push(createError(file, `Valore pgs-option non valido: "${rawOption}".`, "Correggi la sintassi dell'attributo pgs-option.", "@pgs-option", rawOption));
376
+ return;
377
+ }
378
+ if (!documentedOptions.has(option.key)) {
379
+ errors.push(createError(file, `Valore pgs-option non documentato: "${option.key}".`, "Aggiungi la chiave alla sezione @pgs-option.", "@pgs-option", option.key));
380
+ }
381
+ if (["containerID", "containerPGS", "tabIcon"].includes(option.key) && (!option.payload || !option.payload.trim())) {
382
+ errors.push(createError(file, `Payload mancante per pgs-option "${option.key}".`, `Usa ${option.key}[valore] con un valore non vuoto.`, "@pgs-option", option.key));
383
+ }
384
+ if (option.key === "tabIcon" && /\s/.test(option.payload || "")) {
385
+ errors.push(createError(file, `Il payload tabIcon deve contenere una sola classe: "${option.payload}".`, "Usa una singola classe icona, per esempio tabIcon[fa-user].", "@pgs-option", option.key));
386
+ }
387
+ if (option.key === "position") validatePosition(file, option, errors);
388
+ });
389
+
390
+ documentation["pgs-option"].forEach(item => {
391
+ const inTemplate = attributes.options.some(value => splitOption(value).key === item.key);
392
+ if (!inTemplate && !associatedFacts.options.has(item.key) && !containsExactToken(allSourceContent, item.key)) {
393
+ errors.push(createError(file, `Il valore @pgs-option "${item.key}" non è stato trovato nei sorgenti collegati.`, "Correggi il nome o rimuovi l'opzione non implementata.", "@pgs-option", item.key));
394
+ }
395
+ });
396
+
397
+ associatedFacts.options.forEach(option => {
398
+ if (!documentedOptions.has(option)) {
399
+ errors.push(createError(file, `Opzione supportata ma non documentata: "${option}".`, "Aggiungila alla sezione @pgs-option.", "@pgs-option", option));
400
+ }
401
+ });
402
+
403
+ attributes.states.forEach(state => {
404
+ if (!documentedStates.has(state)) {
405
+ errors.push(createError(file, `Valore pgs-state non documentato: "${state}".`, "Aggiungilo alla sezione @pgs-state.", "@pgs-state", state));
406
+ }
407
+ });
408
+
409
+ documentation["pgs-state"].forEach(item => {
410
+ if (!attributes.states.includes(item.key) && !associatedFacts.states.has(item.key) && !containsExactToken(allSourceContent, item.key)) {
411
+ errors.push(createError(file, `Il valore @pgs-state "${item.key}" non è stato trovato nel template, JavaScript o SCSS collegato.`, "Correggi il nome o rimuovi lo stato non implementato.", "@pgs-state", item.key));
412
+ }
413
+ });
414
+
415
+ associatedFacts.states.forEach(state => {
416
+ if (!documentedStates.has(state)) {
417
+ errors.push(createError(file, `Stato supportato ma non documentato: "${state}".`, "Aggiungilo alla sezione @pgs-state.", "@pgs-state", state));
418
+ }
419
+ });
420
+
421
+ documentation.api.forEach(item => {
422
+ const signature = item.key.match(/^(?:new\s+)?((?:pgs|instance)(?:\.[A-Za-z_$][\w$]*)+)\([^)]*\)$/);
423
+ if (!signature) {
424
+ errors.push(createError(file, `Firma API non valida: "${item.key}".`, "Usa una firma chiamabile, per esempio pgs.modal.api(element) oppure instance.open().", "@api", item.key));
425
+ return;
426
+ }
427
+
428
+ const identifiers = signature[1].split(".");
429
+ const implementationName = identifiers.at(-1);
430
+ if (!containsExactToken(allSourceContent, implementationName)) {
431
+ errors.push(createError(file, `API documentata non trovata nei sorgenti: "${item.key}".`, "Correggi la firma oppure rimuovi il metodo non esposto.", "@api", item.key));
432
+ }
433
+ });
434
+
435
+ return errors;
436
+ }
437
+
438
+ function renderList(items) {
439
+ return items.map(item => `- \`${item.key}\`: ${item.description}`).join("\n");
440
+ }
441
+
442
+ function renderMarkdown(template, documentation, markup) {
443
+ const relativeTemplate = relativeToProject(template);
444
+ const marker = `<!-- Automatically generated from ${relativeTemplate}. Edit ${relativeTemplate} and run npm run docs:generate again. -->`;
445
+ const sections = [marker, "", `# ${documentation.title}`, "", documentation.description];
446
+ const sectionMap = [
447
+ ["PGS", documentation.pgs],
448
+ ["PGS Options", documentation["pgs-option"]],
449
+ ["PGS States", documentation["pgs-state"]],
450
+ ["JavaScript API", documentation.api],
451
+ ["Related elements", documentation.related],
452
+ ];
453
+
454
+ sectionMap.forEach(([title, items]) => {
455
+ if (items.length === 0) return;
456
+ sections.push("", `## ${title}`, "", renderList(items));
457
+ });
458
+
459
+ if (documentation.return) sections.push("", "## Output", "", documentation.return);
460
+
461
+ const runs = [...markup.matchAll(/`+/g)].map(match => match[0].length);
462
+ const fence = "`".repeat(Math.max(3, (runs.length ? Math.max(...runs) : 0) + 1));
463
+ sections.push("", "## Example", "", `${fence}html`, markup, fence, "");
464
+ return sections.join("\n");
465
+ }
466
+
467
+ function printErrors(errors) {
468
+ errors.forEach(error => {
469
+ console.error(`[ERRORE] ${error.file}`);
470
+ console.error(error.message);
471
+ if (error.section) console.error(`Sezione: ${error.section}`);
472
+ if (error.value) console.error(`Valore: ${error.value}`);
473
+ console.error(`Suggerimento: ${error.suggestion}`);
474
+ console.error("");
475
+ });
476
+ console.error(`Generazione annullata: ${errors.length} errori bloccanti.`);
477
+ }
478
+
479
+ function main() {
480
+ const templates = walkFiles(TEMPLATE_ROOT, file => path.extname(file).toLowerCase() === ".html");
481
+ const errors = [];
482
+ const outputPaths = new Map();
483
+
484
+ templates.forEach(template => {
485
+ const output = toPosix(path.relative(DOCS_ROOT, getOutputPath(template))).toLowerCase();
486
+ if (!outputPaths.has(output)) outputPaths.set(output, []);
487
+ outputPaths.get(output).push(template);
488
+ });
489
+ outputPaths.forEach((files, output) => {
490
+ if (files.length < 2) return;
491
+ errors.push(createError(relativeToProject(files[0]), `Collisione del percorso di output "docs/${output}" tra: ${files.map(relativeToProject).join(", ")}.`, "Rinomina uno dei template per ottenere percorsi Markdown distinti."));
492
+ });
493
+
494
+ const sources = loadSources();
495
+ const allSourceContent = sources.map(source => source.content).join("\n");
496
+ const parsedTemplates = templates.map(template => {
497
+ const source = fs.readFileSync(template, "utf8");
498
+ const parsed = parseDocumentationBlock(template, source);
499
+ errors.push(...validateTemplate(template, parsed, sources, allSourceContent));
500
+ return { template, parsed };
501
+ });
502
+
503
+ if (errors.length > 0) {
504
+ printErrors(errors);
505
+ process.exitCode = 1;
506
+ return;
507
+ }
508
+
509
+ const expected = new Set();
510
+ const counts = { created: 0, updated: 0, unchanged: 0, removed: 0 };
511
+
512
+ parsedTemplates.forEach(({ template, parsed }) => {
513
+ const output = getOutputPath(template);
514
+ const content = renderMarkdown(template, parsed.data, parsed.markup);
515
+ expected.add(path.resolve(output));
516
+ fs.mkdirSync(path.dirname(output), { recursive: true });
517
+
518
+ if (!fs.existsSync(output)) {
519
+ fs.writeFileSync(output, content, "utf8");
520
+ counts.created += 1;
521
+ console.log(`[CREATO] ${relativeToProject(output)}`);
522
+ return;
523
+ }
524
+
525
+ const existing = normalizeEol(fs.readFileSync(output, "utf8"));
526
+ if (existing === content) {
527
+ counts.unchanged += 1;
528
+ console.log(`[INVARIATO] ${relativeToProject(output)}`);
529
+ return;
530
+ }
531
+
532
+ fs.writeFileSync(output, content, "utf8");
533
+ counts.updated += 1;
534
+ console.log(`[AGGIORNATO] ${relativeToProject(output)}`);
535
+ });
536
+
537
+ MANAGED_DOC_DIRECTORIES
538
+ .flatMap(directory => walkFiles(directory, file => path.extname(file).toLowerCase() === ".md"))
539
+ .forEach(file => {
540
+ if (expected.has(path.resolve(file))) return;
541
+ const firstLine = normalizeEol(fs.readFileSync(file, "utf8")).split("\n", 1)[0];
542
+ if (!GENERATED_MARKER.test(firstLine)) return;
543
+ fs.unlinkSync(file);
544
+ counts.removed += 1;
545
+ console.log(`[RIMOSSO] ${relativeToProject(file)}`);
546
+ });
547
+
548
+ console.log("");
549
+ console.log(`Riepilogo: ${templates.length} template validati; ${counts.created} creati; ${counts.updated} aggiornati; ${counts.unchanged} invariati; ${counts.removed} obsoleti rimossi.`);
550
+ }
551
+
552
+ try {
553
+ main();
554
+ } catch (error) {
555
+ console.error(`[ERRORE] ${error.message}`);
556
+ process.exitCode = 1;
557
+ }
@@ -1,23 +1,52 @@
1
- <ul pgs="grid-1 gapTexts">
2
- <li pgs="accordion" >
1
+ <!--
2
+ /**
3
+ * @title Accordion
4
+ * @description Interactive component that expands one panel at a time while synchronizing visibility, ARIA attributes, and keyboard navigation.
5
+ *
6
+ * @pgs
7
+ * - accordion: identifies each expandable element initialized by the JavaScript module.
8
+ * - accordion-button: identifies the control that opens or closes the associated panel.
9
+ * - accordion-content: identifies the content panel managed through the hidden attribute.
10
+ *
11
+ * @pgs-state
12
+ * - open: indicates the accordion element that is currently open and visible.
13
+ *
14
+ * @api
15
+ * - pgs.accordion.init(root): initializes unregistered accordions within the specified Document or Element.
16
+ * - pgs.accordion.api(element): returns the instance associated with an initialized accordion root.
17
+ * - instance.open(): opens the panel and closes any other accordions on the page.
18
+ * - instance.close(): closes the current panel.
19
+ * - instance.toggle(): toggles the panel between its open and closed states.
20
+ * - instance.refresh(): reruns initialization within the accordion container and returns the instance.
21
+ * - instance.isOpen(): returns true when the open state is active.
22
+ *
23
+ * @related
24
+ * - flexColumnTexts: applies text spacing between the list items.
25
+ *
26
+ * @return HTML list of accessible accordion items with an associated button and panel.
27
+ */
28
+ -->
29
+
30
+ <ul pgs="flexColumnTexts">
31
+ <li pgs="accordion">
3
32
  <button pgs="accordion-button" type="button">
4
33
  <i class="fa-solid fa-circle-question" aria-hidden="true"></i>
5
- <span>Domanda frequente</span>
34
+ <span>Frequently asked question</span>
6
35
  </button>
7
36
 
8
37
  <div pgs="accordion-content" hidden>
9
- <p>Risposta di esempio con contenuto testuale riutilizzabile.</p>
38
+ <p>Example answer with reusable text content.</p>
10
39
  </div>
11
40
  </li>
12
41
 
13
- <li pgs="accordion" >
42
+ <li pgs="accordion">
14
43
  <button pgs="accordion-button" type="button">
15
44
  <i class="fa-solid fa-circle-info" aria-hidden="true"></i>
16
- <span>Seconda sezione</span>
45
+ <span>Second section</span>
17
46
  </button>
18
47
 
19
48
  <div pgs="accordion-content" hidden>
20
- <p>Altro contenuto del pannello accordion.</p>
49
+ <p>Additional accordion panel content.</p>
21
50
  </div>
22
51
  </li>
23
52
  </ul>
@@ -1,3 +1,26 @@
1
+ <!--
2
+ /**
3
+ * @title Badge
4
+ * @description Collection of compact indicators for displaying categories, priorities, and visual states with consistent color variants.
5
+ *
6
+ * @pgs
7
+ * - badge: identifies the base badge variant.
8
+ * - badgePrimary: applies the primary color variant.
9
+ * - badgeStrong: applies the high-contrast variant.
10
+ * - badgeSuccess: represents a successful result.
11
+ * - badgeDot: adds a dot indicator to the badge.
12
+ * - badgeWarning: represents a warning.
13
+ * - badgeError: represents an error.
14
+ * - badgeInfo: represents informational content.
15
+ * - badgeNeutral: applies the neutral variant.
16
+ *
17
+ * @related
18
+ * - flexRow: arranges badges horizontally and allows them to wrap.
19
+ *
20
+ * @return HTML examples of the available badge variants.
21
+ */
22
+ -->
23
+
1
24
  <div pgs="flexRow">
2
25
  <span pgs="badge">
3
26
  Base
@@ -12,17 +35,17 @@
12
35
  </span>
13
36
 
14
37
  <span pgs="badgeSuccess badgeDot">
15
- Attivo
38
+ Active
16
39
  </span>
17
40
 
18
41
  <span pgs="badgeWarning">
19
42
  <i class="fa-solid fa-triangle-exclamation" aria-hidden="true"></i>
20
- Attenzione
43
+ Warning
21
44
  </span>
22
45
 
23
46
  <span pgs="badgeError">
24
47
  <i class="fa-solid fa-xmark" aria-hidden="true"></i>
25
- Errore
48
+ Error
26
49
  </span>
27
50
 
28
51
  <span pgs="badgeInfo">
@@ -31,7 +54,7 @@
31
54
  </span>
32
55
 
33
56
  <span pgs="badgeNeutral" href="#">
34
- Neutrale
57
+ Neutral
35
58
  </span>
36
59
 
37
- </div>
60
+ </div>