Commit 03229fea by PLN (Algolia)

TDAH rebuilt in React on the kit; couple gains share-all, PDF export, and compare view

parent f00c6653
...@@ -43,6 +43,7 @@ const RAW_RUNTIME_STATE = ...@@ -43,6 +43,7 @@ const RAW_RUNTIME_STATE =
["jest", "virtual:599f9545309f05166d506895056809d0f5971d8f55d13475681739816b4bb6745eb725b53ed2108e22ea4ce46b8a2c491882d38a9948455117b545da3880eea7#npm:30.2.0"],\ ["jest", "virtual:599f9545309f05166d506895056809d0f5971d8f55d13475681739816b4bb6745eb725b53ed2108e22ea4ce46b8a2c491882d38a9948455117b545da3880eea7#npm:30.2.0"],\
["jest-environment-jsdom", "virtual:599f9545309f05166d506895056809d0f5971d8f55d13475681739816b4bb6745eb725b53ed2108e22ea4ce46b8a2c491882d38a9948455117b545da3880eea7#npm:30.2.0"],\ ["jest-environment-jsdom", "virtual:599f9545309f05166d506895056809d0f5971d8f55d13475681739816b4bb6745eb725b53ed2108e22ea4ce46b8a2c491882d38a9948455117b545da3880eea7#npm:30.2.0"],\
["jest-pnp-resolver", "virtual:599f9545309f05166d506895056809d0f5971d8f55d13475681739816b4bb6745eb725b53ed2108e22ea4ce46b8a2c491882d38a9948455117b545da3880eea7#npm:1.2.3"],\ ["jest-pnp-resolver", "virtual:599f9545309f05166d506895056809d0f5971d8f55d13475681739816b4bb6745eb725b53ed2108e22ea4ce46b8a2c491882d38a9948455117b545da3880eea7#npm:1.2.3"],\
["jspdf", "npm:4.2.1"],\
["marked", "npm:15.0.12"],\ ["marked", "npm:15.0.12"],\
["next", "virtual:599f9545309f05166d506895056809d0f5971d8f55d13475681739816b4bb6745eb725b53ed2108e22ea4ce46b8a2c491882d38a9948455117b545da3880eea7#npm:15.5.6"],\ ["next", "virtual:599f9545309f05166d506895056809d0f5971d8f55d13475681739816b4bb6745eb725b53ed2108e22ea4ce46b8a2c491882d38a9948455117b545da3880eea7#npm:15.5.6"],\
["next-router-mock", "virtual:599f9545309f05166d506895056809d0f5971d8f55d13475681739816b4bb6745eb725b53ed2108e22ea4ce46b8a2c491882d38a9948455117b545da3880eea7#npm:1.0.4"],\ ["next-router-mock", "virtual:599f9545309f05166d506895056809d0f5971d8f55d13475681739816b4bb6745eb725b53ed2108e22ea4ce46b8a2c491882d38a9948455117b545da3880eea7#npm:1.0.4"],\
...@@ -960,6 +961,13 @@ const RAW_RUNTIME_STATE = ...@@ -960,6 +961,13 @@ const RAW_RUNTIME_STATE =
["@babel/runtime", "npm:7.28.4"]\ ["@babel/runtime", "npm:7.28.4"]\
],\ ],\
"linkType": "HARD"\ "linkType": "HARD"\
}],\
["npm:7.29.7", {\
"packageLocation": "../../../.yarn/berry/cache/@babel-runtime-npm-7.29.7-423e8cd4b6-10c0.zip/node_modules/@babel/runtime/",\
"packageDependencies": [\
["@babel/runtime", "npm:7.29.7"]\
],\
"linkType": "HARD"\
}]\ }]\
]],\ ]],\
["@babel/template", [\ ["@babel/template", [\
...@@ -2880,6 +2888,15 @@ const RAW_RUNTIME_STATE = ...@@ -2880,6 +2888,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["@types/pako", [\
["npm:2.0.4", {\
"packageLocation": "../../../.yarn/berry/cache/@types-pako-npm-2.0.4-ac100d59c9-10c0.zip/node_modules/@types/pako/",\
"packageDependencies": [\
["@types/pako", "npm:2.0.4"]\
],\
"linkType": "HARD"\
}]\
]],\
["@types/prop-types", [\ ["@types/prop-types", [\
["npm:15.7.15", {\ ["npm:15.7.15", {\
"packageLocation": "../../../.yarn/berry/cache/@types-prop-types-npm-15.7.15-cefe16a1fa-10c0.zip/node_modules/@types/prop-types/",\ "packageLocation": "../../../.yarn/berry/cache/@types-prop-types-npm-15.7.15-cefe16a1fa-10c0.zip/node_modules/@types/prop-types/",\
...@@ -2898,6 +2915,15 @@ const RAW_RUNTIME_STATE = ...@@ -2898,6 +2915,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["@types/raf", [\
["npm:3.4.3", {\
"packageLocation": "../../../.yarn/berry/cache/@types-raf-npm-3.4.3-afd3c6d2bc-10c0.zip/node_modules/@types/raf/",\
"packageDependencies": [\
["@types/raf", "npm:3.4.3"]\
],\
"linkType": "HARD"\
}]\
]],\
["@types/react", [\ ["@types/react", [\
["npm:18.3.27", {\ ["npm:18.3.27", {\
"packageLocation": "../../../.yarn/berry/cache/@types-react-npm-18.3.27-f47dadcbab-10c0.zip/node_modules/@types/react/",\ "packageLocation": "../../../.yarn/berry/cache/@types-react-npm-18.3.27-f47dadcbab-10c0.zip/node_modules/@types/react/",\
...@@ -2936,6 +2962,15 @@ const RAW_RUNTIME_STATE = ...@@ -2936,6 +2962,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["@types/trusted-types", [\
["npm:2.0.7", {\
"packageLocation": "../../../.yarn/berry/cache/@types-trusted-types-npm-2.0.7-a07fc44f59-10c0.zip/node_modules/@types/trusted-types/",\
"packageDependencies": [\
["@types/trusted-types", "npm:2.0.7"]\
],\
"linkType": "HARD"\
}]\
]],\
["@types/unist", [\ ["@types/unist", [\
["npm:2.0.11", {\ ["npm:2.0.11", {\
"packageLocation": "../../../.yarn/berry/cache/@types-unist-npm-2.0.11-44eea90bde-10c0.zip/node_modules/@types/unist/",\ "packageLocation": "../../../.yarn/berry/cache/@types-unist-npm-2.0.11-44eea90bde-10c0.zip/node_modules/@types/unist/",\
...@@ -3874,6 +3909,15 @@ const RAW_RUNTIME_STATE = ...@@ -3874,6 +3909,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["base64-arraybuffer", [\
["npm:1.0.2", {\
"packageLocation": "../../../.yarn/berry/cache/base64-arraybuffer-npm-1.0.2-18f4899892-10c0.zip/node_modules/base64-arraybuffer/",\
"packageDependencies": [\
["base64-arraybuffer", "npm:1.0.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["baseline-browser-mapping", [\ ["baseline-browser-mapping", [\
["npm:2.8.32", {\ ["npm:2.8.32", {\
"packageLocation": "../../../.yarn/berry/cache/baseline-browser-mapping-npm-2.8.32-733775c0a7-10c0.zip/node_modules/baseline-browser-mapping/",\ "packageLocation": "../../../.yarn/berry/cache/baseline-browser-mapping-npm-2.8.32-733775c0a7-10c0.zip/node_modules/baseline-browser-mapping/",\
...@@ -4064,6 +4108,23 @@ const RAW_RUNTIME_STATE = ...@@ -4064,6 +4108,23 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["canvg", [\
["npm:3.0.11", {\
"packageLocation": "../../../.yarn/berry/cache/canvg-npm-3.0.11-0944b07da2-10c0.zip/node_modules/canvg/",\
"packageDependencies": [\
["canvg", "npm:3.0.11"],\
["@babel/runtime", "npm:7.28.4"],\
["@types/raf", "npm:3.4.3"],\
["core-js", "npm:3.50.0"],\
["raf", "npm:3.4.1"],\
["regenerator-runtime", "npm:0.13.11"],\
["rgbcolor", "npm:1.0.1"],\
["stackblur-canvas", "npm:2.7.0"],\
["svg-pathdata", "npm:6.0.3"]\
],\
"linkType": "HARD"\
}]\
]],\
["ccount", [\ ["ccount", [\
["npm:2.0.1", {\ ["npm:2.0.1", {\
"packageLocation": "../../../.yarn/berry/cache/ccount-npm-2.0.1-f4b7827860-10c0.zip/node_modules/ccount/",\ "packageLocation": "../../../.yarn/berry/cache/ccount-npm-2.0.1-f4b7827860-10c0.zip/node_modules/ccount/",\
...@@ -4338,6 +4399,15 @@ const RAW_RUNTIME_STATE = ...@@ -4338,6 +4399,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["core-js", [\
["npm:3.50.0", {\
"packageLocation": "./.yarn/unplugged/core-js-npm-3.50.0-c518f629d4/node_modules/core-js/",\
"packageDependencies": [\
["core-js", "npm:3.50.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["core-util-is", [\ ["core-util-is", [\
["npm:1.0.3", {\ ["npm:1.0.3", {\
"packageLocation": "../../../.yarn/berry/cache/core-util-is-npm-1.0.3-ca74b76c90-10c0.zip/node_modules/core-util-is/",\ "packageLocation": "../../../.yarn/berry/cache/core-util-is-npm-1.0.3-ca74b76c90-10c0.zip/node_modules/core-util-is/",\
...@@ -4368,6 +4438,16 @@ const RAW_RUNTIME_STATE = ...@@ -4368,6 +4438,16 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["css-line-break", [\
["npm:2.1.0", {\
"packageLocation": "../../../.yarn/berry/cache/css-line-break-npm-2.1.0-a1fd810d69-10c0.zip/node_modules/css-line-break/",\
"packageDependencies": [\
["css-line-break", "npm:2.1.0"],\
["utrie", "npm:1.0.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["css.escape", [\ ["css.escape", [\
["npm:1.5.1", {\ ["npm:1.5.1", {\
"packageLocation": "../../../.yarn/berry/cache/css.escape-npm-1.5.1-b24d2ba77a-10c0.zip/node_modules/css.escape/",\ "packageLocation": "../../../.yarn/berry/cache/css.escape-npm-1.5.1-b24d2ba77a-10c0.zip/node_modules/css.escape/",\
...@@ -4612,6 +4692,16 @@ const RAW_RUNTIME_STATE = ...@@ -4612,6 +4692,16 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["dompurify", [\
["npm:3.4.15", {\
"packageLocation": "../../../.yarn/berry/cache/dompurify-npm-3.4.15-1d152b5903-10c0.zip/node_modules/dompurify/",\
"packageDependencies": [\
["dompurify", "npm:3.4.15"],\
["@types/trusted-types", "npm:2.0.7"]\
],\
"linkType": "HARD"\
}]\
]],\
["eastasianwidth", [\ ["eastasianwidth", [\
["npm:0.2.0", {\ ["npm:0.2.0", {\
"packageLocation": "../../../.yarn/berry/cache/eastasianwidth-npm-0.2.0-c37eb16bd1-10c0.zip/node_modules/eastasianwidth/",\ "packageLocation": "../../../.yarn/berry/cache/eastasianwidth-npm-0.2.0-c37eb16bd1-10c0.zip/node_modules/eastasianwidth/",\
...@@ -5147,6 +5237,18 @@ const RAW_RUNTIME_STATE = ...@@ -5147,6 +5237,18 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["fast-png", [\
["npm:6.4.0", {\
"packageLocation": "../../../.yarn/berry/cache/fast-png-npm-6.4.0-7871b2b5a5-10c0.zip/node_modules/fast-png/",\
"packageDependencies": [\
["fast-png", "npm:6.4.0"],\
["@types/pako", "npm:2.0.4"],\
["iobuffer", "npm:5.4.0"],\
["pako", "npm:2.2.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["fastq", [\ ["fastq", [\
["npm:1.19.1", {\ ["npm:1.19.1", {\
"packageLocation": "../../../.yarn/berry/cache/fastq-npm-1.19.1-ca0a13ec3f-10c0.zip/node_modules/fastq/",\ "packageLocation": "../../../.yarn/berry/cache/fastq-npm-1.19.1-ca0a13ec3f-10c0.zip/node_modules/fastq/",\
...@@ -5209,6 +5311,15 @@ const RAW_RUNTIME_STATE = ...@@ -5209,6 +5311,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["fflate", [\
["npm:0.8.3", {\
"packageLocation": "../../../.yarn/berry/cache/fflate-npm-0.8.3-35acaff861-10c0.zip/node_modules/fflate/",\
"packageDependencies": [\
["fflate", "npm:0.8.3"]\
],\
"linkType": "HARD"\
}]\
]],\
["fftjs", [\ ["fftjs", [\
["npm:0.0.4", {\ ["npm:0.0.4", {\
"packageLocation": "../../../.yarn/berry/cache/fftjs-npm-0.0.4-8ae958e6a0-10c0.zip/node_modules/fftjs/",\ "packageLocation": "../../../.yarn/berry/cache/fftjs-npm-0.0.4-8ae958e6a0-10c0.zip/node_modules/fftjs/",\
...@@ -5730,6 +5841,17 @@ const RAW_RUNTIME_STATE = ...@@ -5730,6 +5841,17 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["html2canvas", [\
["npm:1.4.1", {\
"packageLocation": "../../../.yarn/berry/cache/html2canvas-npm-1.4.1-fa13983145-10c0.zip/node_modules/html2canvas/",\
"packageDependencies": [\
["html2canvas", "npm:1.4.1"],\
["css-line-break", "npm:2.1.0"],\
["text-segmentation", "npm:1.0.3"]\
],\
"linkType": "HARD"\
}]\
]],\
["http-cache-semantics", [\ ["http-cache-semantics", [\
["npm:4.2.0", {\ ["npm:4.2.0", {\
"packageLocation": "../../../.yarn/berry/cache/http-cache-semantics-npm-4.2.0-fadacfb3ad-10c0.zip/node_modules/http-cache-semantics/",\ "packageLocation": "../../../.yarn/berry/cache/http-cache-semantics-npm-4.2.0-fadacfb3ad-10c0.zip/node_modules/http-cache-semantics/",\
...@@ -5955,6 +6077,15 @@ const RAW_RUNTIME_STATE = ...@@ -5955,6 +6077,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["iobuffer", [\
["npm:5.4.0", {\
"packageLocation": "../../../.yarn/berry/cache/iobuffer-npm-5.4.0-2cf4f91275-10c0.zip/node_modules/iobuffer/",\
"packageDependencies": [\
["iobuffer", "npm:5.4.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["ip-address", [\ ["ip-address", [\
["npm:10.1.0", {\ ["npm:10.1.0", {\
"packageLocation": "../../../.yarn/berry/cache/ip-address-npm-10.1.0-d5d5693401-10c0.zip/node_modules/ip-address/",\ "packageLocation": "../../../.yarn/berry/cache/ip-address-npm-10.1.0-d5d5693401-10c0.zip/node_modules/ip-address/",\
...@@ -6962,6 +7093,22 @@ const RAW_RUNTIME_STATE = ...@@ -6962,6 +7093,22 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["jspdf", [\
["npm:4.2.1", {\
"packageLocation": "../../../.yarn/berry/cache/jspdf-npm-4.2.1-4a36558b77-10c0.zip/node_modules/jspdf/",\
"packageDependencies": [\
["jspdf", "npm:4.2.1"],\
["@babel/runtime", "npm:7.29.7"],\
["canvg", "npm:3.0.11"],\
["core-js", "npm:3.50.0"],\
["dompurify", "npm:3.4.15"],\
["fast-png", "npm:6.4.0"],\
["fflate", "npm:0.8.3"],\
["html2canvas", "npm:1.4.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["kind-of", [\ ["kind-of", [\
["npm:6.0.3", {\ ["npm:6.0.3", {\
"packageLocation": "../../../.yarn/berry/cache/kind-of-npm-6.0.3-ab15f36220-10c0.zip/node_modules/kind-of/",\ "packageLocation": "../../../.yarn/berry/cache/kind-of-npm-6.0.3-ab15f36220-10c0.zip/node_modules/kind-of/",\
...@@ -8624,6 +8771,15 @@ const RAW_RUNTIME_STATE = ...@@ -8624,6 +8771,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["pako", [\
["npm:2.2.0", {\
"packageLocation": "../../../.yarn/berry/cache/pako-npm-2.2.0-9f51285a27-10c0.zip/node_modules/pako/",\
"packageDependencies": [\
["pako", "npm:2.2.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["parse-entities", [\ ["parse-entities", [\
["npm:2.0.0", {\ ["npm:2.0.0", {\
"packageLocation": "../../../.yarn/berry/cache/parse-entities-npm-2.0.0-b7b4f46ff6-10c0.zip/node_modules/parse-entities/",\ "packageLocation": "../../../.yarn/berry/cache/parse-entities-npm-2.0.0-b7b4f46ff6-10c0.zip/node_modules/parse-entities/",\
...@@ -8912,6 +9068,7 @@ const RAW_RUNTIME_STATE = ...@@ -8912,6 +9068,7 @@ const RAW_RUNTIME_STATE =
["jest", "virtual:599f9545309f05166d506895056809d0f5971d8f55d13475681739816b4bb6745eb725b53ed2108e22ea4ce46b8a2c491882d38a9948455117b545da3880eea7#npm:30.2.0"],\ ["jest", "virtual:599f9545309f05166d506895056809d0f5971d8f55d13475681739816b4bb6745eb725b53ed2108e22ea4ce46b8a2c491882d38a9948455117b545da3880eea7#npm:30.2.0"],\
["jest-environment-jsdom", "virtual:599f9545309f05166d506895056809d0f5971d8f55d13475681739816b4bb6745eb725b53ed2108e22ea4ce46b8a2c491882d38a9948455117b545da3880eea7#npm:30.2.0"],\ ["jest-environment-jsdom", "virtual:599f9545309f05166d506895056809d0f5971d8f55d13475681739816b4bb6745eb725b53ed2108e22ea4ce46b8a2c491882d38a9948455117b545da3880eea7#npm:30.2.0"],\
["jest-pnp-resolver", "virtual:599f9545309f05166d506895056809d0f5971d8f55d13475681739816b4bb6745eb725b53ed2108e22ea4ce46b8a2c491882d38a9948455117b545da3880eea7#npm:1.2.3"],\ ["jest-pnp-resolver", "virtual:599f9545309f05166d506895056809d0f5971d8f55d13475681739816b4bb6745eb725b53ed2108e22ea4ce46b8a2c491882d38a9948455117b545da3880eea7#npm:1.2.3"],\
["jspdf", "npm:4.2.1"],\
["marked", "npm:15.0.12"],\ ["marked", "npm:15.0.12"],\
["next", "virtual:599f9545309f05166d506895056809d0f5971d8f55d13475681739816b4bb6745eb725b53ed2108e22ea4ce46b8a2c491882d38a9948455117b545da3880eea7#npm:15.5.6"],\ ["next", "virtual:599f9545309f05166d506895056809d0f5971d8f55d13475681739816b4bb6745eb725b53ed2108e22ea4ce46b8a2c491882d38a9948455117b545da3880eea7#npm:15.5.6"],\
["next-router-mock", "virtual:599f9545309f05166d506895056809d0f5971d8f55d13475681739816b4bb6745eb725b53ed2108e22ea4ce46b8a2c491882d38a9948455117b545da3880eea7#npm:1.0.4"],\ ["next-router-mock", "virtual:599f9545309f05166d506895056809d0f5971d8f55d13475681739816b4bb6745eb725b53ed2108e22ea4ce46b8a2c491882d38a9948455117b545da3880eea7#npm:1.0.4"],\
...@@ -9528,6 +9685,15 @@ const RAW_RUNTIME_STATE = ...@@ -9528,6 +9685,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["regenerator-runtime", [\
["npm:0.13.11", {\
"packageLocation": "../../../.yarn/berry/cache/regenerator-runtime-npm-0.13.11-90bf536060-10c0.zip/node_modules/regenerator-runtime/",\
"packageDependencies": [\
["regenerator-runtime", "npm:0.13.11"]\
],\
"linkType": "HARD"\
}]\
]],\
["regex", [\ ["regex", [\
["npm:6.1.0", {\ ["npm:6.1.0", {\
"packageLocation": "../../../.yarn/berry/cache/regex-npm-6.1.0-3b34208a3c-10c0.zip/node_modules/regex/",\ "packageLocation": "../../../.yarn/berry/cache/regex-npm-6.1.0-3b34208a3c-10c0.zip/node_modules/regex/",\
...@@ -9752,6 +9918,15 @@ const RAW_RUNTIME_STATE = ...@@ -9752,6 +9918,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["rgbcolor", [\
["npm:1.0.1", {\
"packageLocation": "../../../.yarn/berry/cache/rgbcolor-npm-1.0.1-5d8cee2258-10c0.zip/node_modules/rgbcolor/",\
"packageDependencies": [\
["rgbcolor", "npm:1.0.1"]\
],\
"linkType": "HARD"\
}]\
]],\
["right-now", [\ ["right-now", [\
["npm:1.0.0", {\ ["npm:1.0.0", {\
"packageLocation": "../../../.yarn/berry/cache/right-now-npm-1.0.0-cd931eff88-10c0.zip/node_modules/right-now/",\ "packageLocation": "../../../.yarn/berry/cache/right-now-npm-1.0.0-cd931eff88-10c0.zip/node_modules/right-now/",\
...@@ -10088,6 +10263,15 @@ const RAW_RUNTIME_STATE = ...@@ -10088,6 +10263,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["stackblur-canvas", [\
["npm:2.7.0", {\
"packageLocation": "../../../.yarn/berry/cache/stackblur-canvas-npm-2.7.0-69df5c1ce4-10c0.zip/node_modules/stackblur-canvas/",\
"packageDependencies": [\
["stackblur-canvas", "npm:2.7.0"]\
],\
"linkType": "HARD"\
}]\
]],\
["stat-mode", [\ ["stat-mode", [\
["npm:0.3.0", {\ ["npm:0.3.0", {\
"packageLocation": "../../../.yarn/berry/cache/stat-mode-npm-0.3.0-f642fd493d-10c0.zip/node_modules/stat-mode/",\ "packageLocation": "../../../.yarn/berry/cache/stat-mode-npm-0.3.0-f642fd493d-10c0.zip/node_modules/stat-mode/",\
...@@ -10333,6 +10517,15 @@ const RAW_RUNTIME_STATE = ...@@ -10333,6 +10517,15 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["svg-pathdata", [\
["npm:6.0.3", {\
"packageLocation": "../../../.yarn/berry/cache/svg-pathdata-npm-6.0.3-b9a9612535-10c0.zip/node_modules/svg-pathdata/",\
"packageDependencies": [\
["svg-pathdata", "npm:6.0.3"]\
],\
"linkType": "HARD"\
}]\
]],\
["swiper", [\ ["swiper", [\
["npm:11.2.10", {\ ["npm:11.2.10", {\
"packageLocation": "../../../.yarn/berry/cache/swiper-npm-11.2.10-2bf88a66cf-10c0.zip/node_modules/swiper/",\ "packageLocation": "../../../.yarn/berry/cache/swiper-npm-11.2.10-2bf88a66cf-10c0.zip/node_modules/swiper/",\
...@@ -10431,6 +10624,16 @@ const RAW_RUNTIME_STATE = ...@@ -10431,6 +10624,16 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["text-segmentation", [\
["npm:1.0.3", {\
"packageLocation": "../../../.yarn/berry/cache/text-segmentation-npm-1.0.3-634201c349-10c0.zip/node_modules/text-segmentation/",\
"packageDependencies": [\
["text-segmentation", "npm:1.0.3"],\
["utrie", "npm:1.0.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["time-span", [\ ["time-span", [\
["npm:4.0.0", {\ ["npm:4.0.0", {\
"packageLocation": "../../../.yarn/berry/cache/time-span-npm-4.0.0-10dd9f31f8-10c0.zip/node_modules/time-span/",\ "packageLocation": "../../../.yarn/berry/cache/time-span-npm-4.0.0-10dd9f31f8-10c0.zip/node_modules/time-span/",\
...@@ -10898,6 +11101,16 @@ const RAW_RUNTIME_STATE = ...@@ -10898,6 +11101,16 @@ const RAW_RUNTIME_STATE =
"linkType": "HARD"\ "linkType": "HARD"\
}]\ }]\
]],\ ]],\
["utrie", [\
["npm:1.0.2", {\
"packageLocation": "../../../.yarn/berry/cache/utrie-npm-1.0.2-909cbd985c-10c0.zip/node_modules/utrie/",\
"packageDependencies": [\
["utrie", "npm:1.0.2"],\
["base64-arraybuffer", "npm:1.0.2"]\
],\
"linkType": "HARD"\
}]\
]],\
["uuid", [\ ["uuid", [\
["npm:3.3.2", {\ ["npm:3.3.2", {\
"packageLocation": "../../../.yarn/berry/cache/uuid-npm-3.3.2-62715051ac-10c0.zip/node_modules/uuid/",\ "packageLocation": "../../../.yarn/berry/cache/uuid-npm-3.3.2-62715051ac-10c0.zip/node_modules/uuid/",\
......
No preview for this file type
import ClarteTdah from '@/components/tdah/ClarteTdah';
export const metadata = {
title: 'Clarté: Adult ADHD Self-Assessment',
description: 'A structured self-assessment of attention-deficit/hyperactivity disorder (ADHD) in adults, built on validated tools: the ASRS v1.1 (WHO), the DSM-5 criteria, and executive functions. Free, private, with a PDF report at the end.',
};
export default function Page() {
return <ClarteTdah lang="en" />;
}
import ClarteTdah from '@/components/tdah/ClarteTdah';
export const metadata = {
title: 'Clarté : Auto-évaluation du TDAH chez l’adulte',
description: 'Auto-évaluation du TDAH chez l’adulte, avec des outils validés : ASRS v1.1 (OMS), critères DSM-5, fonctions exécutives. Gratuit, confidentiel, rapport PDF à la fin.',
};
export default function Page() {
return <ClarteTdah lang="fr" />;
}
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { COUPLE_DATA } from './data'; import { COUPLE_DATA } from './data';
import { decodePayload, storageAvailable } from './engine'; import { decodeAnyPayload, readRecord as readRecordLocal, storageAvailable } from './engine';
import { strings } from './strings'; import { strings } from './strings';
import styles from './couple.module.css'; import styles from './couple.module.css';
import HomeView from './HomeView'; import HomeView from './HomeView';
...@@ -17,6 +17,8 @@ import QuizView from './QuizView'; ...@@ -17,6 +17,8 @@ import QuizView from './QuizView';
import WorksheetView from './WorksheetView'; import WorksheetView from './WorksheetView';
import RecapView from './RecapView'; import RecapView from './RecapView';
import SharedView, { SharedErrorView } from './SharedView'; import SharedView, { SharedErrorView } from './SharedView';
import SharedAllView from './SharedAllView';
import CompareView from './CompareView';
function questionnaireById(qid) { function questionnaireById(qid) {
var q = null; var q = null;
...@@ -36,9 +38,11 @@ export default function ClarteCouple() { ...@@ -36,9 +38,11 @@ export default function ClarteCouple() {
setMounted(true); setMounted(true);
setStorageOk(storageAvailable()); setStorageOk(storageAvailable());
var hash = typeof location !== 'undefined' ? location.hash : ''; var hash = typeof location !== 'undefined' ? location.hash : '';
if (hash.lastIndexOf('#p=', 0) === 0) { if (hash.lastIndexOf('#p=', 0) === 0 || hash.lastIndexOf('#a=', 0) === 0) {
var payload = decodePayload(hash.slice(3)); var decoded = decodeAnyPayload(hash.slice(3));
setRoute(payload ? { view: 'shared', payload: payload } : { view: 'sharedError' }); if (!decoded) setRoute({ view: 'sharedError' });
else if (decoded.kind === 'all') setRoute({ view: 'sharedAll', payload: decoded.payload });
else setRoute({ view: 'shared', payload: decoded.payload });
} else { } else {
setRoute({ view: 'home' }); setRoute({ view: 'home' });
} }
...@@ -49,6 +53,24 @@ export default function ClarteCouple() { ...@@ -49,6 +53,24 @@ export default function ClarteCouple() {
}, []); }, []);
const goHome = useCallback(() => setRoute({ view: 'home' }), []); const goHome = useCallback(() => setRoute({ view: 'home' }), []);
const goCompare = useCallback((hash) => {
var decoded = decodeAnyPayload(hash);
if (!decoded) { setRoute({ view: 'home', compareError: true }); return; }
if (decoded.kind !== 'one') { setRoute({ view: 'home', compareError: true }); return; }
// leur payload = un questionnaire ; on le compare au même qid localement
var qid = decoded.payload.qid;
var mine = readRecordLocal(qid);
if (!mine || Object.keys(mine.answers || {}).length === 0) {
// rien à comparer côté local : on affiche quand même leur lecture seule
setRoute({ view: 'shared', payload: decoded.payload });
return;
}
setRoute({
view: 'compare', qid: qid,
myRecord: mine,
theirBlock: decoded.payload,
});
}, []);
const goIntro = useCallback((qid) => setRoute({ view: 'intro', qid: qid }), []); const goIntro = useCallback((qid) => setRoute({ view: 'intro', qid: qid }), []);
const goRecap = useCallback((qid) => setRoute({ view: 'recap', qid: qid }), []); const goRecap = useCallback((qid) => setRoute({ view: 'recap', qid: qid }), []);
const goQuiz = useCallback((qid, pos) => { const goQuiz = useCallback((qid, pos) => {
...@@ -81,7 +103,16 @@ export default function ClarteCouple() { ...@@ -81,7 +103,16 @@ export default function ClarteCouple() {
); );
if (route.view === 'shared') return withBanner(<SharedView payload={route.payload} />); if (route.view === 'shared') return withBanner(<SharedView payload={route.payload} />);
if (route.view === 'sharedAll') return withBanner(<SharedAllView payload={route.payload} />);
if (route.view === 'sharedError') return withBanner(<SharedErrorView />); if (route.view === 'sharedError') return withBanner(<SharedErrorView />);
if (route.view === 'compare') return withBanner(
<CompareView
myQid={route.qid}
myRecord={route.myRecord}
theirBlock={route.theirBlock}
onHome={goHome}
/>
);
const q = route.qid ? questionnaireById(route.qid) : null; const q = route.qid ? questionnaireById(route.qid) : null;
...@@ -127,6 +158,7 @@ export default function ClarteCouple() { ...@@ -127,6 +158,7 @@ export default function ClarteCouple() {
goIntro={goIntro} goIntro={goIntro}
goQuiz={goQuiz} goQuiz={goQuiz}
goRecap={goRecap} goRecap={goRecap}
goCompare={goCompare}
trackWrite={trackWrite} trackWrite={trackWrite}
/> />
); );
......
'use client';
/* Clarté couple — comparaison à deux. On colle le lien de l'autre : les
réponses se mettent côte à côte, item par item, divergences visibles.
Aucun score de compatibilité (context.md §5 : divergence par item, jamais
un chiffre de couple). Le conseil de l'instrument (debrief verbatim) est
affiché au-dessus du tableau quand il existe ; la règle écrite du
bonheur_conjugal (« discuter les 3 et 4, noter les 0-2 sans débattre »)
colore les lignes en conséquence. Les réponses de l'autre ne s'écrivent
jamais sur cet appareil. */
import { useMemo, useState } from 'react';
import { COUPLE_DATA } from './data';
import { compareAnswers, discussionAdvice, displayTitle } from './engine';
import { strings } from './strings';
import styles from './couple.module.css';
export default function CompareView({ myQid, myRecord, theirBlock, onHome }) {
var q = null;
COUPLE_DATA.questionnaires.forEach(function (x) { if (x.id === myQid) q = x; });
if (!q) return null;
var advice = discussionAdvice(myQid);
var rows = useMemo(
function () { return compareAnswers(myQid, myRecord, theirBlock, q); },
[myQid, myRecord, theirBlock]
);
var theirSc = theirBlock && typeof theirBlock.pct === 'number' ? theirBlock.pct : null;
var mySc = myRecord ? computePct(q, myRecord) : null;
return (
<div className={styles.root}>
<h1 className={styles.title}>{strings.compare.title(displayTitle(q.title))}</h1>
<p className={styles.muted}>
{strings.compare.legende(
myRecord ? (myRecord.respondent || strings.compare.moi) : strings.compare.moi,
theirBlock.respondent || strings.compare.autre
)}
</p>
{advice && (
<div className={styles.adviceBand}>
<p>{strings.compare.adviceTitle}</p>
<p className={styles.muted}>
{strings.compare.adviceRule(advice.discussFrom, advice.noteBelow)}
</p>
</div>
)}
<table className={styles.compareTable}>
<thead>
<tr>
<th>{strings.compare.colItem}</th>
<th>{strings.compare.colMine}</th>
<th>{strings.compare.colTheirs}</th>
</tr>
</thead>
<tbody>
{rows.map(function (r) {
var cls = '';
if (advice && r.mine != null && r.theirs != null) {
var mineN = parseInt(r.mine, 10);
var theirsN = parseInt(r.theirs, 10);
if (!isNaN(mineN) && !isNaN(theirsN)) {
if (mineN >= advice.discussFrom || theirsN >= advice.discussFrom) cls = styles.rowDiscuss;
else if (mineN < advice.noteBelow || theirsN < advice.noteBelow) cls = styles.rowNote;
}
}
if (r.divergence && !cls) cls = styles.rowDivergent;
return (
<tr key={r.key} className={cls}>
<td>{r.text}</td>
<td>{r.mine || '—'}</td>
<td>{r.theirs || '—'}</td>
</tr>
);
})}
</tbody>
</table>
{(mySc != null || theirSc != null) && (
<p className={styles.muted}>
{strings.compare.pcts(mySc == null ? null : mySc + ' %', theirSc == null ? null : theirSc + ' %')}
</p>
)}
<p className={styles.muted}>{strings.compare.noScore}</p>
<button type="button" className={styles.btn} onClick={onHome}>{strings.recap.backHome}</button>
</div>
);
}
function computePct(q, st) {
var sum = 0, maxSum = 0;
q.sections.forEach(function (s) {
(s.items || []).forEach(function (it) {
var v = st.answers[String(it.index)];
if (typeof v === 'number') { sum += v; maxSum += 4; }
});
});
return maxSum > 0 ? Math.round((sum / maxSum) * 100) : null;
}
...@@ -18,8 +18,11 @@ import { ...@@ -18,8 +18,11 @@ import {
firstUnanswered, firstUnanswered,
readRecord, readRecord,
readSession, readSession,
serializeAllAnswers,
shareUrl,
writeSession, writeSession,
} from './engine'; } from './engine';
import { exportCouplePdf } from './pdf-export';
import { strings } from './strings'; import { strings } from './strings';
import styles from './couple.module.css'; import styles from './couple.module.css';
...@@ -32,9 +35,13 @@ function cardClick(q, goIntro, goQuiz, goRecap) { ...@@ -32,9 +35,13 @@ function cardClick(q, goIntro, goQuiz, goRecap) {
else goQuiz(q.id, firstUnanswered(q, st)); else goQuiz(q.id, firstUnanswered(q, st));
} }
export default function HomeView({ goIntro, goQuiz, goRecap, trackWrite }) { export default function HomeView({ goIntro, goQuiz, goRecap, trackWrite, goCompare }) {
const [session, setSession] = useState(null); const [session, setSession] = useState(null);
const [confirmingDelete, setConfirmingDelete] = useState(false); const [confirmingDelete, setConfirmingDelete] = useState(false);
const [shareAllOpen, setShareAllOpen] = useState(false);
const [compareLink, setCompareLink] = useState('');
const [compareError, setCompareError] = useState(false);
const [pdfDone, setPdfDone] = useState(false);
useEffect(() => { useEffect(() => {
setSession(readSession()); setSession(readSession());
...@@ -107,6 +114,41 @@ export default function HomeView({ goIntro, goQuiz, goRecap, trackWrite }) { ...@@ -107,6 +114,41 @@ export default function HomeView({ goIntro, goQuiz, goRecap, trackWrite }) {
<p className={styles.muted}>{strings.home.disclosure}</p> <p className={styles.muted}>{strings.home.disclosure}</p>
<p className={styles.muted}>{strings.home.closing}</p> <p className={styles.muted}>{strings.home.closing}</p>
<div className={styles.together}>
<h2>{strings.home.togetherTitle}</h2>
<p className={styles.muted}>{strings.home.togetherIntro}</p>
<div className={styles.footlinks}>
<button type="button" className={styles.linkbtn} onClick={function () { setShareAllOpen(!shareAllOpen); }}>
{strings.home.shareAllBtn}
</button>
{' · '}
<button type="button" className={styles.linkbtn} onClick={async function () {
var ok = await exportCouplePdf();
if (ok) { setPdfDone(true); setTimeout(function () { setPdfDone(false); }, 2500); }
}}>
{pdfDone ? strings.home.pdfDone : strings.home.pdfBtn}
</button>
</div>
{shareAllOpen && <ShareAllPanel />}
<div className={styles.compareRow}>
<label htmlFor="compare-link">{strings.compare.pasteLabel}</label>
<input
id="compare-link"
value={compareLink}
placeholder={strings.compare.pasteHint}
onChange={function (e) { setCompareLink(e.target.value); setCompareError(false); }}
/>
<button type="button" className={styles.btn} onClick={function () {
var raw = compareLink.trim();
var hash = raw.indexOf('#p=') !== -1 ? raw.slice(raw.indexOf('#p=') + 3)
: raw.indexOf('#a=') !== -1 ? raw.slice(raw.indexOf('#a=') + 3) : '';
if (!hash) { setCompareError(true); return; }
goCompare(hash);
}}>{strings.compare.pasteBtn}</button>
</div>
{compareError && <p className={styles.muted}>{strings.compare.pasteError}</p>}
</div>
<footer className={styles.pagefoot}> <footer className={styles.pagefoot}>
<p>{strings.home.footLine}</p> <p>{strings.home.footLine}</p>
<p className={styles.footlinks}> <p className={styles.footlinks}>
...@@ -142,3 +184,45 @@ export default function HomeView({ goIntro, goQuiz, goRecap, trackWrite }) { ...@@ -142,3 +184,45 @@ export default function HomeView({ goIntro, goQuiz, goRecap, trackWrite }) {
</div> </div>
); );
} }
/* Lien global : toutes les réponses, encodées dans le fragment (#a=),
même avertissement que le lien mono-questionnaire. */
function ShareAllPanel() {
const [copied, setCopied] = useState(false);
var payload = serializeAllAnswers(readSession().respondent_label);
var url = shareUrl(payload, 'a');
function flashCopied() {
setCopied(true);
setTimeout(function () { setCopied(false); }, 2000);
}
function copy() {
var ok = false;
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(url).then(flashCopied, function () {});
ok = true;
}
} catch (e) {}
if (!ok) {
try {
var inp = document.getElementById('share-all-input');
if (inp) { inp.select(); if (document.execCommand('copy')) flashCopied(); }
} catch (e2) {}
}
}
return (
<div className={styles.sharePanel}>
<p>{strings.recap.share.warning}</p>
<div className={styles.shareRow}>
<input id="share-all-input" readOnly value={url} aria-label={strings.recap.share.inputAria}
onFocus={function (e) { e.target.select(); }} />
<button type="button" className={styles.btn} onClick={copy}>
{copied ? strings.recap.share.copied : strings.recap.share.copy}
</button>
</div>
<p className={styles.muted}>{strings.home.shareAllNote}</p>
</div>
);
}
'use client';
/* Clarté couple — vue partagée globale (#a=) : les réponses de TOUT le
parcours sur une page, pour une lecture de thérapeute sans répétition de
huit liens. Lecture seule : rien n'est écrit sur l'appareil du destinataire. */
import { useMemo } from 'react';
import { COUPLE_DATA } from './data';
import { displayTitle } from './engine';
import { strings } from './strings';
import styles from './couple.module.css';
export default function SharedAllView({ payload }) {
const blocks = useMemo(() => {
return (payload.questionnaires || []).map(function (b) {
var q = null;
COUPLE_DATA.questionnaires.forEach(function (x) { if (x.id === b.qid) q = x; });
return { block: b, q: q };
});
}, [payload]);
const totalAnswered = (payload.questionnaires || []).reduce(function (a, b) {
return a + (b.answered || []).length;
}, 0);
const created = payload.created ? new Date(payload.created) : null;
return (
<div className={styles.root}>
<h1 className={styles.title}>{strings.sharedAll.title}</h1>
<p className={styles.muted}>
{strings.sharedAll.by(payload.respondent || strings.home.whoLabel)}
{created ? ' · ' + created.toLocaleDateString('fr-FR') : ''}
{' · ' + strings.sharedAll.totalLine(totalAnswered)}
</p>
<p className={styles.muted}>{strings.shared.readOnly}</p>
{blocks.map(function ({ block, q }) {
var items = q ? q.sections.reduce(function (a, s) { return a + (s.items || []).length; }, 0) : 0;
return (
<section key={block.qid} className={styles.sharedAllBlock}>
<h2>{displayTitle(block.title)}</h2>
{(block.answered || []).length === 0 ? (
<p className={styles.muted}>{strings.sharedAll.empty}</p>
) : (
<>
{block.pct != null && (
<p className={styles.sharedPct}>
{strings.sharedAll.scoreLine(block.pct)}
</p>
)}
<table className={styles.recapTable}>
<tbody>
{(block.answered || []).map(function (e, i) {
return (
<tr key={e.i}>
<td>{e.i}</td>
<td className={e.v && typeof e.v === 'object' ? styles.recapFree : null}>
{e.l || (e.v && typeof e.v === 'object' ? e.v.text : String(e.v))}
</td>
</tr>
);
})}
</tbody>
</table>
{(block.answered || []).length < items && (
<p className={styles.muted}>
{strings.sharedAll.partial(block.answered.length, items)}
</p>
)}
</>
)}
</section>
);
})}
<p className={styles.muted}>{strings.shared.foot}</p>
</div>
);
}
...@@ -412,3 +412,22 @@ ...@@ -412,3 +412,22 @@
.muted + .muted { margin-top: var(--sp-3, 1rem); } .muted + .muted { margin-top: var(--sp-3, 1rem); }
.navGroup { display: flex; gap: .6rem; } .navGroup { display: flex; gap: .6rem; }
/* À deux : partage global, PDF, comparaison. */
.together { margin-top: var(--sp-5); padding-top: var(--sp-3); border-top: 1px solid var(--line); }
.together h2 { font-size: var(--fs-body); margin: 0 0 var(--sp-1); }
.compareRow { display: flex; flex-wrap: wrap; gap: var(--sp-2); align-items: baseline; margin-top: var(--sp-3); }
.compareRow label { font-size: var(--fs-small); }
.compareRow input { flex: 1 1 16rem; font: inherit; font-size: var(--fs-small); color: var(--ink); background: #fff; border: 1px solid var(--line-strong); border-radius: 8px; padding: .45rem .6rem; min-height: 44px; }
.compareRow input:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.sharedAllBlock { margin: var(--sp-4) 0; }
.sharedAllBlock h2 { font-size: var(--fs-body); color: var(--teal-text); margin: 0 0 var(--sp-1); }
.sharedPct { font-weight: 700; color: var(--teal-text); }
.compareTable { width: 100%; border-collapse: collapse; font-size: var(--fs-small); margin: var(--sp-3) 0; }
.compareTable th { text-align: left; border-bottom: 1px solid var(--line-strong); padding: var(--sp-1) var(--sp-2) var(--sp-1) 0; font-size: var(--fs-caption); color: var(--ink-faint); }
.compareTable td { border-bottom: 1px solid var(--line); padding: var(--sp-1) var(--sp-2) var(--sp-1) 0; vertical-align: top; }
.rowDiscuss { background: var(--teal-soft); }
.rowNote { color: var(--ink-faint); }
.rowDivergent { background: #fff; font-weight: 700; }
.adviceBand { background: var(--teal-soft); border-radius: var(--radius); padding: var(--sp-2) var(--sp-3); margin: var(--sp-3) 0; }
.adviceBand p { margin: 0; font-size: var(--fs-small); }
...@@ -196,9 +196,10 @@ export function decodePayload(s) { ...@@ -196,9 +196,10 @@ export function decodePayload(s) {
} }
} }
export function shareUrl(payload) { export function shareUrl(payload, fragKey) {
if (typeof location === 'undefined') return ''; if (typeof window === 'undefined') return '';
return location.href.split('#')[0] + '#p=' + answersToBase64url(payload); var key = fragKey || 'p';
return window.location.origin + window.location.pathname + '#' + key + '=' + answersToBase64url(payload);
} }
/* ---------- stockage ---------- /* ---------- stockage ----------
...@@ -318,3 +319,80 @@ export function downloadExport() { ...@@ -318,3 +319,80 @@ export function downloadExport() {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}, 0); }, 0);
} }
/* ---------- partage global, export PDF, comparaison à deux ---------- */
/* Sérialise TOUS les questionnaires en un seul payload (#a=). Chaque bloc
réutilise serializeAnswers ; les questionnaires sans réponse sont inclus
vides (le thérapeute voit ce qui reste à remplir). */
export function serializeAllAnswers(respondentLabel) {
var readAll = readAllRecords();
return {
v: 1,
kind: 'all',
respondent: respondentLabel,
created: new Date().toISOString(),
questionnaires: COUPLE_DATA.questionnaires.map(function (q) {
var block = serializeAnswers(q.id, readAll[q.id], respondentLabel);
return block || { qid: q.id, title: q.title, respondent: respondentLabel, answered: [] };
}),
};
}
function readAllRecords() {
var out = {};
COUPLE_DATA.questionnaires.forEach(function (q) { out[q.id] = readRecord(q.id); });
return out;
}
/* Décode les deux formes de lien : mono-questionnaire (#p=) et global (#a=). */
export function decodeAnyPayload(s) {
var p = decodePayload(s);
if (p) return { kind: 'one', payload: p };
try {
var b64 = s.replace(/-/g, '+').replace(/_/g, '/');
var bin = atob(b64);
var bytes = new Uint8Array(bin.length);
for (var i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
var o = JSON.parse(utf8Decode(bytes));
if (o && typeof o === 'object' && o.kind === 'all' && Array.isArray(o.questionnaires)) {
return { kind: 'all', payload: o };
}
} catch (e) {}
return null;
}
/* Le conseil de discussion propre à l'instrument (le debrief du support) :
pour bonheur_conjugal la règle du clinicien est explicite — discuter les
3 et 4, noter les 0-2 sans débattre. On la porte telle quelle ; les
instruments sans règle écrite n'en inventent pas. */
export function discussionAdvice(qid) {
if (qid !== 'bonheur_conjugal') return null;
return {
discussFrom: 3, // items cotés 3-4 par l'un ou l'autre : matière à dire
noteBelow: 3, // items cotés 0-2 : à noter, à adapter, pas à débattre
};
}
/* Regroupe les réponses des deux partenaires, item par item. Retourne des
lignes {key, text, mine, theirs, labelA, labelB, divergence} — jamais un
score de compatibilité (context.md §5 : divergence par item, rien d'autre). */
export function compareAnswers(qid, myRecord, theirBlock, q) {
var rows = [];
var theirMap = {};
(theirBlock && theirBlock.answered || []).forEach(function (e) { theirMap[e.i] = e; });
allItems(q).forEach(function (r) {
var mineV = myRecord && myRecord.answers ? myRecord.answers[r.key] : undefined;
var theirEntry = theirMap[r.key];
var rowsVal = {
key: r.key,
text: r.item.text,
mine: mineV !== undefined ? optionLabel(q, r.section, mineV) : null,
theirs: theirEntry ? (theirEntry.l || '') : null,
};
// divergence : les deux ont répondu et l'étiquette diffère
rowsVal.divergence = rowsVal.mine != null && rowsVal.theirs != null && rowsVal.mine !== rowsVal.theirs;
rows.push(rowsVal);
});
return rows;
}
/* Clarté couple — export PDF des questionnaires remplis (jspdf). Un document,
huit blocs : titre verbatim, instructions, items numérotés avec la réponse
cochée (étiquette), score quand l'instrument en a un. Le destinataire lit
l'équivalent du support papier, rempli. Aucune donnée ne quitte l'appareil
avant l'enregistrement du fichier par la personne elle-même. */
import { COUPLE_DATA } from './data';
import { allItems, readRecord, readSession, optionLabel } from './engine';
export async function exportCouplePdf() {
let JsPDF;
try { ({ jsPDF: JsPDF } = await import('jspdf')); }
catch (e) { return false; }
var doc = new JsPDF({ unit: 'pt', format: 'a4' });
var W = doc.internal.pageSize.getWidth();
var H = doc.internal.pageSize.getHeight();
var y = 50;
function ensure(h) {
if (y + h > H - 60) { foot(); doc.addPage(); y = 50; }
}
function foot() {
var i = doc.internal.getNumberOfPages();
doc.setFont('helvetica', 'normal'); doc.setFontSize(8); doc.setTextColor(130);
doc.text('Clarté · Espace couple · document personnel · page ' + i, 50, H - 30);
}
function text(s, size, bold, indent) {
doc.setFont('helvetica', bold ? 'bold' : 'normal');
doc.setFontSize(size || 10);
doc.setTextColor(40);
var lines = doc.splitTextToSize(s, W - 100 - (indent || 0));
lines.forEach(function (line) { ensure(14); doc.text(line, 50 + (indent || 0), y); y += 14; });
}
var session = readSession();
text('Clarté · Espace couple', 16, true);
text('Réponses de : ' + (session.respondent_label || 'moi') + ' · ' + new Date().toLocaleDateString('fr-FR'), 10);
y += 10;
COUPLE_DATA.questionnaires.forEach(function (q) {
var st = readRecord(q.id);
var items = allItems(q);
var answered = 0;
items.forEach(function (r) { if (st && st.answers[r.key] !== undefined) answered++; });
ensure(30);
y += 8;
text(q.title, 13, true);
if (q.instructions) text(q.instructions, 8);
text(answered + ' / ' + items.length + ' répondues', 8);
y += 4;
items.forEach(function (r, i) {
var v = st ? st.answers[r.key] : undefined;
var label = v !== undefined ? optionLabel(q, r.section, v) : '(non répondu)';
text((i + 1) + '. ' + r.item.text, 9, false, 8);
text('→ ' + label, 9, true, 20);
});
// score de l'instrument, quand il en a un et qu'il est complet
if (q.scoring && q.scoring.method === 'sum_all_items' && st) {
var sum = 0, maxSum = 0, n = 0;
items.forEach(function (r) {
var v = st.answers[r.key];
if (typeof v === 'number') { sum += v; maxSum += 4; n++; }
});
if (n === items.length && maxSum > 0) {
text('Score : ' + Math.round((sum / maxSum) * 100) + ' %', 10, true);
}
}
});
foot();
doc.save('clarte-couple-' + new Date().toISOString().slice(0, 10) + '.pdf');
return true;
}
...@@ -26,6 +26,14 @@ export const strings = { ...@@ -26,6 +26,14 @@ export const strings = {
cardQuestions: (total) => total + ' questions', cardQuestions: (total) => total + ' questions',
cardWorksheet: (total) => total + ' écrits à compléter', cardWorksheet: (total) => total + ' écrits à compléter',
cardPartial: (n, total) => 'En cours · ' + n + ' sur ' + total, cardPartial: (n, total) => 'En cours · ' + n + ' sur ' + total,
togetherTitle: 'À deux, et avec votre praticienne',
togetherIntro:
'Un lien avec toutes vos réponses, un document PDF à apporter en séance, ou la comparaison côte à côte avec la personne que vous aimez.',
shareAllBtn: 'Créer un lien avec toutes mes réponses',
shareAllNote:
'Ce lien regroupe tout le parcours sur une seule page, en lecture seule pour qui l’ouvre. Même prudence que pour un lien simple.',
pdfBtn: 'Exporter tout en PDF (documents remplis)',
pdfDone: 'PDF téléchargé',
}, },
intro: { intro: {
...@@ -110,4 +118,37 @@ export const strings = { ...@@ -110,4 +118,37 @@ export const strings = {
'Lien incomplet ou altéré. Demandez à la personne qui l’a envoyé d’en créer un nouveau.', 'Lien incomplet ou altéré. Demandez à la personne qui l’a envoyé d’en créer un nouveau.',
backHome: 'Retour à l’accueil', backHome: 'Retour à l’accueil',
}, },
sharedAll: {
title: 'Réponses partagées : tout le parcours',
by: (respondent) => 'Partagé par ' + respondent,
totalLine: (n) =>
n === 1 ? '1 réponse au total' : n + ' réponses au total',
empty: 'Aucune réponse à ce questionnaire pour l’instant.',
scoreLine: (pct) => 'Score de l’instrument : ' + pct + ' %',
partial: (n, total) => n + ' réponses sur ' + total + ' : le questionnaire est encore en cours.',
},
compare: {
pasteLabel: 'Comparer nos réponses',
pasteHint: 'Collez ici le lien que l’autre personne vous a envoyé.',
pasteAria: 'Lien de partage à comparer',
pasteBtn: 'Comparer',
pasteError: 'Ce lien n’a pas pu être lu. Demandez un nouveau lien de partage.',
title: (qTitle) => 'À deux : ' + qTitle,
legende: (mine, theirs) => 'Colonne du milieu : ' + mine + ' (cet appareil). Colonne de droite : ' + theirs + '.',
colItem: 'Question',
colMine: 'Moi',
colTheirs: 'L’autre',
moi: 'cet appareil',
autre: 'l’autre',
adviceTitle: 'Le conseil du support d’origine pour en parler :',
adviceRule: (from, below) =>
'Les réponses à ' + from + ' ou 4 sont faites pour être dites ; les réponses sous ' + below +
' se notent pour adapter son comportement, sans en faire un débat.',
pcts: (mine, theirs) =>
'Scores de l’instrument : ' + (mine || '—') + ' (moi) et ' + (theirs || '—') + (theirs ? '' : '') + '.',
noScore:
'Il n’y a pas de score de compatibilité ici, et c’est voulu : ce qui se regarde, c’est chaque question, une par une.',
},
}; };
'use client';
/* Clarté TDAH — racine. Écran unique, une question à la fois (spec §6),
flux = liste de nœuds reconstruite selon l'état de la Partie B (spec §4).
Persistance : contrat clarte:v1 gelé (kit README) — {responses,
currentSection, pos, partBSkipped, lastMilestone?} ; les enregistrements
vanilla existants ({responses, currentSection}) fusionnent sans perte. */
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
ASRS_PART_A, ASRS_PART_B, DIVA_INATTENTION, DIVA_HYPERACTIVITY,
DIVA_CHILDHOOD, DIVA_IMPAIRMENT, EXEC_CLUSTERS, PART_B_IDS, NOTES,
SIX_MONTHS_NOTE_FIRST, TOTAL_QUESTIONS,
} from './data';
import { dict as dictFr } from './i18n/fr';
import { dict as dictEn } from './i18n/en';
import { readState, writeState, clearState, storageAvailable, partAShadedCount, partBAnswered, scope } from './engine';
import HomeView from './HomeView';
import QuizScreen from './QuizScreen';
import RecapView from './RecapView';
import ResultsView from './ResultsView';
import GlossaryView from './GlossaryView';
import styles from './tdah.module.css';
function questionnaireNodes(partBSkipped, responses) {
/* Nœuds de flux : intro → questions (+ porte) → ponts → récapitulatif.
La porte n'apparaît que si la Partie A est sous le seuil ET que la
Partie B n'est ni passée ni commencée (spec §4). */
const nodes = [];
const pushQ = (list, block, extra) =>
list.forEach((q, i) => nodes.push({ type: 'question', q, block, indexInBlock: i, ...extra }));
nodes.push({ type: 'intro', id: 'asrs' });
pushQ(ASRS_PART_A, 'asrsA', { scale: 'asrs' });
if (partAShadedCount(responses) < 4 && !partBSkipped && !partBAnswered(responses)) {
nodes.push({ type: 'gate' });
}
if (!partBSkipped) pushQ(ASRS_PART_B, 'asrsB', { scale: 'asrs' });
nodes.push({ type: 'intro', id: 'dsm' });
pushQ(DIVA_INATTENTION, 'dsmA1', { binary: true });
nodes.push({ type: 'bridge', id: 'a2' });
pushQ(DIVA_HYPERACTIVITY, 'dsmA2', { binary: true });
nodes.push({ type: 'intro', id: 'enfance' });
pushQ(DIVA_CHILDHOOD, 'dsmB', { binary: true });
nodes.push({ type: 'intro', id: 'retentissement' });
pushQ(DIVA_IMPAIRMENT, 'dsmCD', { binary: true });
nodes.push({ type: 'bridge', id: 'brown' });
EXEC_CLUSTERS.forEach((c) => pushQ(c.questions, 'exec', { scale: 'ef', cluster: c }));
nodes.push({ type: 'recap' });
return nodes;
}
export default function ClarteTdah({ lang }) {
const t = lang === 'en' ? dictEn : dictFr;
const [mounted, setMounted] = useState(false);
const [state, setState] = useState(null); // {responses, currentSection, pos, partBSkipped, lastMilestone}
const [storageOk, setStorageOk] = useState(true);
useEffect(() => {
setMounted(true);
setStorageOk(storageAvailable());
const saved = readState();
setState(saved
? { responses: saved.responses || {}, pos: typeof saved.pos === 'number' ? saved.pos : 0,
partBSkipped: !!saved.partBSkipped, lastMilestone: saved.lastMilestone || null,
currentSection: saved.currentSection || 'home' }
: { responses: {}, pos: 0, partBSkipped: false, lastMilestone: null, currentSection: 'home' });
}, []);
const persist = useCallback((next) => {
setState(next);
return writeState(next);
}, []);
const goHome = useCallback(() => {
setState((s) => { const n = { ...s, currentSection: 'home' }; writeState(n); return n; });
setRouteLocal({ view: 'home' });
}, []);
const [route, setRouteLocal] = useState({ view: 'home' });
const nodes = useMemo(
() => (state ? questionnaireNodes(state.partBSkipped, state.responses) : []),
[state && state.partBSkipped, state && !!partBAnswered(state ? state.responses : {}), state && state.responses === state.responses]
);
const commitAnswer = useCallback((qid, value) => {
setState((s) => {
const responses = { ...s.responses, [qid]: value };
const next = { ...s, responses };
// milestones (deck §12) : franchissement → affiché sur l'écran suivant
const answered = Object.keys(responses).length;
const sc = scope(next.partBSkipped);
const inAsrsB = PART_B_IDS.indexOf(qid) !== -1;
const finishedAsrs = answered === (next.partBSkipped ? ASRS_PART_A.length : ASRS_PART_A.length + ASRS_PART_B.length);
const finishedDsm = answered === ASRS_PART_A.length + ASRS_PART_B.length + DIVA_INATTENTION.length + DIVA_HYPERACTIVITY.length + DIVA_CHILDHOOD.length + DIVA_IMPAIRMENT.length;
let milestone = null;
if (answered === sc) milestone = 'all.done';
else if (!inAsrsB && finishedAsrs) milestone = 'asrs.done';
else if (finishedDsm) milestone = 'dsm.done';
else if (answered === Math.floor(sc / 2)) milestone = 'half';
if (milestone && milestone !== s.lastMilestone) next.lastMilestone = milestone;
writeState(next);
return next;
});
}, []);
if (!mounted || !state) return <div className={styles.root} />;
const currentSection = route.view === 'home' ? 'home' : route.view;
if (route.view === 'home') {
return (
<HomeView
t={t} state={state} styles={styles} storageOk={storageOk}
onStart={(pos) => { persist({ ...state, currentSection: 'flow' }); setRouteLocal({ view: 'flow', pos }); }}
onGlossary={() => setRouteLocal({ view: 'glossary' })}
/>
);
}
if (route.view === 'glossary') {
return <GlossaryView t={t} styles={styles} onBack={() => setRouteLocal({ view: 'home' })} />;
}
if (route.view === 'results') {
return (
<ResultsView
t={t} styles={styles} responses={state.responses} lang={lang}
onHome={goHome}
onReview={() => setRouteLocal({ view: 'flow', pos: firstQuestionNode(nodes, state.responses) })}
onRestart={() => { clearState(); setState({ responses: {}, pos: 0, partBSkipped: false, lastMilestone: null, currentSection: 'home' }); setRouteLocal({ view: 'home' }); }}
/>
);
}
// flux : question / porte / récapitulatif
const pos = Math.max(0, Math.min(route.pos ?? state.pos ?? 0, nodes.length - 1));
const node = nodes[pos];
if (node.type === 'recap') {
return (
<RecapView
t={t} styles={styles} state={state} nodes={nodes}
onJump={(i) => setRouteLocal({ view: 'flow', pos: i })}
onResults={() => { persist({ ...state, currentSection: 'results' }); setRouteLocal({ view: 'results' }); }}
onHome={goHome}
/>
);
}
if (node.type === 'intro' || node.type === 'bridge') {
return <Interstitial node={node} t={t} styles={styles} onNext={() => setRouteLocal({ view: 'flow', pos: pos + 1 })} answeredCount={Object.keys(state.responses).length} total={scope(state.partBSkipped)} lang={lang} />;
}
if (node.type === 'gate') {
return (
<GateView
t={t} styles={styles}
onAnswer={() => setRouteLocal({ view: 'flow', pos: pos + 1 })}
onSkip={() => { persist({ ...state, partBSkipped: true }); setRouteLocal({ view: 'flow', pos: pos + 1 }); }}
/>
);
}
return (
<QuizScreen
key={node.q.id}
node={node} t={t} styles={styles}
value={state.responses[node.q.id]}
answered={Object.keys(state.responses).length}
total={scope(state.partBSkipped)}
milestone={state.lastMilestone}
showMilestone={state.lastMilestone && state.lastMilestoneAt === pos}
onPick={(v) => {
commitAnswer(node.q.id, v);
// auto-avance ~350 ms (spec §6.3), jamais sur le dernier écran de bloc
const nextType = nodes[pos + 1] && nodes[pos + 1].type;
if (nextType === 'question') {
setTimeout(() => setRouteLocal((r) => (r.view === 'flow' ? { view: 'flow', pos: pos + 1 } : r)), 350);
} else {
setRouteLocal({ view: 'flow', pos: pos + 1 });
}
}}
onNext={() => setRouteLocal({ view: 'flow', pos: Math.min(pos + 1, nodes.length - 1) })}
onPrev={() => setRouteLocal({ view: 'flow', pos: Math.max(pos - 1, 0) })}
/>
);
}
function firstQuestionNode(nodes, responses) {
for (let i = 0; i < nodes.length; i++) {
if (nodes[i].type === 'question' && responses[nodes[i].q.id] === undefined) return i;
}
return nodes.length - 1;
}
/* Écrans intro / pont (deck §12 : intro.asrs/dsm/enfance/retentissement,
bridge.a2/brown) — un geste, verbatim sous le CTA quand c'est long. */
function Interstitial({ node, t, styles, onNext, answeredCount, total, lang }) {
const key = node.type === 'intro' ? 'intro.' + node.id : 'bridge.' + node.id;
const h = t[key + '.h'];
const p = t[key + '.p'];
return (
<div className={styles.interstitial}>
<h1>{h}</h1>
<p className={styles.muted}>
{answeredCount} {answeredCount === 1 ? (lang === 'en' ? 'answered' : 'répondu') : (lang === 'en' ? 'answered' : 'répondues')} · {total} {lang === 'en' ? 'in the whole flow' : 'dans tout le parcours'}
</p>
<button className={`${styles.primaryBtn} ${styles.btn}`} onClick={onNext}>
{t['intro.continue.btn']}
</button>
<p>{p}</p>
</div>
);
}
/* Porte de la Partie B (spec §4, copy deck gate.*). Décision réversible :
le récapitulatif propose de reprendre la partie. */
function GateView({ t, styles, onAnswer, onSkip }) {
return (
<div className={styles.interstitial}>
<h2>{t['gate.title']}</h2>
<p>{t['gate.body']}</p>
<div className={styles.gateActions}>
<button className={`${styles.primaryBtn} ${styles.btn}`} onClick={onAnswer}>{t['gate.answer.btn']}</button>
<button className={styles.btn} onClick={onSkip}>{t['gate.skip.btn']}</button>
</div>
</div>
);
}
export { questionnaireNodes, firstQuestionNode, TOTAL_QUESTIONS };
'use client';
/* Clarté TDAH — glossaire (spec §7.5) : la seule porte du vocabulaire,
dix entrées verbatim du deck §14. */
export default function GlossaryView({ t, styles, onBack }) {
const keys = ['tdah', 'asrs', 'dsm5', 'inattention', 'hyperactivite', 'impulsivite',
'fonctions.executives', 'memoire.travail', 'retentissement', 'depistage'];
return (
<div className={styles.root}>
<h1>{t['glossary.title']}</h1>
<dl>
{keys.map((k) => (
<div key={k} className={styles.glossEntry}>
<dt>{t['glossary.' + k].split('.')[0]}</dt>
<dd>{t['glossary.' + k]}</dd>
</div>
))}
</dl>
<button className={styles.btn} onClick={onBack}>{t['header.logo.h1']}</button>
</div>
);
}
'use client';
/* Clarté TDAH — accueil (spec §5) : h2 de but, trois cartes outils, consentement
bloquant, carte de reprise sans culpabilité, avertissement six points verbatim
du deck §4, ligne de durée honnête (li5), sauvegarde démontrable (§9.1). */
import { useState } from 'react';
import ProgressHeader from '@/components/clarte/ProgressHeader';
import { TOTAL_QUESTIONS } from './data';
import { progressLine, progressFull } from './QuizScreen';
export default function HomeView({ t, state, styles, storageOk, onStart, onGlossary }) {
const [consent, setConsent] = useState(!!state.consented);
const answered = Object.keys(state.responses).length;
const hasProgress = answered > 0;
const pos = typeof state.pos === 'number' ? state.pos : 0;
const start = () => {
if (!consent) return;
try { const s = JSON.parse(JSON.stringify(state)); s.consented = true; localStorage.setItem('clarte:v1', JSON.stringify(s)); } catch (e) {}
onStart(hasProgress ? pos : 0);
};
return (
<div className={styles.root}>
<h1>{t['header.logo.h1']}<span className={styles.tagline}>{t['header.subtitle']}</span></h1>
<h2>{t['home.h2']}</h2>
<p>{t['home.intro.p']}</p>
<div className={styles.cards}>
<div className={styles.card}>
<h4>{t['home.card.asrs.h4']}</h4>
<p>{t['home.card.asrs.p']}</p>
</div>
<div className={styles.card}>
<h4>{t['home.card.dsm.h4']}</h4>
<p>{t['home.card.dsm.p']}</p>
</div>
<div className={styles.card}>
<h4>{t['home.card.ef.h4']}</h4>
<p>{t['home.card.ef.p']}</p>
</div>
</div>
<div className={styles.warning}>
<h4>{t['home.warning.h4']}</h4>
<ul>
<li><strong>{t['home.warning.li1']}</strong></li>
<li>{t['home.warning.li2']}</li>
<li>{t['home.warning.li3']}</li>
<li>{t['home.warning.li4']}</li>
<li>{t['home.warning.li5']}</li>
<li>{t['home.warning.li6']}</li>
</ul>
</div>
{hasProgress && (
<div className={styles.resume}>
<p>{t['resume.count'].replace('{n}', String(answered)).replace('{total}', String(TOTAL_QUESTIONS)).replace(/question\(s\)/, answered > 1 ? 'questions' : 'question')}</p>
<div className={styles.nav}>
<button className={`${styles.primaryBtn} ${styles.btn}`} onClick={start}>{t['resume.primary']}</button>
</div>
</div>
)}
{!hasProgress && (
<div className={styles.consent}>
<label>
<input type="checkbox" checked={consent} onChange={(e) => setConsent(e.target.checked)} />
<span>{t['home.consent.label']}</span>
</label>
<button className={`${styles.primaryBtn} ${styles.btn}`} disabled={!consent} onClick={start}>
{t['home.start.btn']}
</button>
</div>
)}
<p className={styles.muted}>{t['home.storage.line']}</p>
<button className={styles.linkbtn} onClick={onGlossary}>{t['glossary.link']}</button>
</div>
);
}
'use client';
/* Clarté TDAH — écran question : QuestionCard (kit), pilules ou binaire Oui/Non
(même état sélectionné, pas de corail/sauge, spec §6.2), note repliée
« Pourquoi cette question ? » (deck §7.4, ≤ 1 par écran), clavier : chiffres
(useDigitKeys), flèches, Entrée = avancer. Auto-avance portée par le parent. */
import { useEffect, useRef, useState } from 'react';
import ProgressHeader from '@/components/clarte/ProgressHeader';
import QuestionCard from '@/components/clarte/QuestionCard';
import OptionPills from '@/components/clarte/OptionPills';
import useDigitKeys from '@/components/clarte/useDigitKeys';
import { NOTES, SIX_MONTHS_NOTE_FIRST } from './data';
import styles from './tdah.module.css';
const NBSP = ' ';
export default function QuizScreen({ node, t, styles, value, answered, total, milestone, showMilestone, onPick, onNext, onPrev }) {
const { q, block, scale, binary, cluster, indexInBlock } = node;
const isBinary = !!binary;
const optionCount = isBinary ? 2 : scale === 'asrs' ? 5 : 4;
const ribbon = (() => {
switch (block) {
case 'asrsA': return t['asrs.h2'] + NBSP + '· ' + t['asrs.partA.h3'];
case 'asrsB': return t['asrs.h2'] + NBSP + '· ' + t['asrs.partB.h3'];
case 'dsmA1': return t['diva.h2'] + NBSP + '· ' + t['diva.A1.h3'];
case 'dsmA2': return t['diva.h2'] + NBSP + '· ' + t['diva.A2.h3'];
case 'dsmB': return t['diva.h2'] + NBSP + '· ' + t['diva.B.h3'];
case 'dsmCD': return t['diva.h2'] + NBSP + '· ' + t['diva.CD.h3'];
case 'exec': return t['executive.h2'] + NBSP + '· ' + (cluster ? t[cluster.nameKey] : '');
default: return '';
}
})();
useDigitKeys(optionCount, (i) => {
if (isBinary) onPick(i === 0 ? 1 : 0);
else onPick(i);
});
useEffect(function arrowsAndEnter() {
function onKey(event) {
if (event.ctrlKey || event.metaKey || event.altKey) return;
const tag = event.target && event.target.tagName;
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || (event.target && event.target.isContentEditable)) return;
if (event.key === 'ArrowRight' || event.key === 'Enter') { event.preventDefault(); onNext(); }
else if (event.key === 'ArrowLeft') { event.preventDefault(); onPrev(); }
}
document.addEventListener('keydown', onKey);
return () => document.removeEventListener('keydown', onKey);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const cardRef = useRef(null);
useEffect(function focusQuestion() {
const node2 = cardRef.current && cardRef.current.querySelector('h2');
if (node2) { node2.setAttribute('tabindex', '-1'); node2.focus({ preventScroll: true }); }
}, []);
const noteKey = NOTES[q.id] || null;
const sixMonths = q.id === SIX_MONTHS_NOTE_FIRST;
const options = isBinary
? [{ value: 1, label: t['diva.binary.yes'] }, { value: 0, label: t['diva.binary.no'] }]
: (scale === 'asrs'
? [0, 1, 2, 3, 4].map((i) => ({ value: i, label: t['asrs.option.' + i] }))
: [0, 1, 2, 3].map((i) => ({ value: i, label: t['executive.ef_options.' + i] })));
return (
<div>
<ProgressHeader
answered={answered}
total={total}
milestone={showMilestone && milestone ? t['milestone.' + milestone] : null}
labels={{
answeredLine: (n, m) => progressLine(t, n, m),
fullLine: (n) => progressFull(t, n),
}}
/>
<div ref={cardRef} className={styles.quizCard}>
<QuestionCard
ribbon={ribbon + (sixMonths ? NBSP + '· ' + t['intro.sixMonthsSuffix'] : '')}
subtitle={block === 'exec' ? t[cluster.descKey] : null}
question={t[q.t]}
footer={
<div className={styles.nav}>
<button className={styles.linkbtn} onClick={onPrev}>{t['intro.back'] || '←'}</button>
<button className={styles.linkbtn} onClick={onNext}>{t['intro.skip.btn']}</button>
</div>
}
>
<OptionPills options={options} value={value != null ? value : null} onChange={onPick} indexHint />
{noteKey && (
<details className={styles.note}>
<summary>{t['note.label']}</summary>
<p>{t[noteKey]}</p>
</details>
)}
</QuestionCard>
</div>
</div>
);
}
function progressLine(t, n, m) {
// deck §12 progress.answered : « {n} question(s) répondue(s) · il en reste {m} »
// — le (s) est résolu par le code : 0 et 1 au singulier (deck : « 0 question
// répondue · il en reste 70 »), {n} et {m} substitués.
let s = t['progress.answered'];
s = s.replace(/\{n\}/g, String(n)).replace(/\{m\}/g, String(m));
s = s.replace(/question\(s\)/g, n > 1 ? 'questions' : 'question');
s = s.replace(/répondue\(s\)/g, n > 1 ? 'répondues' : 'répondue');
s = s.replace(/question\(s\)/g, n > 1 ? 'questions' : 'question');
return s;
}
function progressFull(t, n) {
let s = t['progress.answered'];
s = s.replace(/\{n\}/g, String(n)).replace(/\{total\}/g, String(n)).replace(/\{m\}/g, '0');
s = s.replace(/question\(s\)/g, n > 1 ? 'questions' : 'question');
s = s.replace(/répondue\(s\)/g, n > 1 ? 'répondues' : 'répondue');
s = s.replace(/\s*·\s*(il en reste\s*0|0 to go)/, '');
return s;
}
export { progressLine, progressFull };
'use client';
/* Clarté TDAH — récapitulatif (spec §6.4) : les 70 items groupés par bloc,
puces « non répondu » neutres, saut au clic, porte de complétion : tout est
répondu → « Voir mes résultats → » ; sinon compte + aller à la première
manquante / passer les restantes (hors périmètre, comme un saut Partie B). */
import { useMemo } from 'react';
import ProgressHeader from '@/components/clarte/ProgressHeader';
import {
ASRS_PART_A, ASRS_PART_B, DIVA_INATTENTION, DIVA_HYPERACTIVITY,
DIVA_CHILDHOOD, DIVA_IMPAIRMENT, EXEC_CLUSTERS,
} from './data';
import { progressLine } from './QuizScreen';
export default function RecapView({ t, styles, state, nodes, onJump, onResults, onHome }) {
const responses = state.responses;
const groups = useMemo(() => ([
{ label: t['asrs.h2'], qs: [...ASRS_PART_A, ...ASRS_PART_B] },
{ label: t['diva.A1.h3'], qs: DIVA_INATTENTION },
{ label: t['diva.A2.h3'], qs: DIVA_HYPERACTIVITY },
{ label: t['diva.B.h3'], qs: DIVA_CHILDHOOD },
{ label: t['diva.CD.h3'], qs: DIVA_IMPAIRMENT },
...EXEC_CLUSTERS.map((c) => ({ label: t[c.nameKey], qs: c.questions })),
]), [t]);
const nodeIndex = useMemo(() => {
const m = {};
nodes.forEach((n, i) => { if (n.type === 'question') m[n.q.id] = i; });
return m;
}, [nodes]);
const answered = Object.keys(responses).length;
const unanswered = TOTAL_Q() - answered;
const firstMissing = nodes.findIndex((n) => n.type === 'question' && responses[n.q.id] === undefined);
const optionLabel = (qid) => {
const v = responses[qid];
if (v === undefined) return null;
if (DIVA_INATTENTION.concat(DIVA_HYPERACTIVITY, DIVA_CHILDHOOD, DIVA_IMPAIRMENT).some((q) => q.id === qid)) {
return v === 1 ? t['diva.binary.yes'] : t['diva.binary.no'];
}
return v >= 4 ? t['asrs.option.' + v] : (t['asrs.option.' + v] || t['executive.ef_options.' + v]);
};
const skipRemaining = () => {
const next = { ...state, responses: { ...state.responses } };
nodes.forEach((n) => {
if (n.type === 'question' && next.responses[n.q.id] === undefined) next.responses[n.q.id] = 0;
});
// note : un saut Partie B est réversible au récapitulatif ; ici les items
// passés sont mis à 0 comme le `|| 0` du POC (spéc §6.4, byte-identique)
onJump(nodes.length - 1);
};
return (
<div className={styles.root}>
<ProgressHeader
answered={answered}
total={TOTAL_Q()}
labels={{ answeredLine: (n, m) => progressLine(t, n, m), fullLine: (n) => progressLine(t, n, 0) }}
/>
<h1>{t['recap.title']}</h1>
{groups.map((g) => (
<div key={g.label} className={styles.recapGroup}>
<h3>{g.label}</h3>
<ul>
{g.qs.map((q) => {
const label = optionLabel(q.id);
const idx = nodeIndex[q.id];
return (
<li key={q.id}>
<button type="button" className={styles.recapRow} onClick={() => onJump(idx)}>
<span className={styles.recapText}>{t[q.t]}</span>
<span className={label ? styles.recapChip : styles.recapChipEmpty}>
{label || t['recap.unanswered.chip']}
</span>
</button>
</li>
);
})}
</ul>
</div>
))}
<div className={styles.nav}>
<button className={styles.btn} onClick={onHome}>{t['recap.backHome'] || t['resume.secondary']}</button>
{unanswered === 0 ? (
<button className={`${styles.primaryBtn} ${styles.btn}`} onClick={onResults}>{t['recap.complete.btn']}</button>
) : (
<>
<span className={styles.muted}>{t['recap.missing.count'].replace('{n}', String(unanswered)).replace(/question\(s\)/, unanswered > 1 ? 'questions' : 'question')}</span>
<button className={styles.btn} onClick={() => onJump(firstMissing)}>{t['recap.missing.go']}</button>
<button className={styles.btn} onClick={skipRemaining}>{t['recap.missing.skip']}</button>
</>
)}
</div>
</div>
);
}
function TOTAL_Q() {
return 70;
}
'use client';
/* Clarté TDAH — résultats (spec §8) : rappel non-diagnostique, synthèse à
trois puces (variantes n du deck), trois cartes, « Comprendre votre profil »
×3, « Pour la consultation » avec phrase copiable, PDF, annexe. */
import { useMemo, useState } from 'react';
import { calculateResults } from './engine';
import { EXEC_CLUSTERS, DIVA_IMPAIRMENT } from './data';
import { buildReport } from './pdf';
export default function ResultsView({ t, styles, responses, lang, onHome, onReview, onRestart }) {
const r = useMemo(() => calculateResults(responses), [responses]);
const [copied, setCopied] = useState(false);
const n = r.global.positiveIndicators;
const summaryText = n === 0 ? t['results.summary.text.empty'] : n >= 2 ? t['results.summary.text.high'] : t['results.summary.text.low'];
const summary = summaryText.replace(/\{n\}/g, String(n));
const domainLabel = (key) => t['diva.domain.' + key];
const severityLabel = (s) => t['results.severity.' + s];
const copySentence = () => {
const text = t['consult.sentence'];
const done = () => { setCopied(true); setTimeout(() => setCopied(false), 2000); };
try {
if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(done, () => {}); return; }
} catch (e) {}
try {
const inp = document.createElement('textarea');
inp.value = text; document.body.appendChild(inp); inp.select();
if (document.execCommand('copy')) done();
document.body.removeChild(inp);
} catch (e) {}
};
return (
<div className={styles.root}>
<p className={styles.warningInline}><strong>{t['results.warning.p']}</strong></p>
<h2>{t['results.summary.h2']}</h2>
<p className={styles.summaryText}>{summary}</p>
{r.asrs.screeningPositive && <p>{t['results.summary.bullet.asrs']}</p>}
{(r.diva.meetsInattention || r.diva.meetsHyperactivity) && (
<p>{t['results.summary.bullet.dsm'].replace('{presentation}', t['results.presentation.' + r.diva.presentation])}</p>
)}
{r.executive.severity !== 'low' && (
<p>{t['results.summary.bullet.ef'].replace('{niveau}', severityLabel(r.executive.severity))}</p>
)}
<h2>{t['understand.title']}</h2>
<p>{t['understand.asrs.p']}</p>
<p>{t['understand.dsm.p']}</p>
<p>{t['understand.ef.p']}</p>
<div className={styles.card}>
<h3>{t['results.card1.h3']}</h3>
<p>{t['results.card1.status']
.replace('{atteint}', r.asrs.screeningPositive ? 'atteint' : 'non atteint')
.replace('{met}', r.asrs.screeningPositive ? 'met' : 'not met')
.replace('{non atteint}', 'non atteint').replace('{not met}', 'not met')
.replace('{x}', String(r.asrs.partAShaded))}</p>
<p>{t['results.card1.label.partA']} : {r.asrs.partAScore}/24</p>
<p>{t['results.card1.label.partB']}</p>
<p>{t['results.card1.inattention'].replace('{n}', String(r.asrs.inattention))}</p>
<p>{t['results.card1.hyperactivity'].replace('{n}', String(r.asrs.hyperactivity))}</p>
</div>
<div className={styles.card}>
<h3>{t['results.card2.h3']}</h3>
<p>{t['results.presentation.' + r.diva.presentation]}</p>
<p>{t['results.card2.label.A1']} : {r.diva.inattentionCount}/9</p>
<p>{t['results.card2.label.A2']} : {r.diva.hyperactivityCount}/9</p>
<p>{t['results.card2.label.B']} : {r.diva.childhoodCount}/5</p>
<p>{t['results.card2.label.impairment']} : {r.diva.impairmentDomainKeys.map(domainLabel).join(', ') || '—'}</p>
</div>
<div className={styles.card}>
<h3>{t['results.card3.h3']}</h3>
<p>{t['results.card3.status']
.replace('{niveau}', severityLabel(r.executive.severity)).replace('{level}', severityLabel(r.executive.severity))
.replace('{p}', String(r.executive.percentage))}</p>
{EXEC_CLUSTERS.map((c) => (
<p key={c.key}>{t[c.nameKey]} : {r.executive.clusterScores[c.key].percentage}%</p>
))}
</div>
<h2>{t['consult.title']}</h2>
<p>{t['consult.bring.p']}</p>
<p><strong>{t['consult.sentence.label']}</strong></p>
<div className={styles.consultRow}>
<p>{t['consult.sentence']}</p>
<button className={styles.btn} onClick={copySentence}>{copied ? '✓' : t['recap.share.copy']}</button>
</div>
<p><strong>{t['consult.questions.h']}</strong></p>
<ul>
<li>{t['consult.q1']}</li>
<li>{t['consult.q2']}</li>
<li>{t['consult.q3']}</li>
</ul>
<div className={styles.nav}>
<button className={styles.btn} onClick={onHome}>{t['recap.backHome']}</button>
<button className={styles.btn} onClick={onReview}>{t['recap.review.fromresults']}</button>
<button className={styles.btn} onClick={() => buildReport(t, responses, r, lang)}>{t['results.download.btn']}</button>
<button className={styles.btn} onClick={onRestart}>{t['results.restart.btn']}</button>
</div>
</div>
);
}
/* Clarté TDAH — structure de l'instrument. Les TEXTES ne vivent pas ici :
chaque question référence une clé de dictionnaire (i18n/fr.js, i18n/en.js,
générés depuis les decks copy-fr.md / copy-en.md). Les seuils et l'ordre
sont l'instrument : intouchables (deck §11.3). */
export const ASRS_PART_A = [
{ id: 'asrs_1', t: 'asrs.q1', threshold: 2 },
{ id: 'asrs_2', t: 'asrs.q2', threshold: 2 },
{ id: 'asrs_3', t: 'asrs.q3', threshold: 2 },
{ id: 'asrs_4', t: 'asrs.q4', threshold: 3 },
{ id: 'asrs_5', t: 'asrs.q5', threshold: 3 },
{ id: 'asrs_6', t: 'asrs.q6', threshold: 3 },
];
export const ASRS_PART_B = [
{ id: 'asrs_7' }, { id: 'asrs_8' }, { id: 'asrs_9' }, { id: 'asrs_10' },
{ id: 'asrs_11' }, { id: 'asrs_12' }, { id: 'asrs_13' }, { id: 'asrs_14' },
{ id: 'asrs_15' }, { id: 'asrs_16' }, { id: 'asrs_17' }, { id: 'asrs_18' },
].map((q, i) => ({ ...q, t: 'asrs.q' + (i + 7) }));
/* Sous-échelles indicatives (deck §11.2) : items d'inattention au sens de
l'instrument ; le reste compte en hyperactivité/impulsivité. */
export const ASRS_INATTENTION_NUMS = [1, 2, 3, 4, 7, 8, 9, 10, 11];
export const DIVA_INATTENTION = [1, 2, 3, 4, 5, 6, 7, 8, 9].map((i) => ({
id: 'diva_ia_' + i, t: 'diva.ia_' + i, binary: true,
}));
export const DIVA_HYPERACTIVITY = [1, 2, 3, 4, 5, 6, 7, 8, 9].map((i) => ({
id: 'diva_hi_' + i, t: 'diva.hi_' + i, binary: true,
}));
export const DIVA_CHILDHOOD = [1, 2, 3, 4, 5].map((i) => ({
id: 'diva_child_' + i, t: 'diva.child_' + i, binary: true,
}));
export const DIVA_IMPAIRMENT = [1, 2, 3, 4, 5].map((i) => ({
id: 'diva_imp_' + i, t: 'diva.imp_' + i, binary: true,
domainKey: ['work', 'family', 'social', 'leisure', 'selfesteem'][i - 1],
}));
export const EXEC_CLUSTERS = [
{ key: 'activation', nameKey: 'executive.cluster.activation.name', descKey: 'executive.cluster.activation.desc', prefix: 'ef_act' },
{ key: 'focus', nameKey: 'executive.cluster.focus.name', descKey: 'executive.cluster.focus.desc', prefix: 'ef_foc' },
{ key: 'effort', nameKey: 'executive.cluster.effort.name', descKey: 'executive.cluster.effort.desc', prefix: 'ef_eff' },
{ key: 'emotion', nameKey: 'executive.cluster.emotion.name', descKey: 'executive.cluster.emotion.desc', prefix: 'ef_emo' },
{ key: 'memory', nameKey: 'executive.cluster.memory.name', descKey: 'executive.cluster.memory.desc', prefix: 'ef_mem' },
{ key: 'action', nameKey: 'executive.cluster.action.name', descKey: 'executive.cluster.action.desc', prefix: 'ef_act2' },
].map((c) => ({
...c,
questions: [1, 2, 3, 4].map((i) => ({ id: c.prefix + '_' + i, t: c.prefix + '_' + i })),
}));
export const ASRS_IDS = [...ASRS_PART_A, ...ASRS_PART_B].map((q) => q.id);
export const DIVA_IDS = [
...DIVA_INATTENTION, ...DIVA_HYPERACTIVITY, ...DIVA_CHILDHOOD, ...DIVA_IMPAIRMENT,
].map((q) => q.id);
export const EXECUTIVE_IDS = EXEC_CLUSTERS.flatMap((c) => c.questions.map((q) => q.id));
export const ALL_QUESTION_IDS = [...ASRS_IDS, ...DIVA_IDS, ...EXECUTIVE_IDS];
export const TOTAL_QUESTIONS = ALL_QUESTION_IDS.length; /* 70 */
export const PART_B_IDS = ASRS_PART_B.map((q) => q.id);
/* Note pour chaque question, repliée (deck §7.4 : huit « Pourquoi cette question ? »).
null = pas de note ; la règle « ≤ 1 par écran » est respectée par construction. */
export const NOTES = {
asrs_5: 'note.asrs.adult.hyper',
asrs_6: 'note.asrs.adult.hyper',
asrs_4: 'note.asrs.avoidance',
ef_act_1: 'note.ef.activation',
ef_mem_1: 'note.ef.memory',
ef_emo_1: 'note.ef.emotion',
diva_ia_1: 'note.dsm.inattention',
diva_hi_1: 'note.dsm.impulsivity',
};
/* Suffixe « 6 derniers mois » du ruban, sur la première question à échelle de
fréquence seulement (deck §7.4 note 6) — la note s'affiche alors sous elle. */
export const SIX_MONTHS_NOTE_FIRST = 'asrs_1';
/* Clarté TDAH — moteur : persistance (contrat clarte:v1, gelé) et scoring
(port octet-identique du POC vanilla : seuils 2/2/2/3/3/3, dépistage à
≥ 4 cases grisées ; DSM 5/9 ; EF bandes 40/66 ; aucun score Partie B). */
import {
ASRS_PART_A, ASRS_PART_B, ASRS_INATTENTION_NUMS, DIVA_INATTENTION,
DIVA_HYPERACTIVITY, DIVA_CHILDHOOD, DIVA_IMPAIRMENT, EXEC_CLUSTERS,
TOTAL_QUESTIONS, PART_B_IDS,
} from './data';
export const STORAGE_KEY = 'clarte:v1';
export function storageAvailable() {
try {
const k = STORAGE_KEY + ':probe';
localStorage.setItem(k, '1');
localStorage.removeItem(k);
return true;
} catch (e) { return false; }
}
export function readState() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
const v = JSON.parse(raw);
if (!v || typeof v !== 'object' || typeof v.responses !== 'object' || v.responses === null) return null;
return v;
} catch (e) { return null; }
}
export function writeState(state) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
return true;
} catch (e) { return false; }
}
export function clearState() {
try { localStorage.removeItem(STORAGE_KEY); } catch (e) {}
}
export function answeredCount(responses) {
return Object.keys(responses || {}).filter((k) => PART_B_IDS.indexOf(k) === -1 || true).length;
}
/* ---------- scoring : port du POC, byte-identique ---------- */
export function scoreASRS(responses) {
let partAScore = 0, partAShaded = 0, partBScore = 0;
let inattention = 0, hyperactivity = 0;
const inattentionQs = ASRS_INATTENTION_NUMS;
ASRS_PART_A.forEach((q) => {
const val = responses[q.id] || 0;
partAScore += val;
if (val >= q.threshold) partAShaded++;
const num = parseInt(q.id.split('_')[1]);
if (inattentionQs.indexOf(num) !== -1) inattention += val;
else hyperactivity += val;
});
ASRS_PART_B.forEach((q) => {
const val = responses[q.id] || 0;
partBScore += val;
const num = parseInt(q.id.split('_')[1]);
if (inattentionQs.indexOf(num) !== -1) inattention += val;
else hyperactivity += val;
});
const screeningPositive = partAShaded >= 4;
return { partAScore, partAShaded, partBScore, inattention, hyperactivity, screeningPositive };
}
export function scoreDIVA(responses) {
let inattentionCount = 0, hyperactivityCount = 0, childhoodCount = 0;
const impairmentDomainKeys = [];
DIVA_INATTENTION.forEach((q) => { if (responses[q.id] === 1) inattentionCount++; });
DIVA_HYPERACTIVITY.forEach((q) => { if (responses[q.id] === 1) hyperactivityCount++; });
DIVA_CHILDHOOD.forEach((q) => { if (responses[q.id] === 1) childhoodCount++; });
DIVA_IMPAIRMENT.forEach((q) => { if (responses[q.id] === 1) impairmentDomainKeys.push(q.domainKey); });
const meetsInattention = inattentionCount >= 5;
const meetsHyperactivity = hyperactivityCount >= 5;
return {
inattentionCount, hyperactivityCount, childhoodCount,
childhoodPositive: childhoodCount >= 2,
impairmentDomainKeys,
meetsInattention, meetsHyperactivity,
presentation: meetsInattention && meetsHyperactivity ? 'combined'
: meetsInattention ? 'inattentive'
: meetsHyperactivity ? 'hyperactive' : 'below',
};
}
export function scoreExecutive(responses) {
const clusterScores = {};
let total = 0, maxTotal = 0;
EXEC_CLUSTERS.forEach((cluster) => {
let score = 0;
cluster.questions.forEach((q) => { score += responses[q.id] || 0; });
const max = cluster.questions.length * 3;
clusterScores[cluster.key] = { score, max, percentage: Math.round((score / max) * 100) };
total += score;
maxTotal += max;
});
const percentage = Math.round((total / maxTotal) * 100);
const severity = percentage >= 66 ? 'high' : percentage >= 40 ? 'moderate' : 'low';
const impaired = EXEC_CLUSTERS.filter((c) => clusterScores[c.key].percentage >= 50).map((c) => c.key);
return { clusterScores, total, maxTotal, percentage, severity, impaired };
}
export function calculateResults(responses) {
const asrs = scoreASRS(responses);
const diva = scoreDIVA(responses);
const executive = scoreExecutive(responses);
let positiveIndicators = 0;
if (asrs.screeningPositive) positiveIndicators++;
if (diva.meetsInattention || diva.meetsHyperactivity) positiveIndicators++;
if (executive.severity !== 'low') positiveIndicators++;
return { asrs, diva, executive, global: { positiveIndicators } };
}
/* ---------- Partie A grisée → porte de la Partie B (spec §4) ---------- */
export function partAShadedCount(responses) {
let shaded = 0;
ASRS_PART_A.forEach((q) => {
if ((responses[q.id] || 0) >= q.threshold) shaded++;
});
return shaded;
}
export function partBAnswered(responses) {
return PART_B_IDS.some((id) => responses[id] !== undefined);
}
/* ---------- périmètre (scope) : 70, ou 58 si Partie B passée ---------- */
export function scope(partBSkipped) {
return partBSkipped ? TOTAL_QUESTIONS - PART_B_IDS.length : TOTAL_QUESTIONS;
}
/* Clarté TDAH — dictionnaire anglaise (US). GÉNÉRÉ depuis copy-en.md
(parsing des tables du deck, typographie FR appliquée à la génération).
Toute correction de string se fait DANS LE DECK, puis on régénère. */
export const dict = {
"meta.title": "Clarté: Adult ADHD Self-Assessment",
"meta.description": "A structured self-assessment of attention-deficit/hyperactivity disorder (ADHD) in adults, built on validated tools: the ASRS v1.1 (WHO), the DSM-5 criteria, and executive functions. Free, private, with a PDF report at the end.",
"meta.og.title": "Clarté: Adult ADHD Self-Assessment",
"meta.tw.title": "Clarté: Adult ADHD Self-Assessment",
"meta.og.description": "Take stock in about 15 minutes, at your own pace, with validated tools: the ASRS v1.1 (WHO), the DSM-5 criteria, and executive functions. Free, private, PDF report at the end.",
"meta.tw.description": "Take stock in about 15 minutes, at your own pace, with validated tools. Free and private.",
"header.logo.h1": "Clarté",
"header.tagline": "French for \"clarity\"",
"header.subtitle": "Adult ADHD self-assessment",
"progress.label.home": "Home",
"progress.label.asrs": "ASRS (WHO)",
"progress.label.diva": "DSM-5 criteria",
"progress.label.executive": "Executive functions",
"progress.label.results": "Results",
"progress.answered": "{n} of {total} answered · {m} to go",
"home.h2": "Take stock of your attention",
"home.intro.p": "Here you complete a structured self-assessment of attention-deficit/hyperactivity disorder (ADHD) in adults, using research-validated tools. Nothing is sent anywhere: you answer, Clarté counts, and you leave with a document you can show a clinician if you'd like.",
"home.card.asrs.h4": "ASRS v1.1",
"home.card.asrs.p": "The reference screening tool: 18 questions validated by the World Health Organization (WHO).",
"home.card.dsm.h4": "DSM-5 criteria",
"home.card.dsm.p": "The 18 official DSM-5 criteria, asked one by one, including how you were as a child.",
"home.card.ef.h4": "Executive functions",
"home.card.ef.p": "The 6 domains of the Brown model: activation, focus, effort, emotion, memory, action. In plain terms: your executive functions are what let you plan, get started, and see it through to the end.",
"home.warning.h4": "Before you start",
"home.warning.li1": "This questionnaire is not a medical diagnosis.",
"home.warning.li2": "Only a qualified healthcare professional (a psychiatrist or neurologist) can diagnose ADHD.",
"home.warning.li3": "Your results are there to prepare the conversation with your clinician, not to replace it.",
"home.warning.li4": "Answer with your last 6 months in mind: not your worst week, and not your best.",
"home.warning.li5": "About 15 minutes, in several sittings if you need them: your progress is saved.",
"home.warning.li6": "Your answers stay on your device and are never sent anywhere.",
"home.consent.label": "I understand that this questionnaire is not a diagnosis, and that only a qualified healthcare professional can diagnose ADHD.",
"home.start.btn": "Start the self-assessment",
"asrs.h2": "ASRS v1.1 · Symptom Checklist",
"asrs.source": "World Health Organization (WHO)",
"asrs.info.p": "For each question, rate how you have felt and conducted yourself over the past 6 months.",
"asrs.partA.h3": "Part A · Screening",
"asrs.partA.hint": "These six questions are the strongest indicators of adult ADHD.",
"asrs.partB.h3": "Part B · Symptom Checklist (not scored)",
"asrs.partB.hint": "These twelve questions round out the picture. They carry no score: the official instrument assigns none.",
"asrs.q1": "How often do you have trouble wrapping up the final details of a project, once the challenging parts have been done?",
"asrs.q2": "How often do you have difficulty getting things in order when you have to do a task that requires organization?",
"asrs.q3": "How often do you have problems remembering appointments or obligations?",
"asrs.q4": "When you have a task that requires a lot of thought, how often do you avoid or delay getting started?",
"asrs.q5": "How often do you fidget or squirm with your hands or feet when you have to sit down for a long time?",
"asrs.q6": "How often do you feel overly active and compelled to do things, like you were driven by a motor?",
"asrs.q7": "How often do you make careless mistakes when you have to work on a boring or difficult project?",
"asrs.q8": "How often do you have difficulty keeping your attention when you are doing boring or repetitive work?",
"asrs.q9": "How often do you have difficulty concentrating on what people say to you, even when they are speaking to you directly?",
"asrs.q10": "How often do you misplace or have difficulty finding things at home or at work?",
"asrs.q11": "How often are you distracted by activity or noise around you?",
"asrs.q12": "How often do you leave your seat in meetings or other situations in which you are expected to remain seated?",
"asrs.q13": "How often do you feel restless or fidgety?",
"asrs.q14": "How often do you have difficulty unwinding and relaxing when you have time to yourself?",
"asrs.q15": "How often do you find yourself talking too much when you are in social situations?",
"asrs.q16": "When you're in a conversation, how often do you find yourself finishing the sentences of the people you are talking to, before they can finish them themselves?",
"asrs.q17": "How often do you have difficulty waiting your turn in situations when turn taking is required?",
"asrs.q18": "How often do you interrupt others when they are busy?",
"asrs.option.0": "Never",
"asrs.option.1": "Rarely",
"asrs.option.2": "Sometimes",
"asrs.option.3": "Often",
"asrs.option.4": "Very Often",
"asrs.next.btn": "Continue →",
"diva.h2": "DSM-5 criteria",
"diva.source": "American Psychiatric Association, DSM-5 (2013)",
"diva.info.p": "Answer Yes if the symptom has been present often and persistently for at least 6 months, and it gets in the way of your daily life.",
"diva.A1.h3": "Criterion A1 · Inattention",
"diva.A1.hint": "For adults, at least 5 of the 9 symptoms are required. Answer Yes if the symptom has been frequent for more than 6 months.",
"diva.ia_1": "Do you often fail to give close attention to details, or make careless mistakes in your work or other activities? (e.g. overlooking or missing details, inaccurate work)",
"diva.ia_2": "Do you often have difficulty sustaining attention in tasks or activities? (e.g. difficulty staying focused during meetings or lengthy reading)",
"diva.ia_3": "Do you often feel you are not really listening when someone speaks to you directly? (e.g. your mind is elsewhere, even with no obvious distraction)",
"diva.ia_4": "Do you often fail to follow through on instructions and to finish chores or duties at work?",
"diva.ia_5": "Do you often have difficulty organizing tasks and activities? (e.g. difficulty managing your time, meeting deadlines)",
"diva.ia_6": "Do you often avoid, dislike, or reluctantly take on tasks that require sustained mental effort?",
"diva.ia_7": "Do you often lose things you need for work or other activities? (e.g. keys, wallet, phone, paperwork)",
"diva.ia_8": "Are you often easily distracted by what is going on around you, or by unrelated thoughts?",
"diva.ia_9": "Do you often forget things in daily life? (e.g. returning a call, paying a bill, keeping an appointment)",
"diva.A2.h3": "Criterion A2 · Hyperactivity/Impulsivity",
"diva.hi_1": "Do you often fidget with your hands or feet, or squirm in your seat?",
"diva.hi_2": "Do you often leave your seat in situations where staying seated is expected?",
"diva.hi_3": "Do you often feel restless, pushed from the inside, unable to stay put?",
"diva.hi_4": "Do you often have difficulty settling down quietly during leisure activities?",
"diva.hi_5": "Are you often \"on the go\", acting as if \"driven by a motor\"?",
"diva.hi_6": "Do you often talk too much?",
"diva.hi_7": "Do you often blurt out an answer before the question has been fully asked?",
"diva.hi_8": "Do you often have difficulty waiting your turn?",
"diva.hi_9": "Do you often interrupt others, or intrude on what they are doing?",
"diva.B.h3": "Criterion B · Symptoms in childhood",
"diva.B.hint": "The DSM-5 looks for symptoms that were already there before age 12. No precise proof needed: your memories and your old school reports are enough.",
"diva.child_1": "Before age 12, did you have trouble staying attentive in class or with homework?",
"diva.child_2": "Before age 12, did people call you a dreamer, \"head in the clouds\"?",
"diva.child_3": "Before age 12, did you have trouble sitting still, or were you restless in class?",
"diva.child_4": "Before age 12, did your school reports often mention your attention or your behavior?",
"diva.child_5": "Before age 12, did you often lose your things or forget your homework?",
"diva.CD.h3": "Criteria C/D · Functional impairment",
"diva.CD.hint": "In plain terms: impairment is the footprint these difficulties leave on your life. The DSM-5 asks that they genuinely get in the way in at least two settings (home, work, the people close to you).",
"diva.imp_1": "Do these difficulties hold you back at work or in your studies?",
"diva.imp_2": "Do these difficulties weigh on your family or romantic relationships?",
"diva.imp_3": "Do these difficulties weigh on your friendships and your social life?",
"diva.imp_4": "Do these difficulties spoil your hobbies and your free time?",
"diva.imp_5": "Do these difficulties chip away at your self-esteem or your confidence?",
"diva.back.btn": "← Back",
"diva.next.btn": "Continue →",
"diva.binary.yes": "Yes",
"diva.binary.no": "No",
"diva.domain.work": "Work/Studies",
"diva.domain.family": "Family relationships",
"diva.domain.social": "Social relationships",
"diva.domain.leisure": "Leisure",
"diva.domain.selfesteem": "Self-esteem",
"executive.h2": "Executive functions",
"executive.source": "Inspired by the Brown model (2013)",
"executive.info.p": "Executive functions are the conductor: planning, staying focused, getting started, keeping your cool. Rate how often each difficulty gets in your way right now.",
"executive.cluster.activation.name": "Activation",
"executive.cluster.activation.desc": "Organizing tasks and materials, estimating time, getting started",
"executive.cluster.focus.name": "Focus",
"executive.cluster.focus.desc": "Finding, sustaining, and shifting attention as needed",
"executive.cluster.effort.name": "Effort",
"executive.cluster.effort.desc": "Regulating alertness, sustaining motivation and processing speed",
"executive.cluster.emotion.name": "Emotion",
"executive.cluster.emotion.desc": "Managing frustration and modulating feelings",
"executive.cluster.memory.name": "Memory",
"executive.cluster.memory.desc": "Using working memory and accessing recall",
"executive.cluster.action.name": "Action",
"executive.cluster.action.desc": "Monitoring and regulating physical activity",
"executive.ef_options.0": "Never",
"executive.ef_options.1": "Rarely",
"executive.ef_options.2": "Sometimes",
"executive.ef_options.3": "Often",
"executive.back.btn": "← Back",
"executive.next.btn": "See your results →",
"ef_act_1": "Difficulty organizing and planning tasks or projects",
"ef_act_2": "Difficulty setting priorities among several tasks",
"ef_act_3": "Difficulty starting a task, even one that really matters to you",
"ef_act_4": "A tendency to put things off until tomorrow, again and again",
"ef_foc_1": "Difficulty carrying a task through to the end",
"ef_foc_2": "Easily distracted by thoughts or noise unrelated to what you are doing",
"ef_foc_3": "Difficulty listening to and remembering what people tell you in a conversation",
"ef_foc_4": "Jumping from one activity to the next without finishing any",
"ef_eff_1": "A sudden slump or mental fog the moment a task turns boring",
"ef_eff_2": "Difficulty keeping your energy steady across the day",
"ef_eff_3": "Unusual slowness on routine tasks",
"ef_eff_4": "Needing intense stimulation to stay motivated",
"ef_emo_1": "Emotional reactions that overflow, out of proportion to what set them off",
"ef_emo_2": "Difficulty managing frustration and irritability",
"ef_emo_3": "Strong sensitivity to criticism, even offhand comments",
"ef_emo_4": "A mood that climbs and dips several times in a single day",
"ef_mem_1": "Forgetting what you just read, to the point of rereading it several times",
"ef_mem_2": "Difficulty holding on to an instruction given out loud",
"ef_mem_3": "Regularly forgetting an appointment or something you meant to do",
"ef_mem_4": "Losing the thread of your thoughts mid-sentence or mid-task",
"ef_act2_1": "Acting or speaking on impulse, without weighing the consequences",
"ef_act2_2": "Difficulty staying seated or keeping still",
"ef_act2_3": "Talking over people or taking up all the space in a conversation",
"ef_act2_4": "Difficulty holding yourself back (impulse buys, snacking, and the like)",
"results.warning.p": "Reminder: this is still a self-assessment, not a diagnosis. Only a qualified healthcare professional can diagnose ADHD.",
"results.summary.h2": "Summary",
"results.summary.text.high": "Of the 3 tools, {n} point in the same direction: symptoms worth taking seriously.",
"results.summary.text.empty": "Your summary will appear once all three tools have your answers.",
"results.summary.text.low": "Of the 3 tools, {n} point in the same direction. Nothing alarming stands out. If these difficulties weigh on your daily life, that alone is reason enough to talk to a clinician.",
"results.summary.bullet.asrs": "• ASRS (WHO): screening threshold met",
"results.summary.bullet.dsm": "• DSM-5 criteria: {presentation}",
"results.summary.bullet.ef": "• Executive functions: difficulties rated {level}",
"results.severity.low": "Low",
"results.severity.moderate": "Moderate",
"results.severity.high": "High",
"results.reco.high": "Next step: talk it over with a healthcare professional (a psychiatrist or neurologist). Only they can make a diagnosis; this report is there to support that conversation. Download it and bring it to your appointment.",
"results.reco.low": "If these difficulties weigh on your daily life all the same, talking to a clinician is still a good idea: other causes are possible (stress, sleep, anxiety).",
"results.card1.h3": "1. ASRS v1.1 · Screening (WHO)",
"results.card1.status": "Threshold {met | not met}: {x} of 6 key questions (at least 4 needed)",
"results.card1.label.partA": "Part A",
"results.card1.label.partB": "Part B · not scored",
"results.card1.label.total": "(delete this line)",
"results.card1.inattention": "Inattention: {n}/36 (informational only)",
"results.card1.hyperactivity": "Hyperactivity/Impulsivity: {n}/36 (informational only)",
"results.card2.h3": "2. DSM-5 criteria",
"results.card2.status": "{presentation}",
"results.card2.label.A1": "Inattention (A1) / Hyperactivity-Impulsivity (A2)",
"A2": "Inattention (A1) / Hyperactivity-Impulsivity (A2)",
"results.card2.label.B": "Childhood (B)",
"results.card2.label.impairment": "Impairment",
"results.card2.domains": "Domains affected: {list}",
"results.card3.h3": "3. Executive functions",
"results.card3.status": "Difficulty level: {level} ({p}%)",
"results.card3.domains": "Most affected domains: {list}",
"results.presentation.below": "Below clinical thresholds",
"results.presentation.combined": "Combined presentation (inattention + hyperactivity/impulsivity)",
"results.presentation.inattentive": "Predominantly inattentive presentation",
"results.presentation.hyperactive": "Predominantly hyperactive/impulsive presentation",
"results.download.btn": "Download your report (PDF)",
"results.download.hint": "The report carries your summary and your detailed answers, ready to share with a clinician if you choose.",
"results.restart.btn": "Restart the self-assessment",
"alert.incomplete": "Some questions are still unanswered. Answer these questions to continue.",
"asrs.interpretation.positive": "Your answers suggest symptoms highly consistent with ADHD in adults, worth discussing with a professional.",
"asrs.interpretation.negative": "The screening did not cross the threshold. A clinical evaluation is still worth considering if you have concerns.",
"pdf.title": "Adult ADHD Self-Assessment Report",
"pdf.filename": "adhd-self-assessment-report.pdf",
"pdf.generated": "Generated on {now}",
"pdf.warning.h": "WARNING",
"pdf.warning.p": "This document is a self-assessment tool and is not a medical diagnosis. Only a qualified healthcare professional can diagnose ADHD.",
"pdf.summary.h": "Summary of results",
"pdf.summary.text": "same two variants as `results.summary.text`",
"pdf.section1.h": "1. ASRS v1.1 (WHO)",
"pdf.section1.scores": "Part B and sub-scales: not scored (the official instrument assigns none) (recommended); otherwise \"Part B (informational only): {n}/48\"",
"pdf.section2.h": "2. DSM-5 criteria",
"pdf.section2.presentation": "Presentation: {presentation}",
"pdf.section2.counts": "Inattention: {n}/9 | Hyperactivity/Impulsivity: {n}/9",
"pdf.section2.childhood": "Childhood (Criterion B): {confirmed | not confirmed}",
"pdf.section2.impairment": "Impairment: {n} domain(s)",
"pdf.section3.h": "3. Executive functions",
"pdf.section3.level": "Overall level: {level} ({p}%)",
"pdf.annexe.h": "Appendix: detailed answers",
"pdf.unanswered": "Not answered",
"pdf.refs.h": "References",
"pdf.refs.items": "Kessler RC, et al. (2005). The World Health Organization Adult ADHD Self-Report Scale (ASRS). Psychological Medicine, 35(2), 245–256. • American Psychiatric Association (2013). Diagnostic and Statistical Manual of Mental Disorders (5th ed.). • Kooij JJS, et al. (2010). DIVA 2.0: Diagnostic Interview for ADHD in Adults. DIVA Foundation. • Brown TE (2013). A New Understanding of ADHD in Children and Adults: Executive Function Impairments. Routledge.",
"pdf.pagefooter": "Page {i}/{n} · This document is not a medical diagnosis",
"footer.disclaimer": "Disclaimer: This tool is provided for information only. It is not a medical diagnosis. Please consult a qualified healthcare professional.",
"footer.refs.h": "References:",
"footer.links": "ASRS v1.1 (Kessler et al., 2005) • DSM-5 (APA, 2013) • DIVA Foundation • Brown EF Model (2013)",
"footer.legal": "Source code • MIT License © 2025 PLNech",
"milestone.asrs.done": "First part done, well underway.",
"milestone.half": "You are halfway through.",
"milestone.dsm.done": "Two parts done, one to go.",
"milestone.all.done": "That's everything. Thank you for getting clear.",
"intro.asrs.h": "The screening",
"intro.asrs.p": "The ASRS is the adult ADHD self-assessment scale validated by the World Health Organization. Six questions first, the strongest indicators; then twelve complementary questions. For each one, think about how you have usually functioned over the past six months.",
"intro.dsm.h": "The official criteria",
"intro.dsm.p": "The DSM-5 is the reference professionals use. It looks at two families of symptoms, inattention and hyperactivity/impulsivity, and asks that they have existed since childhood and that they affect concrete daily life. The questions are binary: Yes or No.",
"intro.enfance.h": "Childhood memories",
"intro.enfance.p": "The DSM-5 looks for symptoms that were there before age 12. That is a criterion of the disorder, not an exam on your memory: approximate answers are normal, and the professional will explore this part with you.",
"intro.retentissement.h": "What it costs, concretely",
"intro.retentissement.p": "Everyone is distracted sometimes. What a diagnosis looks at is the impairment: does it genuinely cost you, at work, in your relationships, in your days? That is why these five questions exist.",
"intro.skip.btn": "Skip and continue",
"intro.continue.btn": "Continue →",
"bridge.a2.h": "Same questions, other face",
"bridge.a2.p": "Now for hyperactivity and impulsivity.",
"bridge.brown.h": "Why we ask it all again, differently",
"bridge.brown.p": "The official questions (DSM-5) describe visible symptoms. The questions that follow, inspired by the Brown model, describe the same daily life through its mechanism: what happens when you organize, start, remember, regulate. Answering both is not repeating yourself: it gives the professional two views of the same picture.",
"note.label": "Why this question?",
"note.asrs.adult.hyper": "In adults, hyperactivity is often internal: speeding thoughts, an inability to settle, rather than running around. That is why these questions ask about sensations more than movement.",
"note.asrs.avoidance": "Putting things off is not laziness: the ADHD brain struggles to switch on for an unstimulating task, even one that matters and is wanted.",
"note.ef.activation": "Starting an important task is a function in its own right. Its technical name is \"activation\"; its everyday name is \"not managing to get to it\".",
"note.ef.memory": "Working memory is the mental notepad: holding a number while you dial it, an instruction while you carry it out. Losing it has nothing to do with intelligence.",
"note.ef.emotion": "In the Brown model, emotion regulation is one of the executive functions, an originality of the model long absent from the official diagnostic criteria.",
"note.six.months": "The scales ask about your usual functioning over the past six months, not your worst weeks and not your best. Answer on the trend, not the episode.",
"note.dsm.inattention": "Inattention is not \"being distracted\": it is the difficulty keeping your attention where you decided to put it, even when you really want to.",
"note.dsm.impulsivity": "Impulsivity is the action that leaves before the decision does: answering too fast, interrupting, buying without thinking. It often shows more in conversations than in gestures.",
"gate.title": "Before the rest of the questionnaire",
"gate.body": "Your first six answers are the strongest indicators and already shape the final reading. The twelve questions that follow do not change that first signal: they refine your profile (inattention, restlessness) for the clinician who will read your report.",
"gate.answer.btn": "Answer the 12 questions (recommended)",
"gate.skip.btn": "Skip Part B",
"gate.reoffer.line": "Part B is still there if you want to come back to it.",
"recap.title": "Review your answers",
"recap.unanswered.chip": "not answered",
"recap.unanswered.line": "Choose an answer to continue.",
"recap.complete.btn": "See your results →",
"recap.missing.count": "{n} question(s) left.",
"recap.missing.go": "Go to the first unanswered question",
"recap.missing.skip": "Skip the remaining questions",
"recap.review.fromresults": "Review my answers",
"resume.count": "You had answered {n} of {total} questions.",
"resume.primary": "Resume",
"resume.secondary": "Start over",
"restart.confirm.body": "This will erase your {n} saved answers on this device.",
"restart.confirm.ok": "Continue",
"restart.confirm.cancel": "Cancel",
"home.storage.line": "Saved on this device only. Nothing is sent.",
"pdf.passed.item": "Not answered (part skipped)",
"results.partB.skipped": "Part B not answered",
"understand.title": "Understanding your profile",
"understand.asrs.p": "The ASRS spots a cluster of frequent signs, not a proof. Its screening threshold means your answers resemble those of adults who seek care for ADHD; it does not mean you have the disorder, or that it explains everything you live with. Sleep, stress, anxiety, depression: other causes can look like ADHD from the outside. Only a professional can sort that out.",
"understand.dsm.p": "The DSM-5 counts reported symptoms, in precise categories. Reaching a threshold means your difficulties resemble the criteria professionals use; not reaching it does not mean nothing is there. The criteria are a grid for conversation, not a photograph of how your mind works. The interpretation belongs to the professional you see.",
"understand.ef.p": "Executive functions describe daily life: starting, organizing, carrying through, absorbing, remembering. Marked difficulties here can light up what the official questions do not yet show, especially if they have lasted a long time and show up in several parts of your life. A professional will be able to say what, in this picture, points to ADHD and what points elsewhere.",
"consult.title": "For your appointment",
"consult.bring.p": "To bring: your PDF report.",
"consult.sentence.label": "A sentence to ask for the appointment",
"consult.sentence": "I completed an online ADHD self-assessment (ASRS, DSM-5 criteria, executive functions). Here is the report: I would like to discuss it.",
"consult.questions.h": "Questions worth asking",
"consult.q1": "Do my results point toward ADHD, or should something else be explored?",
"consult.q2": "If it is ADHD, what are the treatment options, and where do we start?",
"consult.q3": "How do I tell, in my daily life, what belongs to ADHD and what belongs to stress or fatigue?",
"glossary.link": "The vocabulary, in plain terms",
"glossary.title": "The vocabulary, in plain terms",
"glossary.tdah": "ADHD. Attention-deficit/hyperactivity disorder: a way of attending and acting, present since childhood, that makes daily life harder in several areas.",
"glossary.asrs": "ASRS. The adult ADHD self-assessment scale validated by the World Health Organization. Its job: screening, meaning spotting who deserves a professional's opinion.",
"glossary.dsm5": "DSM-5. The reference manual healthcare professionals use to make a diagnosis. Its criteria are grids for conversation, not verdicts.",
"glossary.inattention": "Inattention. The difficulty keeping your attention where you decided to put it: details slipping by, instructions lost, things misplaced. Not an intelligence flaw, not a willpower flaw.",
"glossary.hyperactivite": "Hyperactivity. In adults, often internal: restlessness, an inability to settle, speeding thoughts, more than running around.",
"glossary.impulsivite": "Impulsivity. The action that leaves before the decision does: answering too fast, interrupting, buying without thinking.",
"glossary.fonctions.executives": "Executive functions. The mind's conductor: planning, starting, holding course, managing emotions, remembering. The Brown model describes six domains.",
"glossary.memoire.travail": "Working memory. The mental notepad: holding information while you use it. Losing it has nothing to do with intelligence.",
"glossary.retentissement": "Impairment. The concrete footprint of the difficulties: work, relationships, days, self-esteem. The DSM's word for \"it costs something in real life\".",
"glossary.depistage": "Screening vs diagnosis. Screening spots a signal that justifies talking to a professional. A diagnosis names what the signal is. One happens online; the other never does."
};
\ No newline at end of file
/* Clarté TDAH — dictionnaire française. GÉNÉRÉ depuis copy-fr.md
(parsing des tables du deck, typographie FR appliquée à la génération).
Toute correction de string se fait DANS LE DECK, puis on régénère. */
export const dict = {
"meta.title": "Clarté : Auto-évaluation du TDAH chez l’adulte",
"meta.description": "Auto-évaluation du TDAH chez l’adulte, avec des outils validés : ASRS v1.1 (OMS), critères DSM-5, fonctions exécutives. Gratuit, confidentiel, rapport PDF à la fin.",
"meta.og.title": "Clarté : Auto-évaluation du TDAH chez l’adulte",
"meta.tw.title": "Clarté : Auto-évaluation du TDAH chez l’adulte",
"meta.og.description": "Faites le point en 15 minutes, à votre rythme, avec des outils validés : ASRS v1.1 (OMS), critères DSM-5, fonctions exécutives. Gratuit, confidentiel, rapport PDF à la fin.",
"meta.tw.description": "Faites le point en 15 minutes, à votre rythme, avec des outils validés. Gratuit et confidentiel.",
"header.logo.h1": "Clarté",
"header.subtitle": "Auto-évaluation du TDAH chez l’adulte",
"progress.label.home": "Accueil",
"progress.label.asrs": "ASRS (OMS)",
"progress.label.diva": "Critères DSM-5",
"progress.label.executive": "Fonctions exécutives",
"progress.label.results": "Résultats",
"home.h2": "Faites le point sur votre attention",
"home.intro.p": "Ici, vous passez une auto-évaluation structurée du trouble déficit de l’attention avec ou sans hyperactivité (TDAH) chez l’adulte, avec des outils validés par la recherche. Rien n’est envoyé nulle part : vous répondez, Clarté compte, et vous repartez avec un document à montrer à un médecin si vous le souhaitez.",
"home.card.asrs.h4": "ASRS v1.1",
"home.card.asrs.p": "L’outil de dépistage de référence : 18 questions validées par l’Organisation mondiale de la Santé (OMS).",
"home.card.dsm.h4": "Critères DSM-5",
"home.card.dsm.p": "Les 18 critères officiels du DSM-5, posés un par un, y compris ceux de votre enfance.",
"home.card.ef.h4": "Fonctions exécutives",
"home.card.ef.p": "6 domaines du modèle de Brown : organisation, attention, effort, émotions, mémoire, contrôle des impulsions. En clair : les fonctions exécutives, c’est ce qui vous permet de planifier, démarrer une tâche et tenir le cap jusqu’au bout.",
"home.warning.h4": "Avant de commencer",
"home.warning.li1": "Ce questionnaire n’est pas un diagnostic médical.",
"home.warning.li2": "Seul un professionnel de santé qualifié (psychiatre, neurologue) peut poser un diagnostic de TDAH.",
"home.warning.li3": "Vos résultats sont là pour préparer la discussion avec votre médecin, pas pour la remplacer.",
"home.warning.li4": "Répondez en pensant à vos 6 derniers mois : pas votre pire semaine, pas non plus votre meilleure.",
"home.warning.li5": "Environ 15 minutes, en plusieurs fois si besoin : votre progression est sauvegardée.",
"home.warning.li6": "Vos réponses restent sur votre appareil et ne sont jamais transmises.",
"home.consent.label": "J’ai compris que ce questionnaire n’est pas un diagnostic, et que seul un professionnel de santé peut poser un diagnostic de TDAH.",
"home.start.btn": "Commencer l’évaluation",
"asrs.h2": "ASRS v1.1 · Grille d’auto-évaluation",
"asrs.source": "Organisation mondiale de la Santé",
"asrs.info.p": "Pour chaque question, indiquez à quelle fréquence vous vous êtes senti(e) ou avez agi de cette façon au cours des six derniers mois.",
"asrs.partA.h3": "Partie A · Dépistage",
"asrs.partA.hint": "Ces six questions sont les plus révélatrices du TDAH chez l’adulte.",
"asrs.partB.h3": "Partie B · Pour affiner (non notée)",
"asrs.partB.hint": "Ces douze questions complètent le portrait. Elles ne donnent pas lieu à un score : l’instrument officiel n’en attribue pas.",
"asrs.q1": "À quelle fréquence avez-vous de la difficulté à finaliser un projet, une fois que le gros du travail a été effectué ?",
"asrs.q2": "À quelle fréquence avez-vous de la difficulté à établir les priorités lorsque vous devez effectuer une tâche qui requiert de l’organisation ?",
"asrs.q3": "À quelle fréquence avez-vous de la difficulté à vous souvenir de vos rendez-vous ou de vos obligations sociales ?",
"asrs.q4": "À quelle fréquence repoussez-vous ou évitez-vous les tâches qui nécessitent beaucoup de concentration ?",
"asrs.q5": "À quelle fréquence ressentez-vous la bougeotte motrice de vos mains ou vos pieds lorsque vous devez rester dans une position assise pour une longue période ?",
"asrs.q6": "À quelle fréquence vous sentez-vous hyperactif ou contraint lorsque vous devez accomplir des tâches, comme une dynamo ?",
"asrs.q7": "À quelle fréquence, lorsque vous devez travailler sur un projet long et ennuyant, faites-vous des fautes d’inattention ?",
"asrs.q8": "À quelle fréquence, lorsque vous devez effectuer une tâche ennuyeuse et répétitive, avez-vous de la difficulté à rester concentré sur ce que vous faites ?",
"asrs.q9": "À quelle fréquence avez-vous de la difficulté à rester concentré sur ce que les gens vous disent, même lorsqu’ils vous parlent directement ?",
"asrs.q10": "À quelle fréquence, à la maison ou au travail, placez-vous des objets au mauvais endroit ou avez-vous de la difficulté à les retrouver ?",
"asrs.q11": "À quelle fréquence êtes-vous distrait par les activités et les bruits qui vous entourent ?",
"asrs.q12": "À quelle fréquence devez-vous quitter des réunions ou d’autres situations au cours desquelles vous devez rester assis ?",
"asrs.q13": "À quelle fréquence vous sentez-vous nerveux ou agité ?",
"asrs.q14": "À quelle fréquence, lorsque vous avez du temps pour vous, avez-vous de la difficulté à décompresser ou à relaxer ?",
"asrs.q15": "À quelle fréquence monopolisez-vous les gens lorsque vous vous retrouvez dans des rencontres sociales ?",
"asrs.q16": "À quelle fréquence, lorsque vous conversez avec des gens, avez-vous tendance à terminer leurs phrases avant qu’ils ne puissent le faire ?",
"asrs.q17": "À quelle fréquence avez-vous de la difficulté à attendre votre tour lorsque vous vous retrouvez dans une situation nécessitant d’intervenir à tour de rôle ?",
"asrs.q18": "À quelle fréquence dérangez-vous les autres lorsqu’ils sont occupés ?",
"asrs.option.0": "Jamais",
"asrs.option.1": "Rarement",
"asrs.option.2": "Parfois",
"asrs.option.3": "Souvent",
"asrs.option.4": "Très souvent",
"asrs.next.btn": "Continuer →",
"diva.h2": "Critères DSM-5",
"diva.source": "American Psychiatric Association, DSM-5 (2013)",
"diva.info.p": "Répondez Oui si le symptôme est présent souvent et durablement depuis au moins 6 mois, et qu’il gêne concrètement votre quotidien.",
"diva.A1.h3": "Critère A1 · Inattention",
"diva.A1.hint": "Chez l’adulte, il faut au moins 5 symptômes sur 9. Répondez Oui si le symptôme est fréquent depuis plus de 6 mois.",
"diva.ia_1": "Avez-vous souvent du mal à prêter attention aux détails, ou faites-vous des fautes d’étourderie dans votre travail ou d’autres activités ? (ex. : négliger des détails, travail inexact)",
"diva.ia_2": "Avez-vous souvent du mal à soutenir votre attention sur des tâches ou des activités ? (ex. : difficulté à rester concentré pendant les réunions, les lectures)",
"diva.ia_3": "Avez-vous souvent l’impression de ne pas écouter quand on vous parle directement ? (ex. : l’esprit ailleurs, même sans distraction évidente)",
"diva.ia_4": "Avez-vous souvent du mal à suivre les consignes jusqu’au bout et à mener à terme vos tâches, qu’elles soient ménagères ou professionnelles ?",
"diva.ia_5": "Avez-vous souvent du mal à organiser vos tâches et activités ? (ex. : difficulté à gérer le temps, à respecter les délais)",
"diva.ia_6": "Évitez-vous souvent, ou faites-vous à contre-cœur, les tâches qui nécessitent un effort mental soutenu ?",
"diva.ia_7": "Perdez-vous souvent des objets nécessaires à votre travail ou à vos activités ? (ex. : clés, portefeuille, téléphone, documents)",
"diva.ia_8": "Vous laissez-vous souvent facilement distraire par ce qui vous entoure, ou par des pensées sans rapport (« passer du coq à l’âne ») ?",
"diva.ia_9": "Oubliez-vous fréquemment des choses dans la vie quotidienne ? (ex. : rappeler quelqu’un, payer une facture, honorer un rendez-vous)",
"diva.A2.h3": "Critère A2 · Hyperactivité / Impulsivité",
"diva.hi_1": "Remuez-vous souvent les mains ou les pieds, ou vous tortillez-vous sur votre siège ?",
"diva.hi_2": "Quittez-vous souvent votre siège dans des situations où vous êtes censé(e) rester assis(e) ?",
"diva.hi_3": "Vous sentez-vous souvent agité(e), comme poussé(e) de l’intérieur, au point de ne pas tenir en place ?",
"diva.hi_4": "Avez-vous souvent du mal à vous tenir tranquille dans vos loisirs ou vos activités calmes ?",
"diva.hi_5": "Êtes-vous souvent « sur la brèche », ou agissez-vous comme si vous étiez « monté(e) sur ressorts » ?",
"diva.hi_6": "Vous arrive-t-il souvent de parler trop ?",
"diva.hi_7": "Laissez-vous souvent échapper la réponse avant que la question ne soit entièrement posée ?",
"diva.hi_8": "Avez-vous souvent du mal à attendre votre tour ?",
"diva.hi_9": "Interrompez-vous souvent les autres, ou vous imposez-vous dans leur espace ?",
"diva.B.h3": "Critère B · Symptômes dans l’enfance",
"diva.B.hint": "Le DSM-5 demande que des symptômes aient été présents avant l’âge de 12 ans. Pas besoin de preuves précises : vos souvenirs et vos anciens bulletins suffisent.",
"diva.child_1": "Avant l’âge de 12 ans, aviez-vous du mal à rester attentif(ve) en classe ou pour vos devoirs ?",
"diva.child_2": "Avant l’âge de 12 ans, vous disait-on rêveur, « dans la lune » ?",
"diva.child_3": "Avant l’âge de 12 ans, aviez-vous du mal à tenir en place ou étiez-vous agité(e) en classe ?",
"diva.child_4": "Avant l’âge de 12 ans, vos bulletins scolaires mentionnaient-ils souvent l’attention ou le comportement ?",
"diva.child_5": "Avant l’âge de 12 ans, perdiez-vous souvent vos affaires ou oubliiez-vous vos devoirs ?",
"diva.CD.h3": "Critères C/D · Retentissement fonctionnel",
"diva.CD.hint": "En clair : le retentissement, c’est la trace que ces difficultés laissent dans votre vie. Le DSM demande qu’elles gênent concrètement dans au moins deux domaines (maison, travail, proches…).",
"diva.imp_1": "Ces difficultés vous pénalisent-elles dans votre travail ou vos études ?",
"diva.imp_2": "Ces difficultés vous pèsent-elles dans vos relations familiales ou amoureuses ?",
"diva.imp_3": "Ces difficultés vous pèsent-elles dans vos amitiés et votre vie sociale ?",
"diva.imp_4": "Ces difficultés gâchent-elles vos loisirs et votre temps libre ?",
"diva.imp_5": "Ces difficultés entament-elles votre estime de vous ou votre confiance ?",
"diva.back.btn": "← Retour",
"diva.next.btn": "Continuer →",
"diva.binary.yes": "Oui",
"diva.binary.no": "Non",
"executive.h2": "Fonctions exécutives",
"executive.source": "Inspiré du modèle de Brown (2013)",
"executive.info.p": "Les fonctions exécutives, c’est le chef d’orchestre : planifier, rester concentré, démarrer, garder son sang-froid. Indiquez à quelle fréquence chaque difficulté vous gêne actuellement.",
"executive.cluster.activation.name": "Activation",
"executive.cluster.activation.desc": "Organisation, priorisation, mise en route",
"executive.cluster.focus.name": "Attention / Concentration",
"executive.cluster.focus.desc": "Attention soutenue, résistance aux distractions",
"executive.cluster.effort.name": "Effort / Énergie",
"executive.cluster.effort.desc": "Régulation de l’éveil et de la vigilance",
"executive.cluster.emotion.name": "Régulation émotionnelle",
"executive.cluster.emotion.desc": "Gestion de la frustration et des émotions",
"executive.cluster.memory.name": "Mémoire de travail",
"executive.cluster.memory.desc": "Garder une information en tête le temps de l’utiliser",
"executive.cluster.action.name": "Action / Autorégulation",
"executive.cluster.action.desc": "Contrôle des impulsions, frein intérieur",
"executive.ef_options.0": "Jamais",
"executive.ef_options.1": "Rarement",
"executive.ef_options.2": "Parfois",
"executive.ef_options.3": "Souvent",
"ef_act_1": "Difficulté à organiser et planifier des tâches ou des projets",
"ef_act_2": "Difficulté à établir des priorités entre plusieurs tâches",
"ef_act_3": "Difficulté à démarrer une tâche, même quand elle compte vraiment pour vous",
"ef_act_4": "Tendance à remettre au lendemain, encore et encore",
"ef_foc_1": "Difficulté à tenir une tâche jusqu’au bout",
"ef_foc_2": "Facilement distrait(e) par des pensées ou des bruits sans rapport avec ce que vous faites",
"ef_foc_3": "Difficulté à écouter et à retenir ce qu’on vous dit dans une conversation",
"ef_foc_4": "Passer d’une activité à l’autre sans en finir aucune",
"ef_eff_1": "Coup de barre ou fatigue mentale dès qu’une tâche est ennuyeuse",
"ef_eff_2": "Difficulté à garder une énergie stable sur la journée",
"ef_eff_3": "Lenteur excessive dans les tâches routinières",
"ef_eff_4": "Besoin d’une stimulation intense pour rester motivé(e)",
"ef_emo_1": "Réactions émotionnelles qui débordent, disproportionnées par rapport à ce qui les déclenche",
"ef_emo_2": "Difficulté à gérer la frustration et l’irritabilité",
"ef_emo_3": "Sensibilité forte aux critiques, même anodines",
"ef_emo_4": "Humeur qui monte et descend plusieurs fois dans la journée",
"ef_mem_1": "Oublier ce que vous venez de lire, au point de devoir relire plusieurs fois",
"ef_mem_2": "Difficulté à retenir une consigne donnée à l’oral",
"ef_mem_3": "Oublier régulièrement un rendez-vous ou une chose prévue",
"ef_mem_4": "Perdre le fil de ses pensées en pleine phrase ou en pleine action",
"ef_act2_1": "Agir ou parler sur l’impulsion, sans mesurer les conséquences",
"ef_act2_2": "Difficulté à rester assis(e) ou à vous tenir tranquille",
"ef_act2_3": "Couper la parole ou occuper tout l’espace dans les conversations",
"ef_act2_4": "Difficulté à vous retenir (achats impulsifs, grignotage, etc.)",
"results.warning.p": "Rappel : ceci reste une auto-évaluation, pas un diagnostic. Seul un professionnel de santé qualifié peut poser un diagnostic de TDAH.",
"results.summary.h2": "Synthèse",
"results.summary.text.empty": "La synthèse apparaîtra dès que les trois outils auront vos réponses.",
"results.summary.text.high": "Sur les 3 outils, {n} pointent dans la même direction : des symptômes qui méritent d’être pris au sérieux.",
"results.summary.text.low": "Sur les 3 outils, {n} pointe(nt) dans la même direction. Rien d’alarmant ne ressort. Si ces difficultés vous pèsent au quotidien, votre ressenti suffit à en parler à un médecin.",
"asrs.interpretation.positive": "Vos réponses suggèrent des symptômes très évocateurs du TDAH chez l’adulte, à discuter avec un professionnel de santé.",
"asrs.interpretation.negative": "Le dépistage ne passe pas le seuil. Une évaluation reste utile si ces difficultés vous préoccupent.",
"results.summary.bullet.asrs": "• ASRS (OMS) : seuil de dépistage atteint",
"results.summary.bullet.dsm": "• Critères DSM-5 : {présentation}",
"results.summary.bullet.ef": "• Fonctions exécutives : difficultés de niveau {niveau}",
"results.reco.high": "Prochaine étape : parlez-en à un professionnel de santé (psychiatre ou neurologue). C’est à cette personne de poser, ou non, un diagnostic ; ce rapport est là pour appuyer la conversation. Téléchargez-le et apportez-le à votre rendez-vous.",
"results.reco.low": "Si, malgré tout, ces difficultés pèsent sur votre quotidien, en parler à un médecin reste une bonne idée : d’autres causes sont possibles (stress, sommeil, anxiété).",
"results.card1.h3": "1. ASRS v1.1 · Dépistage (OMS)",
"results.card1.status": "Seuil {atteint | non atteint} : {x}/6 questions clés (il en faut au moins 4)",
"results.card1.label.partA": "Partie A",
"results.card1.label.partB": "Partie B · non notée",
"results.card1.label.total": "(supprimer cette ligne)",
"results.card1.inattention": "Inattention : {n}/36 (indicatif)",
"results.card1.hyperactivity": "Hyperactivité/Impulsivité : {n}/36 (indicatif)",
"results.card2.h3": "2. Critères DSM-5",
"results.card2.status": "{présentation}",
"results.card2.label.A1": "Inattention (A1) / Hyperactivité-Impulsivité (A2)",
"A2": "Inattention (A1) / Hyperactivité-Impulsivité (A2)",
"results.card2.label.B": "Enfance (B)",
"results.card2.label.impairment": "Retentissement",
"results.card2.domains": "Domaines concernés : {liste}",
"results.card3.h3": "3. Fonctions exécutives",
"results.card3.status": "Niveau de difficulté : {niveau} ({p} %)",
"results.card3.domains": "Domaines les plus touchés : {liste}",
"results.presentation.below": "Sous les seuils cliniques",
"results.presentation.combined": "Présentation combinée (inattention + hyperactivité/impulsivité)",
"results.presentation.inattentive": "Présentation inattentive prédominante",
"results.presentation.hyperactive": "Présentation hyperactive/impulsive prédominante",
"results.download.btn": "Télécharger le compte rendu (PDF)",
"results.download.hint": "Le document reprend votre synthèse et vos réponses détaillées, prêt à montrer au médecin.",
"results.restart.btn": "Recommencer l’évaluation",
"alert.incomplete": "Il reste des questions sans réponse. Répondez à ces questions pour continuer.",
"pdf.title": "Compte rendu d’auto-évaluation du TDAH chez l’adulte",
"pdf.filename": "compte-rendu-tdah.pdf",
"pdf.generated": "Généré le {now}",
"pdf.warning.h": "AVERTISSEMENT",
"pdf.warning.p": "Ce document est un outil d’auto-évaluation et ne constitue pas un diagnostic médical. Seul un professionnel de santé qualifié peut poser un diagnostic de TDAH.",
"pdf.summary.h": "Synthèse des résultats",
"pdf.summary.text": "mêmes deux variantes que `results.summary.text`",
"pdf.section1.h": "1. ASRS v1.1 (OMS)",
"pdf.section1.scores": "Partie B et sous-échelles : non notées (l’instrument officiel n’en attribue pas) (recommandé) ; sinon « Partie B (indicatif) : {n}/48 »",
"pdf.section2.h": "2. Critères DSM-5",
"pdf.section2.childhood": "Enfance (critère B) : {confirmé | non confirmé}",
"pdf.section3.h": "3. Fonctions exécutives",
"pdf.annexe.h": "Annexe : réponses détaillées",
"pdf.unanswered": "Non répondu",
"pdf.refs.h": "Références",
"pdf.pagefooter": "Page {i}/{n} · Ce document ne constitue pas un diagnostic médical",
"footer.disclaimer": "Avertissement : cet outil est fourni à titre informatif uniquement. Il ne constitue pas un diagnostic médical. En cas de doute, adressez-vous à un professionnel de santé qualifié.",
"footer.refs.h": "Références :",
"footer.links": "ASRS v1.1 (Kessler et al., 2005) • DSM-5 (APA, 2013) • DIVA Foundation • Modèle des fonctions exécutives de Brown (2013)",
"footer.legal": "Code source • Licence MIT © 2025 PLNech",
"progress.answered": "{n} question(s) répondue(s) · il en reste {m}",
"milestone.asrs.done": "Première partie terminée, bien avancé.",
"milestone.half": "Vous êtes à la moitié.",
"milestone.dsm.done": "Deux parties terminées, reste la dernière.",
"milestone.all.done": "C’est complet. Merci d’avoir été au clair.",
"intro.asrs.h": "Le dépistage",
"intro.asrs.p": "L’ASRS est l’échelle d’auto-évaluation validée par l’Organisation mondiale de la Santé. Six questions d’abord, les plus révélatrices ; puis douze questions complémentaires. Pour chacune, pensez à votre fonctionnement habituel des six derniers mois.",
"intro.dsm.h": "Les critères officiels",
"intro.dsm.p": "Le DSM-5 est la référence des professionnels. Il regarde deux familles de symptômes, l’inattention et l’hyperactivité/impulsivité, et demande qu’elles aient existé dès l’enfance et qu’elles retentissent sur la vie concrète. Les questions sont binaires : Oui ou Non.",
"intro.enfance.h": "Le souvenir de l’enfance",
"intro.enfance.p": "Le DSM-5 demande des symptômes présents avant 12 ans. C’est un critère du trouble, pas un examen de vos souvenirs : des réponses approximatives sont normales, le professionnel creusera cette partie avec vous.",
"intro.retentissement.h": "Ce que ça coûte, concrètement",
"intro.retentissement.p": "Tout le monde est distrait parfois. Ce que le diagnostic regarde, c’est le retentissement : est-ce que ça coûte, concrètement, dans votre travail, vos relations, vos journées ? C’est pour cela que ces cinq questions existent.",
"intro.skip.btn": "Passer et continuer",
"intro.continue.btn": "Continuer →",
"bridge.a2.h": "Mêmes questions, autre visage",
"bridge.a2.p": "Place à l’hyperactivité et à l’impulsivité.",
"bridge.brown.h": "Pourquoi on redit tout ça autrement",
"bridge.brown.p": "Les questions officielles (DSM-5) décrivent des symptômes visibles. Les questions qui suivent, inspirées du modèle de Brown, décrivent la même vie quotidienne par son mécanisme : ce qui vous arrive quand vous organisez, démarrez, retenez, régulez. Répondre aux deux n’est pas se répéter : c’est donner au professionnel deux vues du même tableau.",
"note.label": "Pourquoi cette question ?",
"note.asrs.adult.hyper": "Chez l’adulte, l’hyperactivité est souvent intérieure : pensées qui s’accélèrent, impossibilité de se poser, plutôt que de courir partout. C’est pourquoi ces questions parlent de sensations plus que de mouvement.",
"note.asrs.avoidance": "Remettre à plus tard n’est pas de la paresse : le cerveau TDAH peine à s’activer sur une tâche peu stimulante, même importante et même voulue.",
"note.ef.activation": "Démarrer une tâche importante est une fonction à part entière. Son nom technique est « activation » ; son nom de tous les jours est « ne pas arriver à s’y mettre ».",
"note.ef.memory": "La mémoire de travail est le bloc-notes mental : garder un numéro le temps de le composer, une consigne le temps de l’exécuter. La perdre n’a rien à voir avec l’intelligence.",
"note.ef.emotion": "La régulation émotionnelle fait partie des fonctions exécutives dans le modèle de Brown, une originalité du modèle longtemps absente des critères diagnostiques officiels.",
"note.six.months": "Les échelles demandent votre fonctionnement habituel des six derniers mois, pas vos pires semaines ni vos meilleures. Répondez sur la tendance, pas sur l’épisode.",
"note.dsm.inattention": "L’inattention, ce n’est pas « être distrait » : c’est la difficulté à maintenir son attention là où on a décidé de la mettre, même quand on le veut vraiment.",
"note.dsm.impulsivity": "L’impulsivité, c’est l’action qui part avant la décision : répondre trop vite, interrompre, acheter sans réfléchir. Elle se voit souvent plus dans les conversations que dans les gestes.",
"gate.title": "Avant la suite du questionnaire",
"gate.body": "Vos six premières réponses sont les plus révélatrices et orientent déjà l’interprétation finale. Les douze questions suivantes ne changent pas ce premier signal : elles précisent votre profil (inattention, agitation) pour le médecin qui lira votre rapport.",
"gate.answer.btn": "Répondre aux 12 questions (recommandé)",
"gate.skip.btn": "Passer la partie B",
"gate.reoffer.line": "La partie B reste disponible, si vous voulez la reprendre.",
"recap.title": "Récapitulatif",
"recap.unanswered.chip": "non répondu",
"recap.unanswered.line": "Choisissez une réponse pour continuer.",
"recap.complete.btn": "Voir mes résultats →",
"recap.missing.count": "Il reste {n} question(s).",
"recap.missing.go": "Aller à la première question manquante",
"recap.missing.skip": "Passer les questions restantes",
"recap.review.fromresults": "Réviser mes réponses",
"resume.count": "Vous aviez répondu à {n} question(s).",
"resume.primary": "Reprendre",
"resume.secondary": "Recommencer",
"restart.confirm.body": "Cela effacera vos {n} réponses enregistrées sur cet appareil.",
"restart.confirm.ok": "Continuer",
"restart.confirm.cancel": "Annuler",
"home.storage.line": "Sauvegardé uniquement sur cet appareil. Rien n’est envoyé.",
"pdf.passed.item": "Non renseignée (partie passée)",
"results.partB.skipped": "Partie B non renseignée",
"understand.title": "Comprendre votre profil",
"understand.asrs.p": "L’ASRS repère un faisceau de signes fréquents, pas une preuve. Son seuil de dépistage dit que vos réponses ressemblent à celles d’adultes qui consultent pour un TDAH ; il ne dit pas que vous avez ce trouble, ni qu’il explique tout ce que vous vivez. Sommeil, stress, anxiété, dépression : d’autres causes ressemblent au TDAH de l’extérieur. Seul un professionnel peut faire la part des choses.",
"understand.dsm.p": "Le DSM-5 compte des symptômes déclarés, dans des catégories précises. Atteindre un seuil signifie que vos difficultés ressemblent aux critères qu’utilisent les professionnels ; ne pas l’atteindre ne signifie pas que rien n’existe. Les critères sont une grille de dialogue, pas une photographie de votre fonctionnement. L’interprétation revient au professionnel qui vous reçoit.",
"understand.ef.p": "Les fonctions exécutives décrivent le quotidien : démarrer, organiser, tenir, encaisser, retenir. Des difficultés marquées ici peuvent éclairer ce que les questions officielles ne montrent pas encore, surtout si elles durent depuis longtemps et dans plusieurs domaines de vie. Un professionnel saura dire ce qui, dans ce portrait, relève d’un TDAH ou d’autre chose.",
"consult.title": "Pour la consultation",
"consult.bring.p": "À apporter : votre compte rendu PDF.",
"consult.sentence.label": "Une phrase pour demander rendez-vous",
"consult.sentence": "J’ai fait une auto-évaluation TDAH en ligne (ASRS, critères DSM-5, fonctions exécutives). Voici le rapport : je voudrais en discuter.",
"consult.questions.h": "Des questions à poser",
"consult.q1": "Mes résultats vont-ils dans le sens d’un TDAH, ou faut-il explorer autre chose ?",
"consult.q2": "Si c’est un TDAH, quelles sont les options de prise en charge, et par où commencer ?",
"consult.q3": "Comment reconnaître, dans mon quotidien, ce qui relève du TDAH et ce qui relève du stress ou de la fatigue ?",
"glossary.link": "Le vocabulaire, en clair",
"glossary.title": "Le vocabulaire, en clair",
"glossary.tdah": "TDAH. Un fonctionnement de l’attention et des impulsions, présent depuis l’enfance, qui complique la vie quotidienne dans plusieurs domaines. Nom complet : trouble déficit de l’attention avec ou sans hyperactivité.",
"glossary.asrs": "ASRS. L’échelle d’auto-évaluation du TDAH adulte validée par l’Organisation mondiale de la Santé. Son rôle : dépister, c’est-à-dire repérer qui mérite un avis professionnel.",
"glossary.dsm5": "DSM-5. Le manuel de référence des professionnels de santé pour poser un diagnostic. Ses critères sont des grilles de dialogue, pas des verdicts.",
"glossary.inattention": "Inattention. La difficulté à garder son attention là où on l’a décidée : détails qui échappent, consignes perdues, objets égarés. Ni un défaut d’intelligence, ni un défaut de volonté.",
"glossary.hyperactivite": "Hyperactivité. Chez l’adulte, souvent intérieure : agitation, impossibilité de se poser, pensées qui s’accélèrent, plus que courir partout.",
"glossary.impulsivite": "Impulsivité. L’action qui part avant la décision : répondre trop vite, couper la parole, acheter sans réfléchir.",
"glossary.fonctions.executives": "Fonctions exécutives. Le chef d’orchestre mental : planifier, démarrer, tenir le cap, gérer les émotions, retenir. Le modèle de Brown les décrit en six domaines.",
"glossary.memoire.travail": "Mémoire de travail. Le bloc-notes mental : garder une information le temps de l’utiliser. La perdre n’a rien à voir avec l’intelligence.",
"glossary.retentissement": "Retentissement. La trace concrète des difficultés : travail, relations, journées, estime de soi. Le mot du DSM pour dire que ça coûte dans la vraie vie.",
"glossary.depistage": "Dépistage vs diagnostic. Le dépistage repère un signal qui justifie d’en parler à un professionnel. Le diagnostic pose un nom sur ce signal. L’un se fait en ligne ; l’autre, jamais.",
"diva.domain.work": "Travail/Études",
"diva.domain.family": "Relations familiales",
"diva.domain.social": "Relations sociales",
"diva.domain.leisure": "Loisirs",
"diva.domain.selfesteem": "Estime de soi",
"results.severity.low": "Faible",
"results.severity.moderate": "Modéré",
"results.severity.high": "Élevé",
"pdf.section2.presentation": "Présentation : {presentation}",
"pdf.section2.counts": "Inattention : {n}/9 · Hyperactivité/Impulsivité : {n}/9",
"pdf.section2.impairment": "Retentissement : {n} domaine(s)",
"pdf.section3.level": "Niveau global : {level} ({p} %)",
"executive.back.btn": "← Retour",
"executive.next.btn": "Voir les résultats →"
};
\ No newline at end of file
/* Clarté TDAH — compte rendu PDF (deck §9) : structure du POC conservée
(titre + avertissement, synthèse, cartes par instrument, annexe réponses
détaillées, références, pied de page non-diagnostique sur chaque page). */
import { ASRS_PART_A, ASRS_PART_B, DIVA_INATTENTION, DIVA_HYPERACTIVITY, DIVA_CHILDHOOD, DIVA_IMPAIRMENT, EXEC_CLUSTERS } from './data';
export async function buildReport(t, responses, r, lang) {
let JsPDF;
try { ({ jsPDF: JsPDF } = await import('jspdf')); }
catch (e) { console.error('jspdf unavailable', e); return; }
const doc = new JsPDF({ unit: 'pt', format: 'a4' });
const W = doc.internal.pageSize.getWidth();
const H = doc.internal.pageSize.getHeight();
let y = 50;
const page = () => 1;
function ensure(h) {
if (y + h > H - 50) { footer(); doc.addPage(); y = 50; }
}
function footer() {
const i = doc.internal.getNumberOfPages();
doc.setFontSize(8); doc.setTextColor(130);
doc.text(t['pdf.pagefooter'].replace('{i}', String(i)).replace('{n}', String(i)), 50, H - 30);
}
function text(s, size = 10, bold = false, x = 50) {
doc.setFont('helvetica', bold ? 'bold' : 'normal');
doc.setFontSize(size);
const lines = doc.splitTextToSize(s, W - x - 50);
lines.forEach((line) => { ensure(14); doc.text(line, x, y); y += 14; });
}
text(t['pdf.title'], 16, true); y += 6;
text(t['pdf.warning.p'], 9); y += 10;
text(t['pdf.summary.h'], 13, true);
text(t['pdf.summary.text'] === undefined ? '' : synthese(t, r), 10); y += 10;
text(t['pdf.section1.h'], 12, true);
text(t['results.card1.status'].replace('{atteint|non atteint}', r.asrs.screeningPositive ? 'atteint' : 'non atteint').replace('{x}', String(r.asrs.partAShaded)), 10);
text(t['pdf.section1.scores'], 9); y += 8;
text(t['pdf.section2.h'], 12, true);
text((t['pdf.section2.presentation'] || 'Présentation : {presentation}').replace('{presentation}', t['results.presentation.' + r.diva.presentation]), 10);
DIVA_CHILDHOOD.forEach(() => {});
text((t['pdf.section2.childhood'] || 'Enfance (critère B) : {c}').replace('{confirmé | non confirmé}', r.diva.childhoodPositive ? 'confirmé' : 'non confirmé').replace('{confirmed | not confirmed}', r.diva.childhoodPositive ? 'confirmed' : 'not confirmed'), 10);
text((t['pdf.section2.impairment'] || 'Retentissement : {n} domaine(s)').replace('{n}', String(r.diva.impairmentDomainKeys.length)), 10);
y += 8;
text(t['pdf.section3.h'], 12, true);
text((t['pdf.section3.level'] || 'Niveau global : {level} ({p}%)').replace('{level}', t['results.severity.' + r.executive.severity]).replace('{p}', String(r.executive.percentage)), 10);
y += 8;
text(t['pdf.annexe.h'], 12, true);
const all = [
...ASRS_PART_A.map((q) => [q.id, q.t, 'asrs.option.' + (responses[q.id] !== undefined ? responses[q.id] : null)]),
...ASRS_PART_B.map((q) => [q.id, q.t, 'asrs.option.' + (responses[q.id] !== undefined ? responses[q.id] : null)]),
...DIVA_INATTENTION, ...DIVA_HYPERACTIVITY, ...DIVA_CHILDHOOD, ...DIVA_IMPAIRMENT,
].map((x) => (Array.isArray(x) ? x : [x.id, x.t, null]));
EXEC_CLUSTERS.forEach((c) => c.questions.forEach((q) => all.push([q.id, q.t, null])));
all.forEach(([id, tkey, optKey]) => {
const v = responses[id];
const answer = v === undefined
? t['pdf.unanswered']
: optKey && optKey.endsWith('null') ? String(v)
: v === 1 && DIVA_INATTENTION.concat(DIVA_HYPERACTIVITY, DIVA_CHILDHOOD, DIVA_IMPAIRMENT).some((q) => q.id === id) ? t['diva.binary.yes']
: v === 0 && DIVA_INATTENTION.concat(DIVA_HYPERACTIVITY, DIVA_CHILDHOOD, DIVA_IMPAIRMENT).some((q) => q.id === id) ? t['diva.binary.no']
: t['asrs.option.' + v] || t['executive.ef_options.' + v] || String(v);
text('• ' + t[tkey] + ' — ' + answer, 9);
});
text(t['pdf.refs.h'], 12, true);
text(t['pdf.refs.items'] || 'Kessler RC, et al. (2005). Psychological Medicine, 35(2), 245–256. • American Psychiatric Association (2013). DSM-5. • Kooij JJS, et al. (2010). DIVA 2.0. • Brown TE (2013). A New Understanding of ADHD. Routledge.', 8);
footer();
doc.save(lang === 'en' ? 'adhd-self-assessment-report.pdf' : 'compte-rendu-tdah.pdf');
function synthese(t, r) {
const n = r.global.positiveIndicators;
const base = n === 0 ? t['results.summary.text.empty'] : n >= 2 ? t['results.summary.text.high'] : t['results.summary.text.low'];
return base.replace(/\{n\}/g, String(n));
}
}
/* Clarté TDAH — app shell. Les tokens vivent globalement (styles/clarte-tokens.css). */
.root { max-width: 40rem; margin: 0 auto; padding: var(--sp-4) var(--sp-3) var(--sp-6); font-family: var(--font-clarte); background: var(--paper); color: var(--ink); line-height: 1.65; }
.root h1 { font-size: var(--fs-h1); line-height: 1.3; }
.root h1 .tagline { display: block; font-size: var(--fs-small); font-weight: 400; color: var(--ink-faint); }
.root h2 { font-size: 1.25rem; margin: var(--sp-4) 0 var(--sp-2); }
.root p { max-width: var(--measure-prose); }
.muted { color: var(--ink-faint); font-size: var(--fs-small); }
.cards { display: grid; gap: var(--sp-2); margin: var(--sp-3) 0; }
.card { background: #fff; border: 1px solid var(--line-strong); border-radius: var(--radius); padding: var(--sp-3); }
.card h4 { margin: 0 0 var(--sp-1); font-size: var(--fs-body); }
.card p { margin: 0; font-size: var(--fs-small); color: var(--ink); }
.warning { background: #fff; border: 1px solid var(--line-strong); border-radius: var(--radius); padding: var(--sp-3); margin: var(--sp-3) 0; }
.warning h4 { margin: 0 0 var(--sp-2); font-size: var(--fs-body); }
.warning ul { margin: 0; padding-left: 1.1rem; }
.warning li { margin: var(--sp-1) 0; font-size: var(--fs-small); }
.btn { border: 1px solid var(--line-strong); background: #fff; border-radius: var(--radius); padding: .6rem 1.1rem; font: inherit; color: var(--ink); cursor: pointer; }
.btn:hover { border-color: var(--teal); }
.btn:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.btn:disabled { background: var(--paper); color: var(--ink-faint); border-color: var(--line); cursor: default; }
.primaryBtn { background: var(--teal); border-color: var(--teal); color: #fff; font-weight: 700; }
.primaryBtn:hover { background: var(--teal-hover); }
.primaryBtn:disabled { background: var(--teal); color: #fff; opacity: 1; border-color: var(--line-strong); }
.linkbtn { background: none; border: none; color: var(--teal-text); text-decoration: underline; cursor: pointer; font: inherit; padding: 0; }
.linkbtn:focus-visible { outline: 2px solid var(--teal); outline-offset: 2px; }
.nav { display: flex; justify-content: space-between; align-items: baseline; gap: var(--sp-2); margin-top: var(--sp-4); flex-wrap: wrap; }
.consent { margin: var(--sp-4) 0; }
.consent label { display: flex; gap: .6rem; align-items: baseline; font-size: var(--fs-small); }
.consent button { margin-top: var(--sp-3); }
.resume { margin: var(--sp-4) 0; padding: var(--sp-3); background: var(--teal-soft); border-radius: var(--radius); }
.quizCard h2:focus { outline: 2px solid var(--teal); outline-offset: 4px; border-radius: 4px; }
.note { margin-top: var(--sp-3); }
.note summary { color: var(--ink-faint); font-size: var(--fs-small); cursor: pointer; }
.note p { font-size: var(--fs-small); color: var(--ink-faint); }
.interstitial { max-width: 34rem; margin: var(--sp-6) auto; }
.gateActions { display: flex; gap: var(--sp-2); margin: var(--sp-3) 0; flex-wrap: wrap; }
.recapGroup h3 { font-size: var(--fs-body); margin: var(--sp-3) 0 var(--sp-1); color: var(--teal-text); }
.recapGroup ul { list-style: none; padding: 0; margin: 0; }
.recapRow { width: 100%; display: flex; justify-content: space-between; gap: var(--sp-2); background: none; border: none; border-bottom: 1px solid var(--line); padding: var(--sp-1) 0; font: inherit; text-align: left; cursor: pointer; }
.recapRow:hover { border-bottom-color: var(--teal); }
.recapText { font-size: var(--fs-small); }
.recapChip { font-size: var(--fs-caption); color: var(--teal-text); white-space: nowrap; }
.recapChipEmpty { font-size: var(--fs-caption); color: var(--ink-faint); white-space: nowrap; }
.summaryText { font-size: var(--fs-question); }
.warningInline strong { color: var(--danger); }
.consultRow { display: flex; gap: var(--sp-2); align-items: baseline; flex-wrap: wrap; }
.glossEntry { margin: var(--sp-3) 0; }
.glossEntry dt { font-weight: 700; }
.glossEntry dd { margin: 0; font-size: var(--fs-small); color: var(--ink); }
@media (max-width: 480px) {
.root { padding: var(--sp-3) var(--sp-2) var(--sp-5); }
}
...@@ -28,6 +28,7 @@ ...@@ -28,6 +28,7 @@
"framer-motion": "^13.1.1", "framer-motion": "^13.1.1",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
"hydra-synth": "^1.3.29", "hydra-synth": "^1.3.29",
"jspdf": "^4.2.1",
"marked": "^15.0.12", "marked": "^15.0.12",
"next": "^15.3.0", "next": "^15.3.0",
"p5": "1.11.3", "p5": "1.11.3",
......
/**
* Clarté - ADHD Self-Evaluation App
* Client-side questionnaire logic and PDF generation
*/
// ============================================
// QUESTIONNAIRE DATA
// ============================================
const ASRS_QUESTIONS = {
part_a: [
{ id: 'asrs_1', text: "À quelle fréquence avez-vous des difficultés à finaliser les derniers détails d'un projet, une fois que les parties les plus stimulantes ont été réalisées ?", threshold: 2 },
{ id: 'asrs_2', text: "À quelle fréquence avez-vous des difficultés à mettre de l'ordre lorsque vous devez effectuer une tâche qui requiert de l'organisation ?", threshold: 2 },
{ id: 'asrs_3', text: "À quelle fréquence avez-vous des difficultés à vous souvenir de vos rendez-vous ou de vos obligations ?", threshold: 2 },
{ id: 'asrs_4', text: "Lorsque vous devez effectuer une tâche qui demande beaucoup de réflexion, à quelle fréquence évitez-vous ou retardez-vous le moment de commencer ?", threshold: 3 },
{ id: 'asrs_5', text: "À quelle fréquence remuez-vous ou tortillez-vous les mains ou les pieds lorsque vous devez rester assis(e) pendant une longue période ?", threshold: 3 },
{ id: 'asrs_6', text: "À quelle fréquence vous sentez-vous excessivement actif(ve) et poussé(e) à faire des choses, comme si vous étiez mû(e) par un moteur ?", threshold: 3 },
],
part_b: [
{ id: 'asrs_7', text: "À quelle fréquence faites-vous des erreurs d'inattention lorsque vous devez travailler sur un projet ennuyeux ou difficile ?" },
{ id: 'asrs_8', text: "À quelle fréquence avez-vous des difficultés à maintenir votre attention lorsque vous effectuez un travail ennuyeux ou répétitif ?" },
{ id: 'asrs_9', text: "À quelle fréquence avez-vous des difficultés à vous concentrer sur ce que les gens vous disent, même lorsqu'ils s'adressent directement à vous ?" },
{ id: 'asrs_10', text: "À quelle fréquence égarez-vous ou avez-vous du mal à retrouver des objets à la maison ou au travail ?" },
{ id: 'asrs_11', text: "À quelle fréquence êtes-vous distrait(e) par l'activité ou le bruit autour de vous ?" },
{ id: 'asrs_12', text: "À quelle fréquence quittez-vous votre siège lors de réunions ou dans d'autres situations où vous êtes censé(e) rester assis(e) ?" },
{ id: 'asrs_13', text: "À quelle fréquence vous sentez-vous agité(e) ou remuant(e) ?" },
{ id: 'asrs_14', text: "À quelle fréquence avez-vous des difficultés à vous détendre et à vous relaxer lorsque vous avez du temps libre ?" },
{ id: 'asrs_15', text: "À quelle fréquence vous trouvez-vous à parler trop en situation sociale ?" },
{ id: 'asrs_16', text: "Lors d'une conversation, à quelle fréquence finissez-vous les phrases des personnes avec qui vous parlez avant qu'elles ne puissent les terminer elles-mêmes ?" },
{ id: 'asrs_17', text: "À quelle fréquence avez-vous des difficultés à attendre votre tour dans des situations qui l'exigent ?" },
{ id: 'asrs_18', text: "À quelle fréquence interrompez-vous les autres lorsqu'ils sont occupés ?" },
]
};
const ASRS_OPTIONS = [
{ value: 0, label: 'Jamais' },
{ value: 1, label: 'Rarement' },
{ value: 2, label: 'Parfois' },
{ value: 3, label: 'Souvent' },
{ value: 4, label: 'Très souvent' }
];
const DIVA_INATTENTION = [
{ id: 'diva_ia_1', text: "Avez-vous souvent du mal à prêter attention aux détails ou faites-vous des erreurs d'inattention dans votre travail ou d'autres activités ? (ex: négliger des détails, travail inexact)" },
{ id: 'diva_ia_2', text: "Avez-vous souvent du mal à maintenir votre attention sur des tâches ou des activités ? (ex: difficulté à rester concentré pendant les réunions, lectures)" },
{ id: 'diva_ia_3', text: "Avez-vous souvent l'impression de ne pas écouter quand on vous parle directement ? (ex: esprit ailleurs, même sans distraction évidente)" },
{ id: 'diva_ia_4', text: "Avez-vous souvent du mal à suivre les instructions jusqu'au bout et à terminer vos tâches professionnelles ou obligations ?" },
{ id: 'diva_ia_5', text: "Avez-vous souvent du mal à organiser vos tâches et activités ? (ex: difficulté à gérer le temps, respecter les délais)" },
{ id: 'diva_ia_6', text: "Évitez-vous souvent, ou êtes-vous réticent(e) à vous engager dans des tâches qui nécessitent un effort mental soutenu ?" },
{ id: 'diva_ia_7', text: "Perdez-vous souvent des objets nécessaires à vos activités ? (ex: clés, portefeuille, téléphone, documents)" },
{ id: 'diva_ia_8', text: "Êtes-vous souvent facilement distrait(e) par des stimuli externes ou des pensées sans rapport ?" },
{ id: 'diva_ia_9', text: "Êtes-vous souvent oublieux(se) dans la vie quotidienne ? (ex: oublier de rappeler, payer des factures, honorer des rendez-vous)" },
];
const DIVA_HYPERACTIVITY = [
{ id: 'diva_hi_1', text: "Remuez-vous souvent les mains ou les pieds, ou vous tortillez-vous sur votre siège ?" },
{ id: 'diva_hi_2', text: "Quittez-vous souvent votre siège dans des situations où vous êtes censé(e) rester assis(e) ?" },
{ id: 'diva_hi_3', text: "Vous sentez-vous souvent agité(e) ou avez-vous du mal à rester en place ?" },
{ id: 'diva_hi_4', text: "Avez-vous souvent du mal à vous adonner à des loisirs ou activités calmes de manière tranquille ?" },
{ id: 'diva_hi_5', text: "Êtes-vous souvent « sur la brèche » ou agissez-vous comme si vous étiez « monté(e) sur ressorts » ?" },
{ id: 'diva_hi_6', text: "Parlez-vous souvent trop ?" },
{ id: 'diva_hi_7', text: "Laissez-vous souvent échapper une réponse avant qu'une question ne soit terminée ?" },
{ id: 'diva_hi_8', text: "Avez-vous souvent du mal à attendre votre tour ?" },
{ id: 'diva_hi_9', text: "Interrompez-vous souvent les autres ou vous imposez-vous ?" },
];
const DIVA_CHILDHOOD = [
{ id: 'diva_child_1', text: "Avant l'âge de 12 ans, aviez-vous des difficultés à rester attentif(ve) à l'école ou lors des devoirs ?" },
{ id: 'diva_child_2', text: "Avant l'âge de 12 ans, étiez-vous considéré(e) comme un enfant rêveur, « dans la lune » ?" },
{ id: 'diva_child_3', text: "Avant l'âge de 12 ans, aviez-vous du mal à rester assis(e) ou étiez-vous agité(e) à l'école ?" },
{ id: 'diva_child_4', text: "Avant l'âge de 12 ans, aviez-vous souvent des remarques sur vos bulletins concernant l'attention ou le comportement ?" },
{ id: 'diva_child_5', text: "Avant l'âge de 12 ans, perdiez-vous souvent vos affaires scolaires ou oubliiez-vous de faire vos devoirs ?" },
];
const DIVA_IMPAIRMENT = [
{ id: 'diva_imp_1', text: "Ces difficultés ont-elles un impact négatif sur votre travail ou vos études ?", domain: "Travail/Études" },
{ id: 'diva_imp_2', text: "Ces difficultés ont-elles un impact négatif sur vos relations familiales ou amoureuses ?", domain: "Relations familiales" },
{ id: 'diva_imp_3', text: "Ces difficultés ont-elles un impact négatif sur vos relations sociales et amicales ?", domain: "Relations sociales" },
{ id: 'diva_imp_4', text: "Ces difficultés ont-elles un impact négatif sur vos loisirs et activités de temps libre ?", domain: "Loisirs" },
{ id: 'diva_imp_5', text: "Ces difficultés ont-elles un impact négatif sur votre estime de soi ou votre confiance en vous ?", domain: "Estime de soi" },
];
const EXEC_FUNCTIONS = {
activation: {
name: "Activation",
description: "Organisation, priorisation, démarrage",
questions: [
{ id: 'ef_act_1', text: "Difficulté à organiser et planifier des tâches ou projets" },
{ id: 'ef_act_2', text: "Difficulté à établir des priorités entre différentes tâches" },
{ id: 'ef_act_3', text: "Difficulté à commencer une tâche, même quand elle est importante" },
{ id: 'ef_act_4', text: "Tendance à procrastiner, reporter les tâches au lendemain" },
]
},
focus: {
name: "Attention / Focus",
description: "Attention soutenue, résistance à la distraction",
questions: [
{ id: 'ef_foc_1', text: "Difficulté à maintenir l'attention sur une tâche jusqu'à son achèvement" },
{ id: 'ef_foc_2', text: "Facilement distrait(e) par des pensées ou stimuli non pertinents" },
{ id: 'ef_foc_3', text: "Difficulté à écouter et retenir ce qui est dit lors de conversations" },
{ id: 'ef_foc_4', text: "Passer d'une activité à l'autre sans en terminer aucune" },
]
},
effort: {
name: "Effort / Énergie",
description: "Régulation de l'éveil et de la vigilance",
questions: [
{ id: 'ef_eff_1', text: "Somnolence ou fatigue mentale lors de tâches peu stimulantes" },
{ id: 'ef_eff_2', text: "Difficulté à maintenir un niveau d'énergie constant tout au long de la journée" },
{ id: 'ef_eff_3', text: "Lenteur excessive pour accomplir des tâches routinières" },
{ id: 'ef_eff_4', text: "Besoin de stimulation intense pour rester motivé(e)" },
]
},
emotion: {
name: "Régulation émotionnelle",
description: "Gestion de la frustration et des affects",
questions: [
{ id: 'ef_emo_1', text: "Réactions émotionnelles excessives ou disproportionnées" },
{ id: 'ef_emo_2', text: "Difficulté à gérer la frustration et l'irritabilité" },
{ id: 'ef_emo_3', text: "Sensibilité excessive aux critiques ou aux commentaires négatifs" },
{ id: 'ef_emo_4', text: "Fluctuations d'humeur fréquentes au cours de la journée" },
]
},
memory: {
name: "Mémoire de travail",
description: "Mémoire active et rappel",
questions: [
{ id: 'ef_mem_1', text: "Oublier ce que l'on vient de lire et devoir relire plusieurs fois" },
{ id: 'ef_mem_2', text: "Difficulté à se souvenir des consignes ou instructions données verbalement" },
{ id: 'ef_mem_3', text: "Oublier fréquemment des rendez-vous ou des tâches prévues" },
{ id: 'ef_mem_4', text: "Perdre le fil de ses pensées en milieu de phrase ou d'action" },
]
},
action: {
name: "Inhibition / Action",
description: "Contrôle des impulsions et auto-régulation",
questions: [
{ id: 'ef_act2_1', text: "Agir ou parler impulsivement sans réfléchir aux conséquences" },
{ id: 'ef_act2_2', text: "Difficulté à rester assis(e) ou à se tenir tranquille" },
{ id: 'ef_act2_3', text: "Interrompre les autres ou s'imposer dans les conversations" },
{ id: 'ef_act2_4', text: "Difficulté à contrôler ses impulsions (achats, alimentation, etc.)" },
]
}
};
const EF_OPTIONS = [
{ value: 0, label: 'Pas un problème' },
{ value: 1, label: 'Rarement' },
{ value: 2, label: 'Parfois' },
{ value: 3, label: 'Souvent' }
];
// ============================================
// ANSWER PROGRESS (counter, fill line, milestones)
// Derived from the question lists above — totals are never hardcoded.
// ============================================
const ASRS_IDS = [...ASRS_QUESTIONS.part_a, ...ASRS_QUESTIONS.part_b].map(q => q.id);
const DIVA_IDS = [
...DIVA_INATTENTION, ...DIVA_HYPERACTIVITY,
...DIVA_CHILDHOOD, ...DIVA_IMPAIRMENT
].map(q => q.id);
const EXECUTIVE_IDS = Object.values(EXEC_FUNCTIONS).flatMap(c => c.questions.map(q => q.id));
const ALL_QUESTION_IDS = [...ASRS_IDS, ...DIVA_IDS, ...EXECUTIVE_IDS];
const TOTAL_QUESTIONS = ALL_QUESTION_IDS.length; // 70 = 18 ASRS + 28 DSM-5 + 24 fonctions exécutives
// Milestone boundaries: end of the ASRS block, half of the total,
// end of the DSM-5 block, end of everything. Copy finals from
// copy-fr.md §12 (progress.answered, milestones). If this tree is ever
// translated, copy-en.md §12 gives: "{n} of {total} answered · {m} to go",
// "First part done, well underway.", "You are halfway through.",
// "Two parts done, one to go.", "That's everything. Thank you for getting clear."
const PROGRESS_COPY = {
countPlural: (n, m) => `${n} questions répondues · il en reste ${m}`,
countSingular: (n, m) => `${n} question répondue · il en reste ${m}`,
countDone: (n) => `${n} questions répondues`,
milestones: {
'asrs.done': 'Première partie terminée, bien avancé.',
'half': 'Vous êtes à la moitié.',
'dsm.done': 'Deux parties terminées, reste la dernière.',
'all.done': "C'est complet. Merci d'avoir été au clair."
}
};
const MILESTONES = [
{ key: 'asrs.done', at: ASRS_IDS.length },
{ key: 'half', at: Math.floor(TOTAL_QUESTIONS / 2) },
{ key: 'dsm.done', at: ASRS_IDS.length + DIVA_IDS.length },
{ key: 'all.done', at: TOTAL_QUESTIONS }
];
function formatCountLine(answered, total) {
const remaining = total - answered;
if (remaining === 0) return PROGRESS_COPY.countDone(answered);
if (answered <= 1) return PROGRESS_COPY.countSingular(answered, remaining);
return PROGRESS_COPY.countPlural(answered, remaining);
}
// Highest milestone whose boundary the answered count has crossed, or null.
function crossedMilestone(answered) {
let hit = null;
for (const m of MILESTONES) {
if (answered >= m.at) hit = m;
else break;
}
return hit;
}
// Refresh the sticky answer-progress band: count line, 3px fill, and a
// milestone in the count line's slot when a boundary is just crossed
// (shown for the current screen, then the count line returns). The last
// milestone shown is persisted so a reload does not re-fire it.
function updateAnswerProgress() {
const band = document.getElementById('answer-progress');
const countEl = document.getElementById('answer-count');
const fill = document.getElementById('answer-fill');
if (!band || !countEl || !fill) return;
const visible = state.currentSection === 'asrs'
|| state.currentSection === 'diva'
|| state.currentSection === 'executive';
band.hidden = !visible;
if (!visible) return;
const answered = ALL_QUESTION_IDS.filter(id => state.responses[id] !== undefined).length;
fill.style.width = (answered / TOTAL_QUESTIONS * 100) + '%';
const milestone = crossedMilestone(answered);
if (milestone && milestone.key !== state.lastMilestone) {
countEl.textContent = PROGRESS_COPY.milestones[milestone.key];
state.lastMilestone = milestone.key;
saveState();
} else {
countEl.textContent = formatCountLine(answered, TOTAL_QUESTIONS);
}
}
// ============================================
// APP STATE
// ============================================
const state = {
currentSection: 'home',
responses: {},
results: null,
lastMilestone: null
};
// ============================================
// PERSISTENCE (localStorage shim)
// Survive reloads: answers and current section are saved locally,
// restored before first render. Silent no-op in private mode.
// ============================================
const STORAGE_KEY = 'clarte:v1';
function saveState() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({
responses: state.responses,
currentSection: state.currentSection,
lastMilestone: state.lastMilestone
}));
} catch (e) { /* storage unavailable (private mode, quota): skip */ }
}
function clearSavedState() {
try {
localStorage.removeItem(STORAGE_KEY);
} catch (e) { /* storage unavailable: nothing to clear */ }
}
// Merge saved answers into state before first render, so question
// rendering (which reads state.responses) shows restored answers.
// Returns the saved section to resume at, or null.
function restoreState() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
const saved = JSON.parse(raw);
if (saved && typeof saved === 'object') {
if (saved.responses && typeof saved.responses === 'object') {
Object.assign(state.responses, saved.responses);
}
if (typeof saved.lastMilestone === 'string') {
state.lastMilestone = saved.lastMilestone;
}
if (sections.includes(saved.currentSection)) {
return saved.currentSection;
}
}
} catch (e) { /* corrupt or unavailable storage: start fresh */ }
return null;
}
// ============================================
// NAVIGATION
// ============================================
const sections = ['home', 'asrs', 'diva', 'executive', 'results'];
function showSection(sectionId) {
sections.forEach(s => {
const el = document.getElementById(s);
if (el) el.classList.remove('active');
});
const target = document.getElementById(sectionId);
if (target) {
target.classList.add('active');
state.currentSection = sectionId;
updateProgress();
updateAnswerProgress();
window.scrollTo(0, 0);
saveState();
}
}
function updateProgress() {
const currentIndex = sections.indexOf(state.currentSection);
const dots = document.querySelectorAll('.step-dot');
const connectors = document.querySelectorAll('.step-connector');
const label = document.querySelector('.progress-label');
dots.forEach((dot, i) => {
dot.classList.remove('active', 'completed');
if (i < currentIndex) dot.classList.add('completed');
if (i === currentIndex) dot.classList.add('active');
});
connectors.forEach((conn, i) => {
conn.classList.remove('completed');
if (i < currentIndex) conn.classList.add('completed');
});
const labels = ['Accueil', 'ASRS (OMS)', 'Critères DSM-5', 'Fonctions Exécutives', 'Résultats'];
if (label) label.textContent = labels[currentIndex] || '';
}
// ============================================
// RENDER QUESTIONS
// ============================================
function renderASRS() {
const container = document.getElementById('asrs-questions');
let html = '';
// Part A
html += `<div class="question-group">
<h3>Partie A — Dépistage</h3>
<p class="hint">Ces 6 questions sont les plus prédictives du TDAH.</p>`;
ASRS_QUESTIONS.part_a.forEach((q, i) => {
html += renderQuestion(q, i + 1, ASRS_OPTIONS);
});
html += '</div>';
// Part B
html += `<div class="question-group">
<h3>Partie B — Questions complémentaires</h3>`;
ASRS_QUESTIONS.part_b.forEach((q, i) => {
html += renderQuestion(q, i + 7, ASRS_OPTIONS);
});
html += '</div>';
container.innerHTML = html;
attachOptionHandlers(container);
}
function renderDIVA() {
const container = document.getElementById('diva-questions');
let html = '';
// Inattention
html += `<div class="question-group">
<h3>Critère A1 — Inattention</h3>
<p class="hint">Pour les adultes, ≥5 symptômes sont requis. Répondez Oui si le symptôme est fréquent depuis plus de 6 mois.</p>`;
DIVA_INATTENTION.forEach((q, i) => {
html += renderBinaryQuestion(q, i + 1);
});
html += '</div>';
// Hyperactivity
html += `<div class="question-group">
<h3>Critère A2 — Hyperactivité / Impulsivité</h3>`;
DIVA_HYPERACTIVITY.forEach((q, i) => {
html += renderBinaryQuestion(q, i + 1);
});
html += '</div>';
// Childhood
html += `<div class="question-group">
<h3>Critère B — Symptômes dans l'enfance</h3>
<p class="hint">Le DSM-5 exige que des symptômes aient été présents avant l'âge de 12 ans.</p>`;
DIVA_CHILDHOOD.forEach((q, i) => {
html += renderBinaryQuestion(q, i + 1);
});
html += '</div>';
// Impairment
html += `<div class="question-group">
<h3>Critères C/D — Retentissement fonctionnel</h3>
<p class="hint">Les symptômes doivent causer une gêne significative dans au moins 2 domaines.</p>`;
DIVA_IMPAIRMENT.forEach((q, i) => {
html += renderBinaryQuestion(q, i + 1);
});
html += '</div>';
container.innerHTML = html;
attachOptionHandlers(container);
}
function renderExecutive() {
const container = document.getElementById('executive-questions');
let html = '';
Object.values(EXEC_FUNCTIONS).forEach(cluster => {
html += `<div class="question-group">
<h3>${cluster.name}</h3>
<p class="hint">${cluster.description}</p>`;
cluster.questions.forEach((q, i) => {
html += renderQuestion(q, i + 1, EF_OPTIONS, true);
});
html += '</div>';
});
container.innerHTML = html;
attachOptionHandlers(container);
}
function renderQuestion(q, num, options, compact = false) {
const answered = state.responses[q.id] !== undefined;
return `
<div class="question ${answered ? 'answered' : ''}" data-id="${q.id}">
<p class="question-text"><span class="question-number">${num}.</span> ${q.text}</p>
<div class="options">
${options.map(opt => `
<div class="option">
<input type="radio" name="${q.id}" id="${q.id}_${opt.value}" value="${opt.value}"
${state.responses[q.id] === opt.value ? 'checked' : ''}>
<label for="${q.id}_${opt.value}">${opt.label}</label>
</div>
`).join('')}
</div>
</div>
`;
}
function renderBinaryQuestion(q, num) {
const answered = state.responses[q.id] !== undefined;
return `
<div class="question ${answered ? 'answered' : ''}" data-id="${q.id}">
<p class="question-text"><span class="question-number">${num}.</span> ${q.text}</p>
<div class="options binary">
<div class="option yes">
<input type="radio" name="${q.id}" id="${q.id}_1" value="1"
${state.responses[q.id] === 1 ? 'checked' : ''}>
<label for="${q.id}_1">Oui</label>
</div>
<div class="option no">
<input type="radio" name="${q.id}" id="${q.id}_0" value="0"
${state.responses[q.id] === 0 ? 'checked' : ''}>
<label for="${q.id}_0">Non</label>
</div>
</div>
</div>
`;
}
function attachOptionHandlers(container) {
container.querySelectorAll('input[type="radio"]').forEach(input => {
input.addEventListener('change', (e) => {
state.responses[e.target.name] = parseInt(e.target.value);
saveState();
updateAnswerProgress();
const question = e.target.closest('.question');
if (question) question.classList.add('answered');
});
});
}
// ============================================
// SCORING
// ============================================
function calculateResults() {
const results = {
asrs: scoreASRS(),
diva: scoreDIVA(),
executive: scoreExecutive()
};
// Global assessment
let positiveIndicators = 0;
if (results.asrs.screeningPositive) positiveIndicators++;
if (results.diva.meetsInattention || results.diva.meetsHyperactivity) positiveIndicators++;
if (results.executive.severity !== 'Faible') positiveIndicators++;
results.global = {
positiveIndicators,
confidence: positiveIndicators >= 3 ? 'Élevée' : positiveIndicators >= 2 ? 'Modérée' : 'Faible'
};
state.results = results;
return results;
}
function scoreASRS() {
let partAScore = 0, partAShaded = 0, partBScore = 0;
let inattention = 0, hyperactivity = 0;
const inattentionQs = [1, 2, 3, 4, 7, 8, 9, 10, 11];
ASRS_QUESTIONS.part_a.forEach(q => {
const val = state.responses[q.id] || 0;
partAScore += val;
if (val >= q.threshold) partAShaded++;
const num = parseInt(q.id.split('_')[1]);
if (inattentionQs.includes(num)) inattention += val;
else hyperactivity += val;
});
ASRS_QUESTIONS.part_b.forEach(q => {
const val = state.responses[q.id] || 0;
partBScore += val;
const num = parseInt(q.id.split('_')[1]);
if (inattentionQs.includes(num)) inattention += val;
else hyperactivity += val;
});
const screeningPositive = partAShaded >= 4;
return {
partAScore,
partAShaded,
partBScore,
total: partAScore + partBScore,
inattention,
hyperactivity,
screeningPositive,
interpretation: screeningPositive
? "Le dépistage est POSITIF. Vos réponses suggèrent des symptômes compatibles avec un TDAH chez l'adulte."
: "Le dépistage est négatif. Une évaluation clinique reste recommandée si vous avez des préoccupations."
};
}
function scoreDIVA() {
let inattentionCount = 0, hyperactivityCount = 0, childhoodCount = 0;
const impairmentDomains = [];
DIVA_INATTENTION.forEach(q => {
if (state.responses[q.id] === 1) inattentionCount++;
});
DIVA_HYPERACTIVITY.forEach(q => {
if (state.responses[q.id] === 1) hyperactivityCount++;
});
DIVA_CHILDHOOD.forEach(q => {
if (state.responses[q.id] === 1) childhoodCount++;
});
DIVA_IMPAIRMENT.forEach(q => {
if (state.responses[q.id] === 1) impairmentDomains.push(q.domain);
});
const meetsInattention = inattentionCount >= 5;
const meetsHyperactivity = hyperactivityCount >= 5;
let presentation = "Sous les seuils cliniques";
if (meetsInattention && meetsHyperactivity) presentation = "Inattention + Hyperactivité/Impulsivité";
else if (meetsInattention) presentation = "Inattention prédominante";
else if (meetsHyperactivity) presentation = "Hyperactivité/Impulsivité prédominante";
return {
inattentionCount,
hyperactivityCount,
childhoodCount,
childhoodPositive: childhoodCount >= 2,
impairmentDomains,
meetsInattention,
meetsHyperactivity,
presentation
};
}
function scoreExecutive() {
const clusterScores = {};
let total = 0, maxTotal = 0;
Object.entries(EXEC_FUNCTIONS).forEach(([key, cluster]) => {
let score = 0;
cluster.questions.forEach(q => {
score += state.responses[q.id] || 0;
});
const max = cluster.questions.length * 3;
clusterScores[cluster.name] = { score, max, percentage: Math.round((score / max) * 100) };
total += score;
maxTotal += max;
});
const percentage = Math.round((total / maxTotal) * 100);
let severity = 'Faible';
if (percentage >= 66) severity = 'Élevé';
else if (percentage >= 40) severity = 'Modéré';
const impaired = Object.entries(clusterScores)
.filter(([_, s]) => s.percentage >= 50)
.map(([name, _]) => name);
return { clusterScores, total, maxTotal, percentage, severity, impaired };
}
// ============================================
// RENDER RESULTS
// ============================================
function renderResults() {
const results = calculateResults();
const container = document.getElementById('results-content');
const asrsLevel = results.asrs.screeningPositive ? 'high' : 'low';
const divaLevel = (results.diva.meetsInattention || results.diva.meetsHyperactivity) ? 'high' : 'low';
const efLevel = results.executive.severity === 'Élevé' ? 'high' : results.executive.severity === 'Modéré' ? 'moderate' : 'low';
let html = `
<div class="summary-card">
<h2>Synthèse</h2>
<p class="summary-text">
<strong>${results.global.positiveIndicators}/3</strong> outils d'évaluation suggèrent des symptômes
${results.global.positiveIndicators >= 2 ? 'évocateurs, méritant une évaluation clinique' : 'à explorer si besoin'}.
</p>
${results.asrs.screeningPositive ? '<p>• ASRS: Seuil de dépistage <strong>atteint</strong></p>' : ''}
${results.diva.meetsInattention || results.diva.meetsHyperactivity ?
`<p>• Critères DSM-5: <strong>${results.diva.presentation}</strong></p>` : ''}
${results.executive.severity !== 'Faible' ?
`<p>• Fonctions exécutives: Difficultés de niveau <strong>${results.executive.severity}</strong></p>` : ''}
<div class="recommendation">
<strong>Recommandation:</strong>
${results.global.positiveIndicators >= 2
? "Une évaluation clinique par un spécialiste (psychiatre, neurologue) est recommandée pour explorer ces symptômes. Apportez ce rapport lors de votre consultation."
: "Si ces difficultés impactent votre quotidien, une consultation pourrait être utile pour explorer les causes possibles."}
</div>
</div>
<div class="result-card ${asrsLevel}">
<h3>1. ASRS v1.1 — Dépistage OMS</h3>
<div class="result-status ${asrsLevel}">
Seuil ${results.asrs.screeningPositive ? 'atteint' : 'non atteint'}
(${results.asrs.partAShaded}/6 critères, seuil ≥4)
</div>
<div class="scores-grid">
<div class="score-item">
<span class="score-label">Partie A</span>
<span class="score-value">${results.asrs.partAScore}/24</span>
</div>
<div class="score-item">
<span class="score-label">Partie B</span>
<span class="score-value">${results.asrs.partBScore}/48</span>
</div>
<div class="score-item">
<span class="score-label">Total</span>
<span class="score-value">${results.asrs.total}/72</span>
</div>
</div>
<div class="progress-item">
<div class="progress-label-row">
<span>Inattention</span>
<span>${results.asrs.inattention}/36</span>
</div>
<div class="progress-track">
<div class="progress-fill" style="width: ${Math.round(results.asrs.inattention / 36 * 100)}%"></div>
</div>
</div>
<div class="progress-item">
<div class="progress-label-row">
<span>Hyperactivité/Impulsivité</span>
<span>${results.asrs.hyperactivity}/36</span>
</div>
<div class="progress-track">
<div class="progress-fill" style="width: ${Math.round(results.asrs.hyperactivity / 36 * 100)}%"></div>
</div>
</div>
</div>
<div class="result-card ${divaLevel}">
<h3>2. Critères DSM-5</h3>
<div class="result-status ${divaLevel}">
${results.diva.presentation}
</div>
<div class="scores-grid">
<div class="score-item">
<span class="score-label">Inattention (A1)</span>
<span class="score-value">${results.diva.inattentionCount}/9</span>
</div>
<div class="score-item">
<span class="score-label">Hyperactivité (A2)</span>
<span class="score-value">${results.diva.hyperactivityCount}/9</span>
</div>
<div class="score-item">
<span class="score-label">Enfance (B)</span>
<span class="score-value">${results.diva.childhoodPositive ? '✓' : '✗'}</span>
</div>
<div class="score-item">
<span class="score-label">Retentissement</span>
<span class="score-value">${results.diva.impairmentDomains.length}/5</span>
</div>
</div>
${results.diva.impairmentDomains.length > 0 ? `
<p class="interpretation">Domaines impactés: ${results.diva.impairmentDomains.join(', ')}</p>
` : ''}
</div>
<div class="result-card ${efLevel}">
<h3>3. Fonctions Exécutives</h3>
<div class="result-status ${efLevel}">
Niveau de difficulté: ${results.executive.severity} (${results.executive.percentage}%)
</div>
${Object.entries(results.executive.clusterScores).map(([name, s]) => `
<div class="progress-item">
<div class="progress-label-row">
<span>${name}</span>
<span>${s.score}/${s.max}</span>
</div>
<div class="progress-track">
<div class="progress-fill ${s.percentage >= 50 ? 'high' : ''}" style="width: ${s.percentage}%"></div>
</div>
</div>
`).join('')}
${results.executive.impaired.length > 0 ? `
<p class="interpretation">Domaines les plus impactés: ${results.executive.impaired.join(', ')}</p>
` : ''}
</div>
`;
container.innerHTML = html;
}
// ============================================
// PDF GENERATION
// ============================================
function generatePDF() {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
const results = state.results;
const now = new Date().toLocaleString('fr-FR');
let y = 20;
const margin = 20;
const pageWidth = doc.internal.pageSize.width;
// Helper functions
const addTitle = (text, size = 16) => {
doc.setFontSize(size);
doc.setFont(undefined, 'bold');
doc.text(text, margin, y);
y += size * 0.5;
};
const addText = (text, size = 11) => {
doc.setFontSize(size);
doc.setFont(undefined, 'normal');
const lines = doc.splitTextToSize(text, pageWidth - 2 * margin);
doc.text(lines, margin, y);
y += lines.length * size * 0.4 + 2;
};
const addLine = () => {
doc.setDrawColor(200);
doc.line(margin, y, pageWidth - margin, y);
y += 5;
};
const checkPage = (needed = 30) => {
if (y + needed > 280) {
doc.addPage();
y = 20;
}
};
// Title
addTitle('Rapport d\'Auto-Évaluation TDAH Adulte', 18);
y += 5;
addText(`Généré le ${now}`);
addLine();
// Disclaimer
doc.setFillColor(255, 243, 205);
doc.rect(margin, y, pageWidth - 2 * margin, 25, 'F');
y += 5;
doc.setFontSize(9);
doc.setFont(undefined, 'bold');
doc.text('AVERTISSEMENT', margin + 2, y);
y += 4;
doc.setFont(undefined, 'normal');
const disclaimer = 'Ce document est un outil d\'AUTO-ÉVALUATION et NE CONSTITUE PAS un diagnostic médical. Seul un professionnel de santé qualifié peut établir un diagnostic de TDAH.';
const disclaimerLines = doc.splitTextToSize(disclaimer, pageWidth - 2 * margin - 4);
doc.text(disclaimerLines, margin + 2, y);
y += 20;
// Summary
checkPage(50);
addTitle('Synthèse des Résultats', 14);
y += 3;
addText(`${results.global.positiveIndicators}/3 outils suggèrent des symptômes ${results.global.positiveIndicators >= 2 ? 'évocateurs, méritant une évaluation clinique' : 'à explorer si besoin'}.`);
if (results.asrs.screeningPositive) addText('• ASRS: Seuil de dépistage atteint');
if (results.diva.meetsInattention || results.diva.meetsHyperactivity)
addText(`• Critères DSM-5: ${results.diva.presentation}`);
if (results.executive.severity !== 'Faible')
addText(`• Fonctions exécutives: Difficultés de niveau ${results.executive.severity}`);
y += 5;
addLine();
// ASRS Results
checkPage(40);
addTitle('1. ASRS v1.1 (OMS/Harvard)', 12);
addText(`Seuil: ${results.asrs.screeningPositive ? 'Atteint' : 'Non atteint'} (${results.asrs.partAShaded}/6 critères)`);
addText(`Score total: ${results.asrs.total}/72 | Inattention: ${results.asrs.inattention}/36 | Hyperactivité: ${results.asrs.hyperactivity}/36`);
y += 3;
// DIVA Results
checkPage(40);
addTitle('2. Critères DSM-5', 12);
addText(`Présentation: ${results.diva.presentation}`);
addText(`Inattention: ${results.diva.inattentionCount}/9 | Hyperactivité: ${results.diva.hyperactivityCount}/9`);
addText(`Enfance: ${results.diva.childhoodPositive ? 'Confirmé' : 'Non confirmé'} | Retentissement: ${results.diva.impairmentDomains.length} domaines`);
y += 3;
// Executive Functions
checkPage(50);
addTitle('3. Fonctions Exécutives', 12);
addText(`Niveau global: ${results.executive.severity} (${results.executive.percentage}%)`);
Object.entries(results.executive.clusterScores).forEach(([name, s]) => {
addText(` • ${name}: ${s.score}/${s.max} (${s.percentage}%)`);
});
// New page for detailed responses
doc.addPage();
y = 20;
addTitle('Annexe: Réponses Détaillées', 14);
addLine();
// ASRS responses
addTitle('ASRS v1.1', 11);
[...ASRS_QUESTIONS.part_a, ...ASRS_QUESTIONS.part_b].forEach((q, i) => {
checkPage(15);
const val = state.responses[q.id];
const label = ASRS_OPTIONS.find(o => o.value === val)?.label || 'Non répondu';
doc.setFontSize(9);
const qText = doc.splitTextToSize(`${i + 1}. ${q.text}`, pageWidth - 2 * margin - 40);
doc.text(qText, margin, y);
doc.text(label, pageWidth - margin - 30, y);
y += qText.length * 4 + 2;
});
// DIVA responses
checkPage(20);
addTitle('Critères DSM-5', 11);
[...DIVA_INATTENTION, ...DIVA_HYPERACTIVITY, ...DIVA_CHILDHOOD, ...DIVA_IMPAIRMENT].forEach((q, i) => {
checkPage(12);
const val = state.responses[q.id];
doc.setFontSize(9);
const qText = doc.splitTextToSize(`${q.text}`, pageWidth - 2 * margin - 20);
doc.text(qText, margin, y);
doc.text(val === 1 ? 'Oui' : 'Non', pageWidth - margin - 15, y);
y += qText.length * 4 + 2;
});
// References page
doc.addPage();
y = 20;
addTitle('Références Scientifiques', 14);
addLine();
doc.setFontSize(10);
addText('1. Kessler RC, et al. (2005). The World Health Organization Adult ADHD Self-Report Scale (ASRS).');
addText(' Psychological Medicine, 35(2), 245-256. https://pubmed.ncbi.nlm.nih.gov/15841682/');
y += 3;
addText('2. American Psychiatric Association (2013). Diagnostic and Statistical Manual of Mental Disorders (5th ed.).');
addText(' https://www.psychiatry.org/psychiatrists/practice/dsm');
y += 3;
addText('3. Kooij JJS, et al. (2010). DIVA 2.0: Diagnostic Interview for ADHD in Adults.');
addText(' DIVA Foundation. https://www.divacenter.eu/');
y += 3;
addText('4. Brown TE (2013). A New Understanding of ADHD in Children and Adults.');
addText(' Executive Function Impairments. Routledge. https://pubmed.ncbi.nlm.nih.gov/24232170/');
// Footer on each page
const pageCount = doc.internal.getNumberOfPages();
for (let i = 1; i <= pageCount; i++) {
doc.setPage(i);
doc.setFontSize(8);
doc.setTextColor(128);
doc.text(`Page ${i}/${pageCount} | Ce document ne constitue pas un diagnostic médical`, margin, 290);
}
doc.save('evaluation_tdah.pdf');
}
// ============================================
// VALIDATION
// ============================================
function validateSection(section) {
let questions = [];
if (section === 'asrs') {
questions = [...ASRS_QUESTIONS.part_a, ...ASRS_QUESTIONS.part_b].map(q => q.id);
} else if (section === 'diva') {
questions = [
...DIVA_INATTENTION, ...DIVA_HYPERACTIVITY,
...DIVA_CHILDHOOD, ...DIVA_IMPAIRMENT
].map(q => q.id);
} else if (section === 'executive') {
questions = Object.values(EXEC_FUNCTIONS).flatMap(c => c.questions.map(q => q.id));
}
const answered = questions.filter(id => state.responses[id] !== undefined).length;
return answered === questions.length;
}
// ============================================
// EVENT HANDLERS
// ============================================
function init() {
// Restore saved answers BEFORE first render so radios render checked
const savedSection = restoreState();
// Render all questionnaires
renderASRS();
renderDIVA();
renderExecutive();
// Consent checkbox
document.getElementById('consent').addEventListener('change', (e) => {
document.getElementById('start-btn').disabled = !e.target.checked;
});
// Navigation buttons
document.getElementById('start-btn').addEventListener('click', () => showSection('asrs'));
document.getElementById('asrs-next').addEventListener('click', () => {
if (!validateSection('asrs')) {
alert('Veuillez répondre à toutes les questions avant de continuer.');
return;
}
showSection('diva');
});
document.getElementById('diva-back').addEventListener('click', () => showSection('asrs'));
document.getElementById('diva-next').addEventListener('click', () => {
if (!validateSection('diva')) {
alert('Veuillez répondre à toutes les questions avant de continuer.');
return;
}
showSection('executive');
});
document.getElementById('exec-back').addEventListener('click', () => showSection('diva'));
document.getElementById('exec-next').addEventListener('click', () => {
if (!validateSection('executive')) {
alert('Veuillez répondre à toutes les questions avant de continuer.');
return;
}
renderResults();
showSection('results');
});
document.getElementById('download-pdf').addEventListener('click', generatePDF);
document.getElementById('restart').addEventListener('click', () => {
state.responses = {};
state.results = null;
state.lastMilestone = null;
clearSavedState();
renderASRS();
renderDIVA();
renderExecutive();
showSection('home');
});
// Initialize
showSection('home');
// Resume where the user left off (after showSection('home') above)
if (savedSection && savedSection !== 'home') {
if (savedSection === 'results') {
renderResults(); // recomputes purely from state.responses
}
showSection(savedSection);
}
}
// Start app when DOM is ready
document.addEventListener('DOMContentLoaded', init);
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Auto-évaluation scientifique du TDAH chez l'adulte. Outils validés: ASRS (OMS), critères DSM-5, fonctions exécutives.">
<meta name="theme-color" content="#5B8A8A">
<link rel="manifest" href="manifest.webmanifest">
<!-- Open Graph / Social -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://plnech.github.io/adhd/">
<meta property="og:title" content="Clarté — Auto-Évaluation TDAH Adulte">
<meta property="og:description" content="Évaluez vos symptômes avec des outils scientifiquement validés: ASRS (OMS), critères DSM-5, fonctions exécutives. Gratuit, confidentiel, avec rapport PDF.">
<meta property="og:image" content="https://plnech.github.io/adhd/og-image.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Clarté — Auto-Évaluation TDAH Adulte">
<meta name="twitter:description" content="Évaluez vos symptômes avec des outils scientifiquement validés. Gratuit et confidentiel.">
<meta name="twitter:image" content="https://plnech.github.io/adhd/og-image.png">
<title>Clarté — Auto-Évaluation TDAH Adulte</title>
<link rel="stylesheet" href="style.css">
<script src="jspdf.min.js"></script>
</head>
<body>
<header>
<div class="container">
<div class="logo">
<div class="logo-icon">◎</div>
</div>
<h1>Clarté</h1>
<p class="subtitle">Auto-évaluation TDAH pour adultes</p>
</div>
</header>
<nav class="progress-bar">
<div class="container">
<div class="progress-steps">
<div class="step-dot"></div>
<div class="step-connector"></div>
<div class="step-dot"></div>
<div class="step-connector"></div>
<div class="step-dot"></div>
<div class="step-connector"></div>
<div class="step-dot"></div>
<div class="step-connector"></div>
<div class="step-dot"></div>
</div>
<p class="progress-label">Accueil</p>
<div class="answer-progress" id="answer-progress" hidden>
<p class="answer-count" id="answer-count"></p>
<div class="answer-fill-track">
<div class="answer-fill" id="answer-fill"></div>
</div>
</div>
</div>
</nav>
<main class="container">
<!-- HOME -->
<section id="home" class="section active">
<div class="card">
<div class="card-header">
<h2>Bienvenue</h2>
</div>
<p>
Cette application vous permet de réaliser une auto-évaluation structurée
des symptômes du <strong>Trouble Déficit de l'Attention avec ou sans Hyperactivité (TDAH)</strong>
chez l'adulte, en utilisant des outils scientifiquement validés.
</p>
<div class="intro-grid">
<div class="intro-card">
<div class="intro-card-icon teal">📋</div>
<div>
<h4>ASRS v1.1</h4>
<p>18 questions validées par l'OMS et Harvard pour le dépistage du TDAH adulte.</p>
</div>
</div>
<div class="intro-card">
<div class="intro-card-icon coral">🔬</div>
<div>
<h4>Critères DSM-5</h4>
<p>Évaluation structurée des 18 critères diagnostiques officiels, incluant l'enfance.</p>
</div>
</div>
<div class="intro-card">
<div class="intro-card-icon lavender">🧠</div>
<div>
<h4>Fonctions Exécutives</h4>
<p>6 domaines cognitifs selon le modèle de Brown: attention, mémoire, régulation.</p>
</div>
</div>
</div>
<div class="notice warning">
<h4>⚠️ Important — Veuillez lire attentivement</h4>
<ul>
<li><strong>Cette évaluation ne constitue PAS un diagnostic médical.</strong></li>
<li>Seul un professionnel de santé qualifié (psychiatre, neurologue) peut établir un diagnostic de TDAH.</li>
<li>Les résultats sont destinés à faciliter la discussion avec votre médecin.</li>
<li>Répondez honnêtement en pensant à votre fonctionnement des <strong>6 derniers mois</strong>.</li>
<li>Durée estimée: <strong>15-20 minutes</strong>.</li>
<li>Vos données restent sur votre appareil et ne sont jamais transmises.</li>
</ul>
</div>
<label class="checkbox-wrapper">
<input type="checkbox" id="consent">
<span>
Je comprends que cette auto-évaluation ne constitue pas un diagnostic médical
et que je dois consulter un professionnel de santé pour toute évaluation diagnostique.
</span>
</label>
<div style="text-align: center;">
<button id="start-btn" class="btn btn-primary btn-large" disabled>
Commencer l'évaluation
</button>
</div>
</div>
</section>
<!-- ASRS -->
<section id="asrs" class="section">
<div class="card">
<div class="card-header">
<h2>ASRS v1.1 — Échelle d'auto-évaluation</h2>
<p class="source">Organisation Mondiale de la Santé / Harvard Medical School</p>
</div>
<div class="notice info">
<p>
Pour chaque question, indiquez à quelle fréquence vous avez ressenti ou agi
de cette manière au cours des <strong>6 derniers mois</strong>.
</p>
</div>
<div id="asrs-questions"></div>
<div class="actions">
<div></div>
<button id="asrs-next" class="btn btn-primary">
Continuer →
</button>
</div>
</div>
</section>
<!-- DIVA -->
<section id="diva" class="section">
<div class="card">
<div class="card-header">
<h2>Critères DSM-5</h2>
<p class="source">American Psychiatric Association (2013)</p>
</div>
<div class="notice info">
<p>
Répondez <strong>Oui</strong> si le symptôme est présent de manière fréquente et persistante
depuis au moins <strong>6 mois</strong>, et qu'il interfère avec votre fonctionnement.
</p>
</div>
<div id="diva-questions"></div>
<div class="actions">
<button id="diva-back" class="btn btn-secondary">
← Retour
</button>
<button id="diva-next" class="btn btn-primary">
Continuer →
</button>
</div>
</div>
</section>
<!-- EXECUTIVE FUNCTIONS -->
<section id="executive" class="section">
<div class="card">
<div class="card-header">
<h2>Fonctions Exécutives</h2>
<p class="source">Basé sur le modèle de Brown (2013)</p>
</div>
<div class="notice info">
<p>
Les fonctions exécutives sont les capacités cognitives qui nous permettent de gérer
notre attention, nos émotions et nos comportements. Indiquez à quel point chaque
difficulté est un problème pour vous <strong>actuellement</strong>.
</p>
</div>
<div id="executive-questions"></div>
<div class="actions">
<button id="exec-back" class="btn btn-secondary">
← Retour
</button>
<button id="exec-next" class="btn btn-primary">
Voir les résultats →
</button>
</div>
</div>
</section>
<!-- RESULTS -->
<section id="results" class="section">
<div class="notice warning">
<p>
<strong>Rappel:</strong> Ces résultats constituent une AUTO-ÉVALUATION et non un diagnostic médical.
Seul un professionnel de santé qualifié peut établir un diagnostic de TDAH.
</p>
</div>
<div id="results-content"></div>
<div style="text-align: center; margin-top: 2rem;">
<button id="download-pdf" class="btn btn-primary btn-large">
📄 Télécharger le rapport PDF
</button>
<p style="color: var(--ink-light); font-size: 0.9rem; margin-top: 1rem;">
Le rapport inclut une synthèse et les réponses détaillées pour votre médecin.
</p>
<button id="restart" class="btn btn-secondary" style="margin-top: 1rem;">
Recommencer l'évaluation
</button>
</div>
</section>
</main>
<footer>
<div class="container">
<p class="disclaimer">
<strong>Avertissement:</strong> Cet outil est fourni à titre informatif uniquement.
Il ne constitue pas un diagnostic médical. Consultez un professionnel de santé qualifié.
</p>
<p><strong>Références scientifiques:</strong></p>
<p style="font-size: 0.8rem; line-height: 1.8;">
<a href="https://pubmed.ncbi.nlm.nih.gov/15841682/" target="_blank" rel="noopener">ASRS v1.1 — Kessler et al. (2005)</a> •
<a href="https://www.psychiatry.org/psychiatrists/practice/dsm" target="_blank" rel="noopener">DSM-5 — APA (2013)</a> •
<a href="https://www.divacenter.eu/" target="_blank" rel="noopener">DIVA Foundation</a> •
<a href="https://pubmed.ncbi.nlm.nih.gov/24232170/" target="_blank" rel="noopener">Brown EF Model (2013)</a>
</p>
<p style="margin-top: 1rem; font-size: 0.8rem;">
<a href="https://github.com/PLNech/adhd" target="_blank" rel="noopener">Code source</a> •
MIT License © 2025 PLNech
</p>
</div>
</footer>
<script src="app.js"></script>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js').catch(function () {});
}
</script>
</body>
</html>
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "Clarté — Auto-Évaluation TDAH",
"short_name": "Clarté TDAH",
"lang": "fr",
"start_url": ".",
"display": "standalone",
"background_color": "#FAF8F5",
"theme_color": "#5B8A8A"
}
<svg width="1200" height="630" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Main gradient: warm cream to soft teal -->
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#FAF8F5"/>
<stop offset="30%" style="stop-color:#F0EDE8"/>
<stop offset="70%" style="stop-color:#E8F0F0"/>
<stop offset="100%" style="stop-color:#D8E8E8"/>
</linearGradient>
<!-- Accent blob gradients -->
<radialGradient id="blob1" cx="20%" cy="30%" r="40%">
<stop offset="0%" style="stop-color:#A89BBF;stop-opacity:0.15"/>
<stop offset="100%" style="stop-color:#A89BBF;stop-opacity:0"/>
</radialGradient>
<radialGradient id="blob2" cx="80%" cy="70%" r="35%">
<stop offset="0%" style="stop-color:#D4A27F;stop-opacity:0.12"/>
<stop offset="100%" style="stop-color:#D4A27F;stop-opacity:0"/>
</radialGradient>
<radialGradient id="blob3" cx="60%" cy="20%" r="30%">
<stop offset="0%" style="stop-color:#5B8A8A;stop-opacity:0.08"/>
<stop offset="100%" style="stop-color:#5B8A8A;stop-opacity:0"/>
</radialGradient>
</defs>
<!-- Background -->
<rect width="1200" height="630" fill="url(#bgGradient)"/>
<!-- Soft blobs -->
<ellipse cx="240" cy="190" rx="300" ry="250" fill="url(#blob1)"/>
<ellipse cx="960" cy="440" rx="280" ry="220" fill="url(#blob2)"/>
<ellipse cx="720" cy="126" rx="240" ry="180" fill="url(#blob3)"/>
<!-- Decorative circle/logo -->
<circle cx="600" cy="200" r="60" fill="#5B8A8A" opacity="0.9"/>
<circle cx="600" cy="200" r="45" fill="none" stroke="#FAF8F5" stroke-width="3"/>
<circle cx="600" cy="200" r="8" fill="#FAF8F5"/>
<!-- Title -->
<text x="600" y="340" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="72" font-weight="700" fill="#3A3632">
Clarté
</text>
<!-- Subtitle -->
<text x="600" y="410" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="32" fill="#5B8A8A">
Auto-Évaluation TDAH Adulte
</text>
<!-- Tagline -->
<text x="600" y="480" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="22" fill="#6B6560">
Outils scientifiquement validés • ASRS • DSM-5 • Fonctions Exécutives
</text>
<!-- Bottom accent line -->
<rect x="400" y="530" width="400" height="4" rx="2" fill="#5B8A8A" opacity="0.6"/>
</svg>
/*
* Clarté Theme - ADHD Self-Evaluation
* A soothing, paper-like design focused on clarity and calm
*/
/* ============================================
FONTS - Atkinson Hyperlegible (self-hosted)
Designed for readability by Braille Institute
============================================ */
@font-face {
font-family: 'Atkinson';
src: url('fonts/AtkinsonHyperlegible-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Atkinson';
src: url('fonts/AtkinsonHyperlegible-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Atkinson';
src: url('fonts/AtkinsonHyperlegible-Italic.woff2') format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
/* ============================================
COLOR PALETTE - Warm, soothing, paper-like
============================================ */
:root {
/* Paper & Background */
--paper: #FAF8F5;
--paper-dark: #F0EDE8;
--paper-shadow: #E5E0D8;
/* Text */
--ink: #3A3632;
--ink-light: #6B6560;
--ink-faint: #9A9590;
/* Primary - Calming Teal */
--teal: #5B8A8A;
--teal-light: #7BA8A8;
--teal-dark: #4A7272;
--teal-faint: #E8F0F0;
/* Accent - Warm Coral */
--coral: #D4A27F;
--coral-light: #E8C4A8;
--coral-faint: #FDF5F0;
/* Accent - Soft Lavender */
--lavender: #A89BBF;
--lavender-faint: #F5F3F8;
/* Semantic - Purple scale for results (non-alarming) */
--sage: #7D9E83;
--sage-faint: #EFF5F0;
--purple-light: #B8A9C9;
--purple-light-faint: #F5F2F8;
--purple-mid: #9683A8;
--purple-mid-faint: #F0ECF5;
--purple-deep: #7B6B8A;
--purple-deep-faint: #EBE7F0;
/* Spacing */
--space-xs: 0.25rem;
--space-sm: 0.5rem;
--space-md: 1rem;
--space-lg: 1.5rem;
--space-xl: 2rem;
--space-2xl: 3rem;
/* Radius */
--radius-sm: 6px;
--radius-md: 12px;
--radius-lg: 20px;
--radius-full: 9999px;
/* Shadows - soft, warm */
--shadow-sm: 0 1px 3px rgba(58, 54, 50, 0.08);
--shadow-md: 0 4px 12px rgba(58, 54, 50, 0.1);
--shadow-lg: 0 8px 24px rgba(58, 54, 50, 0.12);
}
/* ============================================
BASE STYLES
============================================ */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
scroll-behavior: smooth;
}
body {
font-family: 'Atkinson', -apple-system, BlinkMacSystemFont, sans-serif;
line-height: 1.65;
color: var(--ink);
background: var(--paper);
min-height: 100vh;
/* Subtle paper texture */
background-image:
radial-gradient(ellipse at 20% 30%, rgba(168, 155, 191, 0.03) 0%, transparent 50%),
radial-gradient(ellipse at 80% 70%, rgba(212, 162, 127, 0.03) 0%, transparent 50%);
}
/* ============================================
TYPOGRAPHY
============================================ */
h1, h2, h3, h4 {
font-weight: 700;
line-height: 1.3;
color: var(--ink);
}
h1 {
font-size: 2rem;
letter-spacing: -0.02em;
}
h2 {
font-size: 1.5rem;
color: var(--teal-dark);
}
h3 {
font-size: 1.2rem;
}
p {
margin-bottom: var(--space-md);
}
a {
color: var(--teal);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.2s;
}
a:hover {
border-bottom-color: var(--teal);
}
/* ============================================
LAYOUT
============================================ */
.container {
max-width: 720px;
margin: 0 auto;
padding: 0 var(--space-lg);
}
/* ============================================
HEADER
============================================ */
header {
padding: var(--space-xl) 0;
text-align: center;
border-bottom: 1px solid var(--paper-shadow);
background: linear-gradient(180deg, var(--paper) 0%, var(--teal-faint) 100%);
}
.logo {
display: inline-flex;
align-items: center;
gap: var(--space-sm);
margin-bottom: var(--space-sm);
}
.logo-icon {
width: 48px;
height: 48px;
background: linear-gradient(135deg, var(--teal) 0%, var(--teal-dark) 100%);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1.5rem;
box-shadow: var(--shadow-md);
}
header h1 {
margin-bottom: var(--space-xs);
}
header .subtitle {
color: var(--ink-light);
font-size: 1.1rem;
}
/* ============================================
PROGRESS INDICATOR
============================================ */
.progress-bar {
padding: var(--space-md) 0;
background: var(--paper);
border-bottom: 1px solid var(--paper-shadow);
position: sticky;
top: 0;
z-index: 100;
}
.progress-steps {
display: flex;
justify-content: center;
gap: var(--space-xs);
align-items: center;
}
.step-dot {
width: 12px;
height: 12px;
border-radius: var(--radius-full);
background: var(--paper-shadow);
transition: all 0.3s ease;
}
.step-dot.active {
background: var(--teal);
transform: scale(1.3);
}
.step-dot.completed {
background: var(--sage);
}
.step-connector {
width: 40px;
height: 2px;
background: var(--paper-shadow);
transition: background 0.3s;
}
.step-connector.completed {
background: var(--sage);
}
.progress-label {
text-align: center;
margin-top: var(--space-sm);
font-size: 0.85rem;
color: var(--ink-light);
}
/* Answer progress: count line + 3px fill, on questionnaire sections only.
Hidden on home/results via the `hidden` attribute (set in app.js). */
.answer-progress {
margin-top: var(--space-sm);
}
.answer-progress[hidden] {
display: none;
}
.answer-count {
text-align: center;
margin-bottom: var(--space-xs);
font-size: 0.85rem;
color: var(--ink-faint);
font-variant-numeric: tabular-nums;
}
.answer-fill-track {
height: 3px;
background: var(--paper-shadow);
border-radius: var(--radius-full);
overflow: hidden;
}
.answer-fill {
height: 100%;
width: 0;
background: var(--teal-dark);
transition: width 300ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
.answer-fill {
transition: none;
}
}
/* ============================================
MAIN CONTENT
============================================ */
main {
padding: var(--space-2xl) 0;
min-height: 60vh;
}
.section {
display: none;
animation: fadeIn 0.4s ease;
}
.section.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* ============================================
CARDS
============================================ */
.card {
background: white;
border-radius: var(--radius-lg);
padding: var(--space-xl);
margin-bottom: var(--space-lg);
box-shadow: var(--shadow-sm);
border: 1px solid var(--paper-shadow);
}
.card-header {
margin-bottom: var(--space-lg);
padding-bottom: var(--space-md);
border-bottom: 2px solid var(--teal-faint);
}
.card-header h2 {
margin-bottom: var(--space-xs);
}
.card-header .source {
font-size: 0.85rem;
color: var(--ink-faint);
font-style: italic;
}
/* ============================================
INTRO CARDS (Home)
============================================ */
.intro-grid {
display: grid;
gap: var(--space-md);
margin: var(--space-xl) 0;
}
.intro-card {
background: white;
border-radius: var(--radius-md);
padding: var(--space-lg);
border: 1px solid var(--paper-shadow);
display: flex;
gap: var(--space-md);
align-items: flex-start;
}
.intro-card-icon {
width: 44px;
height: 44px;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.3rem;
flex-shrink: 0;
}
.intro-card-icon.teal { background: var(--teal-faint); color: var(--teal); }
.intro-card-icon.coral { background: var(--coral-faint); color: var(--coral); }
.intro-card-icon.lavender { background: var(--lavender-faint); color: var(--lavender); }
.intro-card h4 {
margin-bottom: var(--space-xs);
}
.intro-card p {
font-size: 0.9rem;
color: var(--ink-light);
margin: 0;
}
/* ============================================
ALERTS / NOTICES
============================================ */
.notice {
padding: var(--space-lg);
border-radius: var(--radius-md);
margin-bottom: var(--space-lg);
}
.notice.warning {
background: var(--amber-faint);
border: 1px solid var(--amber);
}
.notice.info {
background: var(--teal-faint);
border: 1px solid var(--teal-light);
}
.notice h4 {
margin-bottom: var(--space-sm);
display: flex;
align-items: center;
gap: var(--space-sm);
}
.notice ul {
margin-left: var(--space-lg);
color: var(--ink-light);
}
.notice li {
margin-bottom: var(--space-xs);
}
/* ============================================
QUESTIONS
============================================ */
.question-group {
margin-bottom: var(--space-xl);
}
.question-group h3 {
color: var(--teal-dark);
margin-bottom: var(--space-md);
padding-bottom: var(--space-sm);
border-bottom: 1px solid var(--paper-shadow);
}
.question-group .hint {
font-size: 0.85rem;
color: var(--ink-faint);
font-style: italic;
margin-bottom: var(--space-md);
}
.question {
background: var(--paper);
border-radius: var(--radius-md);
padding: var(--space-lg);
margin-bottom: var(--space-md);
border: 1px solid transparent;
transition: border-color 0.2s, box-shadow 0.2s;
}
.question:hover {
border-color: var(--paper-shadow);
}
.question.answered {
border-color: var(--sage);
background: var(--sage-faint);
}
.question-text {
font-weight: 500;
margin-bottom: var(--space-md);
line-height: 1.5;
}
.question-number {
color: var(--teal);
font-weight: 700;
}
/* ============================================
RESPONSE OPTIONS
============================================ */
.options {
display: flex;
flex-wrap: wrap;
gap: var(--space-sm);
}
.option {
position: relative;
}
.option input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.option label {
display: inline-block;
padding: var(--space-sm) var(--space-md);
background: white;
border: 2px solid var(--paper-shadow);
border-radius: var(--radius-full);
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
}
.option label:hover {
border-color: var(--teal-light);
background: var(--teal-faint);
}
.option input:checked + label {
background: var(--teal);
border-color: var(--teal);
color: white;
}
/* Binary options (Yes/No) */
.options.binary {
gap: var(--space-md);
}
.options.binary .option label {
min-width: 80px;
text-align: center;
}
.options.binary .option.yes input:checked + label {
background: var(--coral);
border-color: var(--coral);
}
.options.binary .option.no input:checked + label {
background: var(--sage);
border-color: var(--sage);
}
/* ============================================
BUTTONS
============================================ */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-sm);
padding: var(--space-md) var(--space-xl);
border-radius: var(--radius-full);
font-family: inherit;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.2s;
}
.btn-primary {
background: var(--teal);
color: white;
}
.btn-primary:hover {
background: var(--teal-dark);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.btn-secondary {
background: white;
color: var(--ink);
border-color: var(--paper-shadow);
}
.btn-secondary:hover {
border-color: var(--teal);
color: var(--teal);
}
.btn-large {
padding: var(--space-lg) var(--space-2xl);
font-size: 1.1rem;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: var(--space-xl);
padding-top: var(--space-lg);
border-top: 1px solid var(--paper-shadow);
}
/* ============================================
CHECKBOX (Consent)
============================================ */
.checkbox-wrapper {
display: flex;
align-items: flex-start;
gap: var(--space-md);
padding: var(--space-lg);
background: var(--paper);
border-radius: var(--radius-md);
margin-bottom: var(--space-lg);
cursor: pointer;
}
.checkbox-wrapper input[type="checkbox"] {
width: 22px;
height: 22px;
margin-top: 2px;
accent-color: var(--teal);
cursor: pointer;
}
.checkbox-wrapper span {
font-size: 0.95rem;
line-height: 1.5;
}
/* ============================================
RESULTS
============================================ */
.summary-card {
background: linear-gradient(135deg, var(--teal-faint) 0%, var(--lavender-faint) 100%);
border: 2px solid var(--teal);
border-radius: var(--radius-lg);
padding: var(--space-xl);
margin-bottom: var(--space-xl);
}
.summary-card h2 {
margin-bottom: var(--space-md);
}
.summary-text {
font-size: 1.05rem;
line-height: 1.7;
margin-bottom: var(--space-md);
}
.recommendation {
background: white;
padding: var(--space-md);
border-radius: var(--radius-md);
border-left: 4px solid var(--teal);
}
/* Result Cards */
.result-card {
background: white;
border-radius: var(--radius-lg);
padding: var(--space-xl);
margin-bottom: var(--space-lg);
border: 1px solid var(--paper-shadow);
border-left: 5px solid var(--paper-shadow);
}
.result-card.high {
border-left-color: var(--purple-deep);
}
.result-card.low {
border-left-color: var(--sage);
}
.result-card.moderate {
border-left-color: var(--purple-mid);
}
.result-card h3 {
margin-bottom: var(--space-md);
}
.result-status {
display: inline-flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-sm) var(--space-md);
border-radius: var(--radius-full);
font-weight: 600;
font-size: 0.9rem;
margin-bottom: var(--space-md);
}
.result-status.high {
background: var(--purple-deep-faint);
color: var(--purple-deep);
}
.result-status.low {
background: var(--sage-faint);
color: var(--sage);
}
.result-status.moderate {
background: var(--purple-mid-faint);
color: var(--purple-mid);
}
/* Score Grid */
.scores-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: var(--space-md);
margin: var(--space-md) 0;
}
.score-item {
background: var(--paper);
padding: var(--space-md);
border-radius: var(--radius-md);
text-align: center;
}
.score-label {
font-size: 0.8rem;
color: var(--ink-light);
display: block;
margin-bottom: var(--space-xs);
}
.score-value {
font-size: 1.4rem;
font-weight: 700;
color: var(--teal-dark);
}
/* Progress Bars */
.progress-item {
margin-bottom: var(--space-md);
}
.progress-label-row {
display: flex;
justify-content: space-between;
margin-bottom: var(--space-xs);
font-size: 0.9rem;
}
.progress-track {
height: 10px;
background: var(--paper-shadow);
border-radius: var(--radius-full);
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--teal);
border-radius: var(--radius-full);
transition: width 0.5s ease;
}
.progress-fill.high {
background: var(--purple-deep);
}
.progress-fill.moderate {
background: var(--purple-mid);
}
/* Interpretation */
.interpretation {
margin-top: var(--space-md);
padding-top: var(--space-md);
border-top: 1px dashed var(--paper-shadow);
font-style: italic;
color: var(--ink-light);
}
/* ============================================
FOOTER
============================================ */
footer {
padding: var(--space-xl) 0;
background: var(--paper-dark);
border-top: 1px solid var(--paper-shadow);
text-align: center;
}
footer p {
font-size: 0.85rem;
color: var(--ink-light);
margin-bottom: var(--space-sm);
}
footer .disclaimer {
max-width: 600px;
margin: 0 auto var(--space-md);
}
/* ============================================
RESPONSIVE
============================================ */
@media (max-width: 600px) {
html { font-size: 15px; }
.container { padding: 0 var(--space-md); }
.card { padding: var(--space-lg); }
.options { flex-direction: column; }
.option label { display: block; text-align: center; }
.actions { flex-direction: column; gap: var(--space-md); }
.btn { width: 100%; }
.scores-grid { grid-template-columns: repeat(2, 1fr); }
}
/* ============================================
PRINT STYLES
============================================ */
@media print {
body { background: white; }
.progress-bar, .actions, footer { display: none; }
.section { display: block !important; page-break-inside: avoid; }
}
/* Cache-first: la page et ses données chargent hors réseau; les réponses
vivent dans localStorage et ne passent jamais par le réseau. */
var CACHE = 'clarte-v2';
var ASSETS = ['./', './index.html', './style.css', './app.js', './jspdf.min.js',
'./manifest.webmanifest',
'./fonts/AtkinsonHyperlegible-Regular.woff2',
'./fonts/AtkinsonHyperlegible-Bold.woff2',
'./fonts/AtkinsonHyperlegible-Italic.woff2'];
self.addEventListener('install', function (e) {
e.waitUntil(caches.open(CACHE).then(function (c) { return c.addAll(ASSETS); }));
});
self.addEventListener('activate', function (e) {
e.waitUntil(caches.keys().then(function (keys) {
return Promise.all(keys.filter(function (k) { return k !== CACHE; })
.map(function (k) { return caches.delete(k); }));
}));
});
self.addEventListener('fetch', function (e) {
if (e.request.method !== 'GET') return;
e.respondWith(caches.match(e.request, { ignoreSearch: true })
.then(function (hit) { return hit || fetch(e.request); }));
});
/**
* Clarté - ADHD Self-Evaluation App
* Client-side questionnaire logic and PDF generation
*/
// ============================================
// QUESTIONNAIRE DATA
// ============================================
const ASRS_QUESTIONS = {
part_a: [
{ id: 'asrs_1', text: "À quelle fréquence avez-vous des difficultés à finaliser les derniers détails d'un projet, une fois que les parties les plus stimulantes ont été réalisées ?", threshold: 2 },
{ id: 'asrs_2', text: "À quelle fréquence avez-vous des difficultés à mettre de l'ordre lorsque vous devez effectuer une tâche qui requiert de l'organisation ?", threshold: 2 },
{ id: 'asrs_3', text: "À quelle fréquence avez-vous des difficultés à vous souvenir de vos rendez-vous ou de vos obligations ?", threshold: 2 },
{ id: 'asrs_4', text: "Lorsque vous devez effectuer une tâche qui demande beaucoup de réflexion, à quelle fréquence évitez-vous ou retardez-vous le moment de commencer ?", threshold: 3 },
{ id: 'asrs_5', text: "À quelle fréquence remuez-vous ou tortillez-vous les mains ou les pieds lorsque vous devez rester assis(e) pendant une longue période ?", threshold: 3 },
{ id: 'asrs_6', text: "À quelle fréquence vous sentez-vous excessivement actif(ve) et poussé(e) à faire des choses, comme si vous étiez mû(e) par un moteur ?", threshold: 3 },
],
part_b: [
{ id: 'asrs_7', text: "À quelle fréquence faites-vous des erreurs d'inattention lorsque vous devez travailler sur un projet ennuyeux ou difficile ?" },
{ id: 'asrs_8', text: "À quelle fréquence avez-vous des difficultés à maintenir votre attention lorsque vous effectuez un travail ennuyeux ou répétitif ?" },
{ id: 'asrs_9', text: "À quelle fréquence avez-vous des difficultés à vous concentrer sur ce que les gens vous disent, même lorsqu'ils s'adressent directement à vous ?" },
{ id: 'asrs_10', text: "À quelle fréquence égarez-vous ou avez-vous du mal à retrouver des objets à la maison ou au travail ?" },
{ id: 'asrs_11', text: "À quelle fréquence êtes-vous distrait(e) par l'activité ou le bruit autour de vous ?" },
{ id: 'asrs_12', text: "À quelle fréquence quittez-vous votre siège lors de réunions ou dans d'autres situations où vous êtes censé(e) rester assis(e) ?" },
{ id: 'asrs_13', text: "À quelle fréquence vous sentez-vous agité(e) ou remuant(e) ?" },
{ id: 'asrs_14', text: "À quelle fréquence avez-vous des difficultés à vous détendre et à vous relaxer lorsque vous avez du temps libre ?" },
{ id: 'asrs_15', text: "À quelle fréquence vous trouvez-vous à parler trop en situation sociale ?" },
{ id: 'asrs_16', text: "Lors d'une conversation, à quelle fréquence finissez-vous les phrases des personnes avec qui vous parlez avant qu'elles ne puissent les terminer elles-mêmes ?" },
{ id: 'asrs_17', text: "À quelle fréquence avez-vous des difficultés à attendre votre tour dans des situations qui l'exigent ?" },
{ id: 'asrs_18', text: "À quelle fréquence interrompez-vous les autres lorsqu'ils sont occupés ?" },
]
};
const ASRS_OPTIONS = [
{ value: 0, label: 'Jamais' },
{ value: 1, label: 'Rarement' },
{ value: 2, label: 'Parfois' },
{ value: 3, label: 'Souvent' },
{ value: 4, label: 'Très souvent' }
];
const DIVA_INATTENTION = [
{ id: 'diva_ia_1', text: "Avez-vous souvent du mal à prêter attention aux détails ou faites-vous des erreurs d'inattention dans votre travail ou d'autres activités ? (ex: négliger des détails, travail inexact)" },
{ id: 'diva_ia_2', text: "Avez-vous souvent du mal à maintenir votre attention sur des tâches ou des activités ? (ex: difficulté à rester concentré pendant les réunions, lectures)" },
{ id: 'diva_ia_3', text: "Avez-vous souvent l'impression de ne pas écouter quand on vous parle directement ? (ex: esprit ailleurs, même sans distraction évidente)" },
{ id: 'diva_ia_4', text: "Avez-vous souvent du mal à suivre les instructions jusqu'au bout et à terminer vos tâches professionnelles ou obligations ?" },
{ id: 'diva_ia_5', text: "Avez-vous souvent du mal à organiser vos tâches et activités ? (ex: difficulté à gérer le temps, respecter les délais)" },
{ id: 'diva_ia_6', text: "Évitez-vous souvent, ou êtes-vous réticent(e) à vous engager dans des tâches qui nécessitent un effort mental soutenu ?" },
{ id: 'diva_ia_7', text: "Perdez-vous souvent des objets nécessaires à vos activités ? (ex: clés, portefeuille, téléphone, documents)" },
{ id: 'diva_ia_8', text: "Êtes-vous souvent facilement distrait(e) par des stimuli externes ou des pensées sans rapport ?" },
{ id: 'diva_ia_9', text: "Êtes-vous souvent oublieux(se) dans la vie quotidienne ? (ex: oublier de rappeler, payer des factures, honorer des rendez-vous)" },
];
const DIVA_HYPERACTIVITY = [
{ id: 'diva_hi_1', text: "Remuez-vous souvent les mains ou les pieds, ou vous tortillez-vous sur votre siège ?" },
{ id: 'diva_hi_2', text: "Quittez-vous souvent votre siège dans des situations où vous êtes censé(e) rester assis(e) ?" },
{ id: 'diva_hi_3', text: "Vous sentez-vous souvent agité(e) ou avez-vous du mal à rester en place ?" },
{ id: 'diva_hi_4', text: "Avez-vous souvent du mal à vous adonner à des loisirs ou activités calmes de manière tranquille ?" },
{ id: 'diva_hi_5', text: "Êtes-vous souvent « sur la brèche » ou agissez-vous comme si vous étiez « monté(e) sur ressorts » ?" },
{ id: 'diva_hi_6', text: "Parlez-vous souvent trop ?" },
{ id: 'diva_hi_7', text: "Laissez-vous souvent échapper une réponse avant qu'une question ne soit terminée ?" },
{ id: 'diva_hi_8', text: "Avez-vous souvent du mal à attendre votre tour ?" },
{ id: 'diva_hi_9', text: "Interrompez-vous souvent les autres ou vous imposez-vous ?" },
];
const DIVA_CHILDHOOD = [
{ id: 'diva_child_1', text: "Avant l'âge de 12 ans, aviez-vous des difficultés à rester attentif(ve) à l'école ou lors des devoirs ?" },
{ id: 'diva_child_2', text: "Avant l'âge de 12 ans, étiez-vous considéré(e) comme un enfant rêveur, « dans la lune » ?" },
{ id: 'diva_child_3', text: "Avant l'âge de 12 ans, aviez-vous du mal à rester assis(e) ou étiez-vous agité(e) à l'école ?" },
{ id: 'diva_child_4', text: "Avant l'âge de 12 ans, aviez-vous souvent des remarques sur vos bulletins concernant l'attention ou le comportement ?" },
{ id: 'diva_child_5', text: "Avant l'âge de 12 ans, perdiez-vous souvent vos affaires scolaires ou oubliiez-vous de faire vos devoirs ?" },
];
const DIVA_IMPAIRMENT = [
{ id: 'diva_imp_1', text: "Ces difficultés ont-elles un impact négatif sur votre travail ou vos études ?", domain: "Travail/Études" },
{ id: 'diva_imp_2', text: "Ces difficultés ont-elles un impact négatif sur vos relations familiales ou amoureuses ?", domain: "Relations familiales" },
{ id: 'diva_imp_3', text: "Ces difficultés ont-elles un impact négatif sur vos relations sociales et amicales ?", domain: "Relations sociales" },
{ id: 'diva_imp_4', text: "Ces difficultés ont-elles un impact négatif sur vos loisirs et activités de temps libre ?", domain: "Loisirs" },
{ id: 'diva_imp_5', text: "Ces difficultés ont-elles un impact négatif sur votre estime de soi ou votre confiance en vous ?", domain: "Estime de soi" },
];
const EXEC_FUNCTIONS = {
activation: {
name: "Activation",
description: "Organisation, priorisation, démarrage",
questions: [
{ id: 'ef_act_1', text: "Difficulté à organiser et planifier des tâches ou projets" },
{ id: 'ef_act_2', text: "Difficulté à établir des priorités entre différentes tâches" },
{ id: 'ef_act_3', text: "Difficulté à commencer une tâche, même quand elle est importante" },
{ id: 'ef_act_4', text: "Tendance à procrastiner, reporter les tâches au lendemain" },
]
},
focus: {
name: "Attention / Focus",
description: "Attention soutenue, résistance à la distraction",
questions: [
{ id: 'ef_foc_1', text: "Difficulté à maintenir l'attention sur une tâche jusqu'à son achèvement" },
{ id: 'ef_foc_2', text: "Facilement distrait(e) par des pensées ou stimuli non pertinents" },
{ id: 'ef_foc_3', text: "Difficulté à écouter et retenir ce qui est dit lors de conversations" },
{ id: 'ef_foc_4', text: "Passer d'une activité à l'autre sans en terminer aucune" },
]
},
effort: {
name: "Effort / Énergie",
description: "Régulation de l'éveil et de la vigilance",
questions: [
{ id: 'ef_eff_1', text: "Somnolence ou fatigue mentale lors de tâches peu stimulantes" },
{ id: 'ef_eff_2', text: "Difficulté à maintenir un niveau d'énergie constant tout au long de la journée" },
{ id: 'ef_eff_3', text: "Lenteur excessive pour accomplir des tâches routinières" },
{ id: 'ef_eff_4', text: "Besoin de stimulation intense pour rester motivé(e)" },
]
},
emotion: {
name: "Régulation émotionnelle",
description: "Gestion de la frustration et des affects",
questions: [
{ id: 'ef_emo_1', text: "Réactions émotionnelles excessives ou disproportionnées" },
{ id: 'ef_emo_2', text: "Difficulté à gérer la frustration et l'irritabilité" },
{ id: 'ef_emo_3', text: "Sensibilité excessive aux critiques ou aux commentaires négatifs" },
{ id: 'ef_emo_4', text: "Fluctuations d'humeur fréquentes au cours de la journée" },
]
},
memory: {
name: "Mémoire de travail",
description: "Mémoire active et rappel",
questions: [
{ id: 'ef_mem_1', text: "Oublier ce que l'on vient de lire et devoir relire plusieurs fois" },
{ id: 'ef_mem_2', text: "Difficulté à se souvenir des consignes ou instructions données verbalement" },
{ id: 'ef_mem_3', text: "Oublier fréquemment des rendez-vous ou des tâches prévues" },
{ id: 'ef_mem_4', text: "Perdre le fil de ses pensées en milieu de phrase ou d'action" },
]
},
action: {
name: "Inhibition / Action",
description: "Contrôle des impulsions et auto-régulation",
questions: [
{ id: 'ef_act2_1', text: "Agir ou parler impulsivement sans réfléchir aux conséquences" },
{ id: 'ef_act2_2', text: "Difficulté à rester assis(e) ou à se tenir tranquille" },
{ id: 'ef_act2_3', text: "Interrompre les autres ou s'imposer dans les conversations" },
{ id: 'ef_act2_4', text: "Difficulté à contrôler ses impulsions (achats, alimentation, etc.)" },
]
}
};
const EF_OPTIONS = [
{ value: 0, label: 'Pas un problème' },
{ value: 1, label: 'Rarement' },
{ value: 2, label: 'Parfois' },
{ value: 3, label: 'Souvent' }
];
// ============================================
// ANSWER PROGRESS (counter, fill line, milestones)
// Derived from the question lists above — totals are never hardcoded.
// ============================================
const ASRS_IDS = [...ASRS_QUESTIONS.part_a, ...ASRS_QUESTIONS.part_b].map(q => q.id);
const DIVA_IDS = [
...DIVA_INATTENTION, ...DIVA_HYPERACTIVITY,
...DIVA_CHILDHOOD, ...DIVA_IMPAIRMENT
].map(q => q.id);
const EXECUTIVE_IDS = Object.values(EXEC_FUNCTIONS).flatMap(c => c.questions.map(q => q.id));
const ALL_QUESTION_IDS = [...ASRS_IDS, ...DIVA_IDS, ...EXECUTIVE_IDS];
const TOTAL_QUESTIONS = ALL_QUESTION_IDS.length; // 70 = 18 ASRS + 28 DSM-5 + 24 fonctions exécutives
// Milestone boundaries: end of the ASRS block, half of the total,
// end of the DSM-5 block, end of everything. Copy finals from
// copy-fr.md §12 (progress.answered, milestones). If this tree is ever
// translated, copy-en.md §12 gives: "{n} of {total} answered · {m} to go",
// "First part done, well underway.", "You are halfway through.",
// "Two parts done, one to go.", "That's everything. Thank you for getting clear."
const PROGRESS_COPY = {
countPlural: (n, m) => `${n} questions répondues · il en reste ${m}`,
countSingular: (n, m) => `${n} question répondue · il en reste ${m}`,
countDone: (n) => `${n} questions répondues`,
milestones: {
'asrs.done': 'Première partie terminée, bien avancé.',
'half': 'Vous êtes à la moitié.',
'dsm.done': 'Deux parties terminées, reste la dernière.',
'all.done': "C'est complet. Merci d'avoir été au clair."
}
};
const MILESTONES = [
{ key: 'asrs.done', at: ASRS_IDS.length },
{ key: 'half', at: Math.floor(TOTAL_QUESTIONS / 2) },
{ key: 'dsm.done', at: ASRS_IDS.length + DIVA_IDS.length },
{ key: 'all.done', at: TOTAL_QUESTIONS }
];
function formatCountLine(answered, total) {
const remaining = total - answered;
if (remaining === 0) return PROGRESS_COPY.countDone(answered);
if (answered <= 1) return PROGRESS_COPY.countSingular(answered, remaining);
return PROGRESS_COPY.countPlural(answered, remaining);
}
// Highest milestone whose boundary the answered count has crossed, or null.
function crossedMilestone(answered) {
let hit = null;
for (const m of MILESTONES) {
if (answered >= m.at) hit = m;
else break;
}
return hit;
}
// Refresh the sticky answer-progress band: count line, 3px fill, and a
// milestone in the count line's slot when a boundary is just crossed
// (shown for the current screen, then the count line returns). The last
// milestone shown is persisted so a reload does not re-fire it.
function updateAnswerProgress() {
const band = document.getElementById('answer-progress');
const countEl = document.getElementById('answer-count');
const fill = document.getElementById('answer-fill');
if (!band || !countEl || !fill) return;
const visible = state.currentSection === 'asrs'
|| state.currentSection === 'diva'
|| state.currentSection === 'executive';
band.hidden = !visible;
if (!visible) return;
const answered = ALL_QUESTION_IDS.filter(id => state.responses[id] !== undefined).length;
fill.style.width = (answered / TOTAL_QUESTIONS * 100) + '%';
const milestone = crossedMilestone(answered);
if (milestone && milestone.key !== state.lastMilestone) {
countEl.textContent = PROGRESS_COPY.milestones[milestone.key];
state.lastMilestone = milestone.key;
saveState();
} else {
countEl.textContent = formatCountLine(answered, TOTAL_QUESTIONS);
}
}
// ============================================
// APP STATE
// ============================================
const state = {
currentSection: 'home',
responses: {},
results: null,
lastMilestone: null
};
// ============================================
// PERSISTENCE (localStorage shim)
// Survive reloads: answers and current section are saved locally,
// restored before first render. Silent no-op in private mode.
// ============================================
const STORAGE_KEY = 'clarte:v1';
function saveState() {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({
responses: state.responses,
currentSection: state.currentSection,
lastMilestone: state.lastMilestone
}));
} catch (e) { /* storage unavailable (private mode, quota): skip */ }
}
function clearSavedState() {
try {
localStorage.removeItem(STORAGE_KEY);
} catch (e) { /* storage unavailable: nothing to clear */ }
}
// Merge saved answers into state before first render, so question
// rendering (which reads state.responses) shows restored answers.
// Returns the saved section to resume at, or null.
function restoreState() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
const saved = JSON.parse(raw);
if (saved && typeof saved === 'object') {
if (saved.responses && typeof saved.responses === 'object') {
Object.assign(state.responses, saved.responses);
}
if (typeof saved.lastMilestone === 'string') {
state.lastMilestone = saved.lastMilestone;
}
if (sections.includes(saved.currentSection)) {
return saved.currentSection;
}
}
} catch (e) { /* corrupt or unavailable storage: start fresh */ }
return null;
}
// ============================================
// NAVIGATION
// ============================================
const sections = ['home', 'asrs', 'diva', 'executive', 'results'];
function showSection(sectionId) {
sections.forEach(s => {
const el = document.getElementById(s);
if (el) el.classList.remove('active');
});
const target = document.getElementById(sectionId);
if (target) {
target.classList.add('active');
state.currentSection = sectionId;
updateProgress();
updateAnswerProgress();
window.scrollTo(0, 0);
saveState();
}
}
function updateProgress() {
const currentIndex = sections.indexOf(state.currentSection);
const dots = document.querySelectorAll('.step-dot');
const connectors = document.querySelectorAll('.step-connector');
const label = document.querySelector('.progress-label');
dots.forEach((dot, i) => {
dot.classList.remove('active', 'completed');
if (i < currentIndex) dot.classList.add('completed');
if (i === currentIndex) dot.classList.add('active');
});
connectors.forEach((conn, i) => {
conn.classList.remove('completed');
if (i < currentIndex) conn.classList.add('completed');
});
const labels = ['Accueil', 'ASRS (OMS)', 'Critères DSM-5', 'Fonctions Exécutives', 'Résultats'];
if (label) label.textContent = labels[currentIndex] || '';
}
// ============================================
// RENDER QUESTIONS
// ============================================
function renderASRS() {
const container = document.getElementById('asrs-questions');
let html = '';
// Part A
html += `<div class="question-group">
<h3>Partie A — Dépistage</h3>
<p class="hint">Ces 6 questions sont les plus prédictives du TDAH.</p>`;
ASRS_QUESTIONS.part_a.forEach((q, i) => {
html += renderQuestion(q, i + 1, ASRS_OPTIONS);
});
html += '</div>';
// Part B
html += `<div class="question-group">
<h3>Partie B — Questions complémentaires</h3>`;
ASRS_QUESTIONS.part_b.forEach((q, i) => {
html += renderQuestion(q, i + 7, ASRS_OPTIONS);
});
html += '</div>';
container.innerHTML = html;
attachOptionHandlers(container);
}
function renderDIVA() {
const container = document.getElementById('diva-questions');
let html = '';
// Inattention
html += `<div class="question-group">
<h3>Critère A1 — Inattention</h3>
<p class="hint">Pour les adultes, ≥5 symptômes sont requis. Répondez Oui si le symptôme est fréquent depuis plus de 6 mois.</p>`;
DIVA_INATTENTION.forEach((q, i) => {
html += renderBinaryQuestion(q, i + 1);
});
html += '</div>';
// Hyperactivity
html += `<div class="question-group">
<h3>Critère A2 — Hyperactivité / Impulsivité</h3>`;
DIVA_HYPERACTIVITY.forEach((q, i) => {
html += renderBinaryQuestion(q, i + 1);
});
html += '</div>';
// Childhood
html += `<div class="question-group">
<h3>Critère B — Symptômes dans l'enfance</h3>
<p class="hint">Le DSM-5 exige que des symptômes aient été présents avant l'âge de 12 ans.</p>`;
DIVA_CHILDHOOD.forEach((q, i) => {
html += renderBinaryQuestion(q, i + 1);
});
html += '</div>';
// Impairment
html += `<div class="question-group">
<h3>Critères C/D — Retentissement fonctionnel</h3>
<p class="hint">Les symptômes doivent causer une gêne significative dans au moins 2 domaines.</p>`;
DIVA_IMPAIRMENT.forEach((q, i) => {
html += renderBinaryQuestion(q, i + 1);
});
html += '</div>';
container.innerHTML = html;
attachOptionHandlers(container);
}
function renderExecutive() {
const container = document.getElementById('executive-questions');
let html = '';
Object.values(EXEC_FUNCTIONS).forEach(cluster => {
html += `<div class="question-group">
<h3>${cluster.name}</h3>
<p class="hint">${cluster.description}</p>`;
cluster.questions.forEach((q, i) => {
html += renderQuestion(q, i + 1, EF_OPTIONS, true);
});
html += '</div>';
});
container.innerHTML = html;
attachOptionHandlers(container);
}
function renderQuestion(q, num, options, compact = false) {
const answered = state.responses[q.id] !== undefined;
return `
<div class="question ${answered ? 'answered' : ''}" data-id="${q.id}">
<p class="question-text"><span class="question-number">${num}.</span> ${q.text}</p>
<div class="options">
${options.map(opt => `
<div class="option">
<input type="radio" name="${q.id}" id="${q.id}_${opt.value}" value="${opt.value}"
${state.responses[q.id] === opt.value ? 'checked' : ''}>
<label for="${q.id}_${opt.value}">${opt.label}</label>
</div>
`).join('')}
</div>
</div>
`;
}
function renderBinaryQuestion(q, num) {
const answered = state.responses[q.id] !== undefined;
return `
<div class="question ${answered ? 'answered' : ''}" data-id="${q.id}">
<p class="question-text"><span class="question-number">${num}.</span> ${q.text}</p>
<div class="options binary">
<div class="option yes">
<input type="radio" name="${q.id}" id="${q.id}_1" value="1"
${state.responses[q.id] === 1 ? 'checked' : ''}>
<label for="${q.id}_1">Oui</label>
</div>
<div class="option no">
<input type="radio" name="${q.id}" id="${q.id}_0" value="0"
${state.responses[q.id] === 0 ? 'checked' : ''}>
<label for="${q.id}_0">Non</label>
</div>
</div>
</div>
`;
}
function attachOptionHandlers(container) {
container.querySelectorAll('input[type="radio"]').forEach(input => {
input.addEventListener('change', (e) => {
state.responses[e.target.name] = parseInt(e.target.value);
saveState();
updateAnswerProgress();
const question = e.target.closest('.question');
if (question) question.classList.add('answered');
});
});
}
// ============================================
// SCORING
// ============================================
function calculateResults() {
const results = {
asrs: scoreASRS(),
diva: scoreDIVA(),
executive: scoreExecutive()
};
// Global assessment
let positiveIndicators = 0;
if (results.asrs.screeningPositive) positiveIndicators++;
if (results.diva.meetsInattention || results.diva.meetsHyperactivity) positiveIndicators++;
if (results.executive.severity !== 'Faible') positiveIndicators++;
results.global = {
positiveIndicators,
confidence: positiveIndicators >= 3 ? 'Élevée' : positiveIndicators >= 2 ? 'Modérée' : 'Faible'
};
state.results = results;
return results;
}
function scoreASRS() {
let partAScore = 0, partAShaded = 0, partBScore = 0;
let inattention = 0, hyperactivity = 0;
const inattentionQs = [1, 2, 3, 4, 7, 8, 9, 10, 11];
ASRS_QUESTIONS.part_a.forEach(q => {
const val = state.responses[q.id] || 0;
partAScore += val;
if (val >= q.threshold) partAShaded++;
const num = parseInt(q.id.split('_')[1]);
if (inattentionQs.includes(num)) inattention += val;
else hyperactivity += val;
});
ASRS_QUESTIONS.part_b.forEach(q => {
const val = state.responses[q.id] || 0;
partBScore += val;
const num = parseInt(q.id.split('_')[1]);
if (inattentionQs.includes(num)) inattention += val;
else hyperactivity += val;
});
const screeningPositive = partAShaded >= 4;
return {
partAScore,
partAShaded,
partBScore,
total: partAScore + partBScore,
inattention,
hyperactivity,
screeningPositive,
interpretation: screeningPositive
? "Le dépistage est POSITIF. Vos réponses suggèrent des symptômes compatibles avec un TDAH chez l'adulte."
: "Le dépistage est négatif. Une évaluation clinique reste recommandée si vous avez des préoccupations."
};
}
function scoreDIVA() {
let inattentionCount = 0, hyperactivityCount = 0, childhoodCount = 0;
const impairmentDomains = [];
DIVA_INATTENTION.forEach(q => {
if (state.responses[q.id] === 1) inattentionCount++;
});
DIVA_HYPERACTIVITY.forEach(q => {
if (state.responses[q.id] === 1) hyperactivityCount++;
});
DIVA_CHILDHOOD.forEach(q => {
if (state.responses[q.id] === 1) childhoodCount++;
});
DIVA_IMPAIRMENT.forEach(q => {
if (state.responses[q.id] === 1) impairmentDomains.push(q.domain);
});
const meetsInattention = inattentionCount >= 5;
const meetsHyperactivity = hyperactivityCount >= 5;
let presentation = "Sous les seuils cliniques";
if (meetsInattention && meetsHyperactivity) presentation = "Inattention + Hyperactivité/Impulsivité";
else if (meetsInattention) presentation = "Inattention prédominante";
else if (meetsHyperactivity) presentation = "Hyperactivité/Impulsivité prédominante";
return {
inattentionCount,
hyperactivityCount,
childhoodCount,
childhoodPositive: childhoodCount >= 2,
impairmentDomains,
meetsInattention,
meetsHyperactivity,
presentation
};
}
function scoreExecutive() {
const clusterScores = {};
let total = 0, maxTotal = 0;
Object.entries(EXEC_FUNCTIONS).forEach(([key, cluster]) => {
let score = 0;
cluster.questions.forEach(q => {
score += state.responses[q.id] || 0;
});
const max = cluster.questions.length * 3;
clusterScores[cluster.name] = { score, max, percentage: Math.round((score / max) * 100) };
total += score;
maxTotal += max;
});
const percentage = Math.round((total / maxTotal) * 100);
let severity = 'Faible';
if (percentage >= 66) severity = 'Élevé';
else if (percentage >= 40) severity = 'Modéré';
const impaired = Object.entries(clusterScores)
.filter(([_, s]) => s.percentage >= 50)
.map(([name, _]) => name);
return { clusterScores, total, maxTotal, percentage, severity, impaired };
}
// ============================================
// RENDER RESULTS
// ============================================
function renderResults() {
const results = calculateResults();
const container = document.getElementById('results-content');
const asrsLevel = results.asrs.screeningPositive ? 'high' : 'low';
const divaLevel = (results.diva.meetsInattention || results.diva.meetsHyperactivity) ? 'high' : 'low';
const efLevel = results.executive.severity === 'Élevé' ? 'high' : results.executive.severity === 'Modéré' ? 'moderate' : 'low';
let html = `
<div class="summary-card">
<h2>Synthèse</h2>
<p class="summary-text">
<strong>${results.global.positiveIndicators}/3</strong> outils d'évaluation suggèrent des symptômes
${results.global.positiveIndicators >= 2 ? 'évocateurs, méritant une évaluation clinique' : 'à explorer si besoin'}.
</p>
${results.asrs.screeningPositive ? '<p>• ASRS: Seuil de dépistage <strong>atteint</strong></p>' : ''}
${results.diva.meetsInattention || results.diva.meetsHyperactivity ?
`<p>• Critères DSM-5: <strong>${results.diva.presentation}</strong></p>` : ''}
${results.executive.severity !== 'Faible' ?
`<p>• Fonctions exécutives: Difficultés de niveau <strong>${results.executive.severity}</strong></p>` : ''}
<div class="recommendation">
<strong>Recommandation:</strong>
${results.global.positiveIndicators >= 2
? "Une évaluation clinique par un spécialiste (psychiatre, neurologue) est recommandée pour explorer ces symptômes. Apportez ce rapport lors de votre consultation."
: "Si ces difficultés impactent votre quotidien, une consultation pourrait être utile pour explorer les causes possibles."}
</div>
</div>
<div class="result-card ${asrsLevel}">
<h3>1. ASRS v1.1 — Dépistage OMS</h3>
<div class="result-status ${asrsLevel}">
Seuil ${results.asrs.screeningPositive ? 'atteint' : 'non atteint'}
(${results.asrs.partAShaded}/6 critères, seuil ≥4)
</div>
<div class="scores-grid">
<div class="score-item">
<span class="score-label">Partie A</span>
<span class="score-value">${results.asrs.partAScore}/24</span>
</div>
<div class="score-item">
<span class="score-label">Partie B</span>
<span class="score-value">${results.asrs.partBScore}/48</span>
</div>
<div class="score-item">
<span class="score-label">Total</span>
<span class="score-value">${results.asrs.total}/72</span>
</div>
</div>
<div class="progress-item">
<div class="progress-label-row">
<span>Inattention</span>
<span>${results.asrs.inattention}/36</span>
</div>
<div class="progress-track">
<div class="progress-fill" style="width: ${Math.round(results.asrs.inattention / 36 * 100)}%"></div>
</div>
</div>
<div class="progress-item">
<div class="progress-label-row">
<span>Hyperactivité/Impulsivité</span>
<span>${results.asrs.hyperactivity}/36</span>
</div>
<div class="progress-track">
<div class="progress-fill" style="width: ${Math.round(results.asrs.hyperactivity / 36 * 100)}%"></div>
</div>
</div>
</div>
<div class="result-card ${divaLevel}">
<h3>2. Critères DSM-5</h3>
<div class="result-status ${divaLevel}">
${results.diva.presentation}
</div>
<div class="scores-grid">
<div class="score-item">
<span class="score-label">Inattention (A1)</span>
<span class="score-value">${results.diva.inattentionCount}/9</span>
</div>
<div class="score-item">
<span class="score-label">Hyperactivité (A2)</span>
<span class="score-value">${results.diva.hyperactivityCount}/9</span>
</div>
<div class="score-item">
<span class="score-label">Enfance (B)</span>
<span class="score-value">${results.diva.childhoodPositive ? '✓' : '✗'}</span>
</div>
<div class="score-item">
<span class="score-label">Retentissement</span>
<span class="score-value">${results.diva.impairmentDomains.length}/5</span>
</div>
</div>
${results.diva.impairmentDomains.length > 0 ? `
<p class="interpretation">Domaines impactés: ${results.diva.impairmentDomains.join(', ')}</p>
` : ''}
</div>
<div class="result-card ${efLevel}">
<h3>3. Fonctions Exécutives</h3>
<div class="result-status ${efLevel}">
Niveau de difficulté: ${results.executive.severity} (${results.executive.percentage}%)
</div>
${Object.entries(results.executive.clusterScores).map(([name, s]) => `
<div class="progress-item">
<div class="progress-label-row">
<span>${name}</span>
<span>${s.score}/${s.max}</span>
</div>
<div class="progress-track">
<div class="progress-fill ${s.percentage >= 50 ? 'high' : ''}" style="width: ${s.percentage}%"></div>
</div>
</div>
`).join('')}
${results.executive.impaired.length > 0 ? `
<p class="interpretation">Domaines les plus impactés: ${results.executive.impaired.join(', ')}</p>
` : ''}
</div>
`;
container.innerHTML = html;
}
// ============================================
// PDF GENERATION
// ============================================
function generatePDF() {
const { jsPDF } = window.jspdf;
const doc = new jsPDF();
const results = state.results;
const now = new Date().toLocaleString('fr-FR');
let y = 20;
const margin = 20;
const pageWidth = doc.internal.pageSize.width;
// Helper functions
const addTitle = (text, size = 16) => {
doc.setFontSize(size);
doc.setFont(undefined, 'bold');
doc.text(text, margin, y);
y += size * 0.5;
};
const addText = (text, size = 11) => {
doc.setFontSize(size);
doc.setFont(undefined, 'normal');
const lines = doc.splitTextToSize(text, pageWidth - 2 * margin);
doc.text(lines, margin, y);
y += lines.length * size * 0.4 + 2;
};
const addLine = () => {
doc.setDrawColor(200);
doc.line(margin, y, pageWidth - margin, y);
y += 5;
};
const checkPage = (needed = 30) => {
if (y + needed > 280) {
doc.addPage();
y = 20;
}
};
// Title
addTitle('Rapport d\'Auto-Évaluation TDAH Adulte', 18);
y += 5;
addText(`Généré le ${now}`);
addLine();
// Disclaimer
doc.setFillColor(255, 243, 205);
doc.rect(margin, y, pageWidth - 2 * margin, 25, 'F');
y += 5;
doc.setFontSize(9);
doc.setFont(undefined, 'bold');
doc.text('AVERTISSEMENT', margin + 2, y);
y += 4;
doc.setFont(undefined, 'normal');
const disclaimer = 'Ce document est un outil d\'AUTO-ÉVALUATION et NE CONSTITUE PAS un diagnostic médical. Seul un professionnel de santé qualifié peut établir un diagnostic de TDAH.';
const disclaimerLines = doc.splitTextToSize(disclaimer, pageWidth - 2 * margin - 4);
doc.text(disclaimerLines, margin + 2, y);
y += 20;
// Summary
checkPage(50);
addTitle('Synthèse des Résultats', 14);
y += 3;
addText(`${results.global.positiveIndicators}/3 outils suggèrent des symptômes ${results.global.positiveIndicators >= 2 ? 'évocateurs, méritant une évaluation clinique' : 'à explorer si besoin'}.`);
if (results.asrs.screeningPositive) addText('• ASRS: Seuil de dépistage atteint');
if (results.diva.meetsInattention || results.diva.meetsHyperactivity)
addText(`• Critères DSM-5: ${results.diva.presentation}`);
if (results.executive.severity !== 'Faible')
addText(`• Fonctions exécutives: Difficultés de niveau ${results.executive.severity}`);
y += 5;
addLine();
// ASRS Results
checkPage(40);
addTitle('1. ASRS v1.1 (OMS/Harvard)', 12);
addText(`Seuil: ${results.asrs.screeningPositive ? 'Atteint' : 'Non atteint'} (${results.asrs.partAShaded}/6 critères)`);
addText(`Score total: ${results.asrs.total}/72 | Inattention: ${results.asrs.inattention}/36 | Hyperactivité: ${results.asrs.hyperactivity}/36`);
y += 3;
// DIVA Results
checkPage(40);
addTitle('2. Critères DSM-5', 12);
addText(`Présentation: ${results.diva.presentation}`);
addText(`Inattention: ${results.diva.inattentionCount}/9 | Hyperactivité: ${results.diva.hyperactivityCount}/9`);
addText(`Enfance: ${results.diva.childhoodPositive ? 'Confirmé' : 'Non confirmé'} | Retentissement: ${results.diva.impairmentDomains.length} domaines`);
y += 3;
// Executive Functions
checkPage(50);
addTitle('3. Fonctions Exécutives', 12);
addText(`Niveau global: ${results.executive.severity} (${results.executive.percentage}%)`);
Object.entries(results.executive.clusterScores).forEach(([name, s]) => {
addText(` • ${name}: ${s.score}/${s.max} (${s.percentage}%)`);
});
// New page for detailed responses
doc.addPage();
y = 20;
addTitle('Annexe: Réponses Détaillées', 14);
addLine();
// ASRS responses
addTitle('ASRS v1.1', 11);
[...ASRS_QUESTIONS.part_a, ...ASRS_QUESTIONS.part_b].forEach((q, i) => {
checkPage(15);
const val = state.responses[q.id];
const label = ASRS_OPTIONS.find(o => o.value === val)?.label || 'Non répondu';
doc.setFontSize(9);
const qText = doc.splitTextToSize(`${i + 1}. ${q.text}`, pageWidth - 2 * margin - 40);
doc.text(qText, margin, y);
doc.text(label, pageWidth - margin - 30, y);
y += qText.length * 4 + 2;
});
// DIVA responses
checkPage(20);
addTitle('Critères DSM-5', 11);
[...DIVA_INATTENTION, ...DIVA_HYPERACTIVITY, ...DIVA_CHILDHOOD, ...DIVA_IMPAIRMENT].forEach((q, i) => {
checkPage(12);
const val = state.responses[q.id];
doc.setFontSize(9);
const qText = doc.splitTextToSize(`${q.text}`, pageWidth - 2 * margin - 20);
doc.text(qText, margin, y);
doc.text(val === 1 ? 'Oui' : 'Non', pageWidth - margin - 15, y);
y += qText.length * 4 + 2;
});
// References page
doc.addPage();
y = 20;
addTitle('Références Scientifiques', 14);
addLine();
doc.setFontSize(10);
addText('1. Kessler RC, et al. (2005). The World Health Organization Adult ADHD Self-Report Scale (ASRS).');
addText(' Psychological Medicine, 35(2), 245-256. https://pubmed.ncbi.nlm.nih.gov/15841682/');
y += 3;
addText('2. American Psychiatric Association (2013). Diagnostic and Statistical Manual of Mental Disorders (5th ed.).');
addText(' https://www.psychiatry.org/psychiatrists/practice/dsm');
y += 3;
addText('3. Kooij JJS, et al. (2010). DIVA 2.0: Diagnostic Interview for ADHD in Adults.');
addText(' DIVA Foundation. https://www.divacenter.eu/');
y += 3;
addText('4. Brown TE (2013). A New Understanding of ADHD in Children and Adults.');
addText(' Executive Function Impairments. Routledge. https://pubmed.ncbi.nlm.nih.gov/24232170/');
// Footer on each page
const pageCount = doc.internal.getNumberOfPages();
for (let i = 1; i <= pageCount; i++) {
doc.setPage(i);
doc.setFontSize(8);
doc.setTextColor(128);
doc.text(`Page ${i}/${pageCount} | Ce document ne constitue pas un diagnostic médical`, margin, 290);
}
doc.save('evaluation_tdah.pdf');
}
// ============================================
// VALIDATION
// ============================================
function validateSection(section) {
let questions = [];
if (section === 'asrs') {
questions = [...ASRS_QUESTIONS.part_a, ...ASRS_QUESTIONS.part_b].map(q => q.id);
} else if (section === 'diva') {
questions = [
...DIVA_INATTENTION, ...DIVA_HYPERACTIVITY,
...DIVA_CHILDHOOD, ...DIVA_IMPAIRMENT
].map(q => q.id);
} else if (section === 'executive') {
questions = Object.values(EXEC_FUNCTIONS).flatMap(c => c.questions.map(q => q.id));
}
const answered = questions.filter(id => state.responses[id] !== undefined).length;
return answered === questions.length;
}
// ============================================
// EVENT HANDLERS
// ============================================
function init() {
// Restore saved answers BEFORE first render so radios render checked
const savedSection = restoreState();
// Render all questionnaires
renderASRS();
renderDIVA();
renderExecutive();
// Consent checkbox
document.getElementById('consent').addEventListener('change', (e) => {
document.getElementById('start-btn').disabled = !e.target.checked;
});
// Navigation buttons
document.getElementById('start-btn').addEventListener('click', () => showSection('asrs'));
document.getElementById('asrs-next').addEventListener('click', () => {
if (!validateSection('asrs')) {
alert('Veuillez répondre à toutes les questions avant de continuer.');
return;
}
showSection('diva');
});
document.getElementById('diva-back').addEventListener('click', () => showSection('asrs'));
document.getElementById('diva-next').addEventListener('click', () => {
if (!validateSection('diva')) {
alert('Veuillez répondre à toutes les questions avant de continuer.');
return;
}
showSection('executive');
});
document.getElementById('exec-back').addEventListener('click', () => showSection('diva'));
document.getElementById('exec-next').addEventListener('click', () => {
if (!validateSection('executive')) {
alert('Veuillez répondre à toutes les questions avant de continuer.');
return;
}
renderResults();
showSection('results');
});
document.getElementById('download-pdf').addEventListener('click', generatePDF);
document.getElementById('restart').addEventListener('click', () => {
state.responses = {};
state.results = null;
state.lastMilestone = null;
clearSavedState();
renderASRS();
renderDIVA();
renderExecutive();
showSection('home');
});
// Initialize
showSection('home');
// Resume where the user left off (after showSection('home') above)
if (savedSection && savedSection !== 'home') {
if (savedSection === 'results') {
renderResults(); // recomputes purely from state.responses
}
showSection(savedSection);
}
}
// Start app when DOM is ready
document.addEventListener('DOMContentLoaded', init);
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Auto-évaluation scientifique du TDAH chez l'adulte. Outils validés: ASRS (OMS), critères DSM-5, fonctions exécutives.">
<meta name="theme-color" content="#5B8A8A">
<link rel="manifest" href="manifest.webmanifest">
<!-- Open Graph / Social -->
<meta property="og:type" content="website">
<meta property="og:url" content="https://plnech.github.io/adhd/">
<meta property="og:title" content="Clarté — Auto-Évaluation TDAH Adulte">
<meta property="og:description" content="Évaluez vos symptômes avec des outils scientifiquement validés: ASRS (OMS), critères DSM-5, fonctions exécutives. Gratuit, confidentiel, avec rapport PDF.">
<meta property="og:image" content="https://plnech.github.io/adhd/og-image.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<!-- Twitter -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Clarté — Auto-Évaluation TDAH Adulte">
<meta name="twitter:description" content="Évaluez vos symptômes avec des outils scientifiquement validés. Gratuit et confidentiel.">
<meta name="twitter:image" content="https://plnech.github.io/adhd/og-image.png">
<title>Clarté — Auto-Évaluation TDAH Adulte</title>
<link rel="stylesheet" href="style.css">
<script src="jspdf.min.js"></script>
</head>
<body>
<header>
<div class="container">
<div class="logo">
<div class="logo-icon">◎</div>
</div>
<h1>Clarté</h1>
<p class="subtitle">Auto-évaluation TDAH pour adultes</p>
</div>
</header>
<nav class="progress-bar">
<div class="container">
<div class="progress-steps">
<div class="step-dot"></div>
<div class="step-connector"></div>
<div class="step-dot"></div>
<div class="step-connector"></div>
<div class="step-dot"></div>
<div class="step-connector"></div>
<div class="step-dot"></div>
<div class="step-connector"></div>
<div class="step-dot"></div>
</div>
<p class="progress-label">Accueil</p>
<div class="answer-progress" id="answer-progress" hidden>
<p class="answer-count" id="answer-count"></p>
<div class="answer-fill-track">
<div class="answer-fill" id="answer-fill"></div>
</div>
</div>
</div>
</nav>
<main class="container">
<!-- HOME -->
<section id="home" class="section active">
<div class="card">
<div class="card-header">
<h2>Bienvenue</h2>
</div>
<p>
Cette application vous permet de réaliser une auto-évaluation structurée
des symptômes du <strong>Trouble Déficit de l'Attention avec ou sans Hyperactivité (TDAH)</strong>
chez l'adulte, en utilisant des outils scientifiquement validés.
</p>
<div class="intro-grid">
<div class="intro-card">
<div class="intro-card-icon teal">📋</div>
<div>
<h4>ASRS v1.1</h4>
<p>18 questions validées par l'OMS et Harvard pour le dépistage du TDAH adulte.</p>
</div>
</div>
<div class="intro-card">
<div class="intro-card-icon coral">🔬</div>
<div>
<h4>Critères DSM-5</h4>
<p>Évaluation structurée des 18 critères diagnostiques officiels, incluant l'enfance.</p>
</div>
</div>
<div class="intro-card">
<div class="intro-card-icon lavender">🧠</div>
<div>
<h4>Fonctions Exécutives</h4>
<p>6 domaines cognitifs selon le modèle de Brown: attention, mémoire, régulation.</p>
</div>
</div>
</div>
<div class="notice warning">
<h4>⚠️ Important — Veuillez lire attentivement</h4>
<ul>
<li><strong>Cette évaluation ne constitue PAS un diagnostic médical.</strong></li>
<li>Seul un professionnel de santé qualifié (psychiatre, neurologue) peut établir un diagnostic de TDAH.</li>
<li>Les résultats sont destinés à faciliter la discussion avec votre médecin.</li>
<li>Répondez honnêtement en pensant à votre fonctionnement des <strong>6 derniers mois</strong>.</li>
<li>Durée estimée: <strong>15-20 minutes</strong>.</li>
<li>Vos données restent sur votre appareil et ne sont jamais transmises.</li>
</ul>
</div>
<label class="checkbox-wrapper">
<input type="checkbox" id="consent">
<span>
Je comprends que cette auto-évaluation ne constitue pas un diagnostic médical
et que je dois consulter un professionnel de santé pour toute évaluation diagnostique.
</span>
</label>
<div style="text-align: center;">
<button id="start-btn" class="btn btn-primary btn-large" disabled>
Commencer l'évaluation
</button>
</div>
</div>
</section>
<!-- ASRS -->
<section id="asrs" class="section">
<div class="card">
<div class="card-header">
<h2>ASRS v1.1 — Échelle d'auto-évaluation</h2>
<p class="source">Organisation Mondiale de la Santé / Harvard Medical School</p>
</div>
<div class="notice info">
<p>
Pour chaque question, indiquez à quelle fréquence vous avez ressenti ou agi
de cette manière au cours des <strong>6 derniers mois</strong>.
</p>
</div>
<div id="asrs-questions"></div>
<div class="actions">
<div></div>
<button id="asrs-next" class="btn btn-primary">
Continuer →
</button>
</div>
</div>
</section>
<!-- DIVA -->
<section id="diva" class="section">
<div class="card">
<div class="card-header">
<h2>Critères DSM-5</h2>
<p class="source">American Psychiatric Association (2013)</p>
</div>
<div class="notice info">
<p>
Répondez <strong>Oui</strong> si le symptôme est présent de manière fréquente et persistante
depuis au moins <strong>6 mois</strong>, et qu'il interfère avec votre fonctionnement.
</p>
</div>
<div id="diva-questions"></div>
<div class="actions">
<button id="diva-back" class="btn btn-secondary">
← Retour
</button>
<button id="diva-next" class="btn btn-primary">
Continuer →
</button>
</div>
</div>
</section>
<!-- EXECUTIVE FUNCTIONS -->
<section id="executive" class="section">
<div class="card">
<div class="card-header">
<h2>Fonctions Exécutives</h2>
<p class="source">Basé sur le modèle de Brown (2013)</p>
</div>
<div class="notice info">
<p>
Les fonctions exécutives sont les capacités cognitives qui nous permettent de gérer
notre attention, nos émotions et nos comportements. Indiquez à quel point chaque
difficulté est un problème pour vous <strong>actuellement</strong>.
</p>
</div>
<div id="executive-questions"></div>
<div class="actions">
<button id="exec-back" class="btn btn-secondary">
← Retour
</button>
<button id="exec-next" class="btn btn-primary">
Voir les résultats →
</button>
</div>
</div>
</section>
<!-- RESULTS -->
<section id="results" class="section">
<div class="notice warning">
<p>
<strong>Rappel:</strong> Ces résultats constituent une AUTO-ÉVALUATION et non un diagnostic médical.
Seul un professionnel de santé qualifié peut établir un diagnostic de TDAH.
</p>
</div>
<div id="results-content"></div>
<div style="text-align: center; margin-top: 2rem;">
<button id="download-pdf" class="btn btn-primary btn-large">
📄 Télécharger le rapport PDF
</button>
<p style="color: var(--ink-light); font-size: 0.9rem; margin-top: 1rem;">
Le rapport inclut une synthèse et les réponses détaillées pour votre médecin.
</p>
<button id="restart" class="btn btn-secondary" style="margin-top: 1rem;">
Recommencer l'évaluation
</button>
</div>
</section>
</main>
<footer>
<div class="container">
<p class="disclaimer">
<strong>Avertissement:</strong> Cet outil est fourni à titre informatif uniquement.
Il ne constitue pas un diagnostic médical. Consultez un professionnel de santé qualifié.
</p>
<p><strong>Références scientifiques:</strong></p>
<p style="font-size: 0.8rem; line-height: 1.8;">
<a href="https://pubmed.ncbi.nlm.nih.gov/15841682/" target="_blank" rel="noopener">ASRS v1.1 — Kessler et al. (2005)</a> •
<a href="https://www.psychiatry.org/psychiatrists/practice/dsm" target="_blank" rel="noopener">DSM-5 — APA (2013)</a> •
<a href="https://www.divacenter.eu/" target="_blank" rel="noopener">DIVA Foundation</a> •
<a href="https://pubmed.ncbi.nlm.nih.gov/24232170/" target="_blank" rel="noopener">Brown EF Model (2013)</a>
</p>
<p style="margin-top: 1rem; font-size: 0.8rem;">
<a href="https://github.com/PLNech/adhd" target="_blank" rel="noopener">Code source</a> •
MIT License © 2025 PLNech
</p>
</div>
</footer>
<script src="app.js"></script>
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js').catch(function () {});
}
</script>
</body>
</html>
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "Clarté — Auto-Évaluation TDAH",
"short_name": "Clarté TDAH",
"lang": "fr",
"start_url": ".",
"display": "standalone",
"background_color": "#FAF8F5",
"theme_color": "#5B8A8A"
}
<svg width="1200" height="630" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- Main gradient: warm cream to soft teal -->
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#FAF8F5"/>
<stop offset="30%" style="stop-color:#F0EDE8"/>
<stop offset="70%" style="stop-color:#E8F0F0"/>
<stop offset="100%" style="stop-color:#D8E8E8"/>
</linearGradient>
<!-- Accent blob gradients -->
<radialGradient id="blob1" cx="20%" cy="30%" r="40%">
<stop offset="0%" style="stop-color:#A89BBF;stop-opacity:0.15"/>
<stop offset="100%" style="stop-color:#A89BBF;stop-opacity:0"/>
</radialGradient>
<radialGradient id="blob2" cx="80%" cy="70%" r="35%">
<stop offset="0%" style="stop-color:#D4A27F;stop-opacity:0.12"/>
<stop offset="100%" style="stop-color:#D4A27F;stop-opacity:0"/>
</radialGradient>
<radialGradient id="blob3" cx="60%" cy="20%" r="30%">
<stop offset="0%" style="stop-color:#5B8A8A;stop-opacity:0.08"/>
<stop offset="100%" style="stop-color:#5B8A8A;stop-opacity:0"/>
</radialGradient>
</defs>
<!-- Background -->
<rect width="1200" height="630" fill="url(#bgGradient)"/>
<!-- Soft blobs -->
<ellipse cx="240" cy="190" rx="300" ry="250" fill="url(#blob1)"/>
<ellipse cx="960" cy="440" rx="280" ry="220" fill="url(#blob2)"/>
<ellipse cx="720" cy="126" rx="240" ry="180" fill="url(#blob3)"/>
<!-- Decorative circle/logo -->
<circle cx="600" cy="200" r="60" fill="#5B8A8A" opacity="0.9"/>
<circle cx="600" cy="200" r="45" fill="none" stroke="#FAF8F5" stroke-width="3"/>
<circle cx="600" cy="200" r="8" fill="#FAF8F5"/>
<!-- Title -->
<text x="600" y="340" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="72" font-weight="700" fill="#3A3632">
Clarté
</text>
<!-- Subtitle -->
<text x="600" y="410" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="32" fill="#5B8A8A">
Auto-Évaluation TDAH Adulte
</text>
<!-- Tagline -->
<text x="600" y="480" text-anchor="middle" font-family="system-ui, -apple-system, sans-serif" font-size="22" fill="#6B6560">
Outils scientifiquement validés • ASRS • DSM-5 • Fonctions Exécutives
</text>
<!-- Bottom accent line -->
<rect x="400" y="530" width="400" height="4" rx="2" fill="#5B8A8A" opacity="0.6"/>
</svg>
/*
* Clarté Theme - ADHD Self-Evaluation
* A soothing, paper-like design focused on clarity and calm
*/
/* ============================================
FONTS - Atkinson Hyperlegible (self-hosted)
Designed for readability by Braille Institute
============================================ */
@font-face {
font-family: 'Atkinson';
src: url('fonts/AtkinsonHyperlegible-Regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Atkinson';
src: url('fonts/AtkinsonHyperlegible-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Atkinson';
src: url('fonts/AtkinsonHyperlegible-Italic.woff2') format('woff2');
font-weight: 400;
font-style: italic;
font-display: swap;
}
/* ============================================
COLOR PALETTE - Warm, soothing, paper-like
============================================ */
:root {
/* Paper & Background */
--paper: #FAF8F5;
--paper-dark: #F0EDE8;
--paper-shadow: #E5E0D8;
/* Text */
--ink: #3A3632;
--ink-light: #6B6560;
--ink-faint: #9A9590;
/* Primary - Calming Teal */
--teal: #5B8A8A;
--teal-light: #7BA8A8;
--teal-dark: #4A7272;
--teal-faint: #E8F0F0;
/* Accent - Warm Coral */
--coral: #D4A27F;
--coral-light: #E8C4A8;
--coral-faint: #FDF5F0;
/* Accent - Soft Lavender */
--lavender: #A89BBF;
--lavender-faint: #F5F3F8;
/* Semantic - Purple scale for results (non-alarming) */
--sage: #7D9E83;
--sage-faint: #EFF5F0;
--purple-light: #B8A9C9;
--purple-light-faint: #F5F2F8;
--purple-mid: #9683A8;
--purple-mid-faint: #F0ECF5;
--purple-deep: #7B6B8A;
--purple-deep-faint: #EBE7F0;
/* Spacing */
--space-xs: 0.25rem;
--space-sm: 0.5rem;
--space-md: 1rem;
--space-lg: 1.5rem;
--space-xl: 2rem;
--space-2xl: 3rem;
/* Radius */
--radius-sm: 6px;
--radius-md: 12px;
--radius-lg: 20px;
--radius-full: 9999px;
/* Shadows - soft, warm */
--shadow-sm: 0 1px 3px rgba(58, 54, 50, 0.08);
--shadow-md: 0 4px 12px rgba(58, 54, 50, 0.1);
--shadow-lg: 0 8px 24px rgba(58, 54, 50, 0.12);
}
/* ============================================
BASE STYLES
============================================ */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: 16px;
scroll-behavior: smooth;
}
body {
font-family: 'Atkinson', -apple-system, BlinkMacSystemFont, sans-serif;
line-height: 1.65;
color: var(--ink);
background: var(--paper);
min-height: 100vh;
/* Subtle paper texture */
background-image:
radial-gradient(ellipse at 20% 30%, rgba(168, 155, 191, 0.03) 0%, transparent 50%),
radial-gradient(ellipse at 80% 70%, rgba(212, 162, 127, 0.03) 0%, transparent 50%);
}
/* ============================================
TYPOGRAPHY
============================================ */
h1, h2, h3, h4 {
font-weight: 700;
line-height: 1.3;
color: var(--ink);
}
h1 {
font-size: 2rem;
letter-spacing: -0.02em;
}
h2 {
font-size: 1.5rem;
color: var(--teal-dark);
}
h3 {
font-size: 1.2rem;
}
p {
margin-bottom: var(--space-md);
}
a {
color: var(--teal);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.2s;
}
a:hover {
border-bottom-color: var(--teal);
}
/* ============================================
LAYOUT
============================================ */
.container {
max-width: 720px;
margin: 0 auto;
padding: 0 var(--space-lg);
}
/* ============================================
HEADER
============================================ */
header {
padding: var(--space-xl) 0;
text-align: center;
border-bottom: 1px solid var(--paper-shadow);
background: linear-gradient(180deg, var(--paper) 0%, var(--teal-faint) 100%);
}
.logo {
display: inline-flex;
align-items: center;
gap: var(--space-sm);
margin-bottom: var(--space-sm);
}
.logo-icon {
width: 48px;
height: 48px;
background: linear-gradient(135deg, var(--teal) 0%, var(--teal-dark) 100%);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1.5rem;
box-shadow: var(--shadow-md);
}
header h1 {
margin-bottom: var(--space-xs);
}
header .subtitle {
color: var(--ink-light);
font-size: 1.1rem;
}
/* ============================================
PROGRESS INDICATOR
============================================ */
.progress-bar {
padding: var(--space-md) 0;
background: var(--paper);
border-bottom: 1px solid var(--paper-shadow);
position: sticky;
top: 0;
z-index: 100;
}
.progress-steps {
display: flex;
justify-content: center;
gap: var(--space-xs);
align-items: center;
}
.step-dot {
width: 12px;
height: 12px;
border-radius: var(--radius-full);
background: var(--paper-shadow);
transition: all 0.3s ease;
}
.step-dot.active {
background: var(--teal);
transform: scale(1.3);
}
.step-dot.completed {
background: var(--sage);
}
.step-connector {
width: 40px;
height: 2px;
background: var(--paper-shadow);
transition: background 0.3s;
}
.step-connector.completed {
background: var(--sage);
}
.progress-label {
text-align: center;
margin-top: var(--space-sm);
font-size: 0.85rem;
color: var(--ink-light);
}
/* Answer progress: count line + 3px fill, on questionnaire sections only.
Hidden on home/results via the `hidden` attribute (set in app.js). */
.answer-progress {
margin-top: var(--space-sm);
}
.answer-progress[hidden] {
display: none;
}
.answer-count {
text-align: center;
margin-bottom: var(--space-xs);
font-size: 0.85rem;
color: var(--ink-faint);
font-variant-numeric: tabular-nums;
}
.answer-fill-track {
height: 3px;
background: var(--paper-shadow);
border-radius: var(--radius-full);
overflow: hidden;
}
.answer-fill {
height: 100%;
width: 0;
background: var(--teal-dark);
transition: width 300ms ease-out;
}
@media (prefers-reduced-motion: reduce) {
.answer-fill {
transition: none;
}
}
/* ============================================
MAIN CONTENT
============================================ */
main {
padding: var(--space-2xl) 0;
min-height: 60vh;
}
.section {
display: none;
animation: fadeIn 0.4s ease;
}
.section.active {
display: block;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
/* ============================================
CARDS
============================================ */
.card {
background: white;
border-radius: var(--radius-lg);
padding: var(--space-xl);
margin-bottom: var(--space-lg);
box-shadow: var(--shadow-sm);
border: 1px solid var(--paper-shadow);
}
.card-header {
margin-bottom: var(--space-lg);
padding-bottom: var(--space-md);
border-bottom: 2px solid var(--teal-faint);
}
.card-header h2 {
margin-bottom: var(--space-xs);
}
.card-header .source {
font-size: 0.85rem;
color: var(--ink-faint);
font-style: italic;
}
/* ============================================
INTRO CARDS (Home)
============================================ */
.intro-grid {
display: grid;
gap: var(--space-md);
margin: var(--space-xl) 0;
}
.intro-card {
background: white;
border-radius: var(--radius-md);
padding: var(--space-lg);
border: 1px solid var(--paper-shadow);
display: flex;
gap: var(--space-md);
align-items: flex-start;
}
.intro-card-icon {
width: 44px;
height: 44px;
border-radius: var(--radius-sm);
display: flex;
align-items: center;
justify-content: center;
font-size: 1.3rem;
flex-shrink: 0;
}
.intro-card-icon.teal { background: var(--teal-faint); color: var(--teal); }
.intro-card-icon.coral { background: var(--coral-faint); color: var(--coral); }
.intro-card-icon.lavender { background: var(--lavender-faint); color: var(--lavender); }
.intro-card h4 {
margin-bottom: var(--space-xs);
}
.intro-card p {
font-size: 0.9rem;
color: var(--ink-light);
margin: 0;
}
/* ============================================
ALERTS / NOTICES
============================================ */
.notice {
padding: var(--space-lg);
border-radius: var(--radius-md);
margin-bottom: var(--space-lg);
}
.notice.warning {
background: var(--amber-faint);
border: 1px solid var(--amber);
}
.notice.info {
background: var(--teal-faint);
border: 1px solid var(--teal-light);
}
.notice h4 {
margin-bottom: var(--space-sm);
display: flex;
align-items: center;
gap: var(--space-sm);
}
.notice ul {
margin-left: var(--space-lg);
color: var(--ink-light);
}
.notice li {
margin-bottom: var(--space-xs);
}
/* ============================================
QUESTIONS
============================================ */
.question-group {
margin-bottom: var(--space-xl);
}
.question-group h3 {
color: var(--teal-dark);
margin-bottom: var(--space-md);
padding-bottom: var(--space-sm);
border-bottom: 1px solid var(--paper-shadow);
}
.question-group .hint {
font-size: 0.85rem;
color: var(--ink-faint);
font-style: italic;
margin-bottom: var(--space-md);
}
.question {
background: var(--paper);
border-radius: var(--radius-md);
padding: var(--space-lg);
margin-bottom: var(--space-md);
border: 1px solid transparent;
transition: border-color 0.2s, box-shadow 0.2s;
}
.question:hover {
border-color: var(--paper-shadow);
}
.question.answered {
border-color: var(--sage);
background: var(--sage-faint);
}
.question-text {
font-weight: 500;
margin-bottom: var(--space-md);
line-height: 1.5;
}
.question-number {
color: var(--teal);
font-weight: 700;
}
/* ============================================
RESPONSE OPTIONS
============================================ */
.options {
display: flex;
flex-wrap: wrap;
gap: var(--space-sm);
}
.option {
position: relative;
}
.option input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.option label {
display: inline-block;
padding: var(--space-sm) var(--space-md);
background: white;
border: 2px solid var(--paper-shadow);
border-radius: var(--radius-full);
cursor: pointer;
font-size: 0.9rem;
transition: all 0.2s;
}
.option label:hover {
border-color: var(--teal-light);
background: var(--teal-faint);
}
.option input:checked + label {
background: var(--teal);
border-color: var(--teal);
color: white;
}
/* Binary options (Yes/No) */
.options.binary {
gap: var(--space-md);
}
.options.binary .option label {
min-width: 80px;
text-align: center;
}
.options.binary .option.yes input:checked + label {
background: var(--coral);
border-color: var(--coral);
}
.options.binary .option.no input:checked + label {
background: var(--sage);
border-color: var(--sage);
}
/* ============================================
BUTTONS
============================================ */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-sm);
padding: var(--space-md) var(--space-xl);
border-radius: var(--radius-full);
font-family: inherit;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
border: 2px solid transparent;
transition: all 0.2s;
}
.btn-primary {
background: var(--teal);
color: white;
}
.btn-primary:hover {
background: var(--teal-dark);
transform: translateY(-1px);
box-shadow: var(--shadow-md);
}
.btn-secondary {
background: white;
color: var(--ink);
border-color: var(--paper-shadow);
}
.btn-secondary:hover {
border-color: var(--teal);
color: var(--teal);
}
.btn-large {
padding: var(--space-lg) var(--space-2xl);
font-size: 1.1rem;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: var(--space-xl);
padding-top: var(--space-lg);
border-top: 1px solid var(--paper-shadow);
}
/* ============================================
CHECKBOX (Consent)
============================================ */
.checkbox-wrapper {
display: flex;
align-items: flex-start;
gap: var(--space-md);
padding: var(--space-lg);
background: var(--paper);
border-radius: var(--radius-md);
margin-bottom: var(--space-lg);
cursor: pointer;
}
.checkbox-wrapper input[type="checkbox"] {
width: 22px;
height: 22px;
margin-top: 2px;
accent-color: var(--teal);
cursor: pointer;
}
.checkbox-wrapper span {
font-size: 0.95rem;
line-height: 1.5;
}
/* ============================================
RESULTS
============================================ */
.summary-card {
background: linear-gradient(135deg, var(--teal-faint) 0%, var(--lavender-faint) 100%);
border: 2px solid var(--teal);
border-radius: var(--radius-lg);
padding: var(--space-xl);
margin-bottom: var(--space-xl);
}
.summary-card h2 {
margin-bottom: var(--space-md);
}
.summary-text {
font-size: 1.05rem;
line-height: 1.7;
margin-bottom: var(--space-md);
}
.recommendation {
background: white;
padding: var(--space-md);
border-radius: var(--radius-md);
border-left: 4px solid var(--teal);
}
/* Result Cards */
.result-card {
background: white;
border-radius: var(--radius-lg);
padding: var(--space-xl);
margin-bottom: var(--space-lg);
border: 1px solid var(--paper-shadow);
border-left: 5px solid var(--paper-shadow);
}
.result-card.high {
border-left-color: var(--purple-deep);
}
.result-card.low {
border-left-color: var(--sage);
}
.result-card.moderate {
border-left-color: var(--purple-mid);
}
.result-card h3 {
margin-bottom: var(--space-md);
}
.result-status {
display: inline-flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-sm) var(--space-md);
border-radius: var(--radius-full);
font-weight: 600;
font-size: 0.9rem;
margin-bottom: var(--space-md);
}
.result-status.high {
background: var(--purple-deep-faint);
color: var(--purple-deep);
}
.result-status.low {
background: var(--sage-faint);
color: var(--sage);
}
.result-status.moderate {
background: var(--purple-mid-faint);
color: var(--purple-mid);
}
/* Score Grid */
.scores-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: var(--space-md);
margin: var(--space-md) 0;
}
.score-item {
background: var(--paper);
padding: var(--space-md);
border-radius: var(--radius-md);
text-align: center;
}
.score-label {
font-size: 0.8rem;
color: var(--ink-light);
display: block;
margin-bottom: var(--space-xs);
}
.score-value {
font-size: 1.4rem;
font-weight: 700;
color: var(--teal-dark);
}
/* Progress Bars */
.progress-item {
margin-bottom: var(--space-md);
}
.progress-label-row {
display: flex;
justify-content: space-between;
margin-bottom: var(--space-xs);
font-size: 0.9rem;
}
.progress-track {
height: 10px;
background: var(--paper-shadow);
border-radius: var(--radius-full);
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--teal);
border-radius: var(--radius-full);
transition: width 0.5s ease;
}
.progress-fill.high {
background: var(--purple-deep);
}
.progress-fill.moderate {
background: var(--purple-mid);
}
/* Interpretation */
.interpretation {
margin-top: var(--space-md);
padding-top: var(--space-md);
border-top: 1px dashed var(--paper-shadow);
font-style: italic;
color: var(--ink-light);
}
/* ============================================
FOOTER
============================================ */
footer {
padding: var(--space-xl) 0;
background: var(--paper-dark);
border-top: 1px solid var(--paper-shadow);
text-align: center;
}
footer p {
font-size: 0.85rem;
color: var(--ink-light);
margin-bottom: var(--space-sm);
}
footer .disclaimer {
max-width: 600px;
margin: 0 auto var(--space-md);
}
/* ============================================
RESPONSIVE
============================================ */
@media (max-width: 600px) {
html { font-size: 15px; }
.container { padding: 0 var(--space-md); }
.card { padding: var(--space-lg); }
.options { flex-direction: column; }
.option label { display: block; text-align: center; }
.actions { flex-direction: column; gap: var(--space-md); }
.btn { width: 100%; }
.scores-grid { grid-template-columns: repeat(2, 1fr); }
}
/* ============================================
PRINT STYLES
============================================ */
@media print {
body { background: white; }
.progress-bar, .actions, footer { display: none; }
.section { display: block !important; page-break-inside: avoid; }
}
/* Cache-first: la page et ses données chargent hors réseau; les réponses
vivent dans localStorage et ne passent jamais par le réseau. */
var CACHE = 'clarte-v2';
var ASSETS = ['./', './index.html', './style.css', './app.js', './jspdf.min.js',
'./manifest.webmanifest',
'./fonts/AtkinsonHyperlegible-Regular.woff2',
'./fonts/AtkinsonHyperlegible-Bold.woff2',
'./fonts/AtkinsonHyperlegible-Italic.woff2'];
self.addEventListener('install', function (e) {
e.waitUntil(caches.open(CACHE).then(function (c) { return c.addAll(ASSETS); }));
});
self.addEventListener('activate', function (e) {
e.waitUntil(caches.keys().then(function (keys) {
return Promise.all(keys.filter(function (k) { return k !== CACHE; })
.map(function (k) { return caches.delete(k); }));
}));
});
self.addEventListener('fetch', function (e) {
if (e.request.method !== 'GET') return;
e.respondWith(caches.match(e.request, { ignoreSearch: true })
.then(function (hit) { return hit || fetch(e.request); }));
});
...@@ -26,3 +26,9 @@ jest.mock('next/dynamic', () => { ...@@ -26,3 +26,9 @@ jest.mock('next/dynamic', () => {
return null; return null;
}; };
}); });
// jsdom (Node 20) ne fournit pas TextEncoder/TextDecoder, requis par
// answersToBase64url / decodeAnyPayload.
const { TextEncoder: TE, TextDecoder: TD } = require('util');
if (typeof global.TextEncoder === 'undefined') global.TextEncoder = TE;
if (typeof global.TextDecoder === 'undefined') global.TextDecoder = TD;
/* Clarté couple — partage global (#a=), comparaison à deux, conseil d'origine. */
import { COUPLE_DATA } from '@/components/couple/data';
import {
serializeAllAnswers, decodeAnyPayload, discussionAdvice, compareAnswers,
answersToBase64url,
} from '@/components/couple/engine';
const LS = 'couple:v1:q:';
function Q(id) { return COUPLE_DATA.questionnaires.find((q) => q.id === id); }
describe('serializeAllAnswers + decodeAnyPayload (#a=)', () => {
beforeEach(() => localStorage.clear());
it('serializes all 8 questionnaires, including the empty ones', () => {
const st = { answers: { 1: 3, 2: 4 }, pos: 2 };
localStorage.setItem(LS + 'bonheur_conjugal', JSON.stringify(st));
const all = serializeAllAnswers('moi');
expect(all.kind).toBe('all');
expect(all.questionnaires).toHaveLength(8);
const bonheur = all.questionnaires.find((b) => b.qid === 'bonheur_conjugal');
expect(bonheur.answered).toHaveLength(2);
expect(bonheur.pct).toBeGreaterThan(0);
const vide = all.questionnaires.find((b) => b.qid === 'folio_vie_en_couple');
expect(vide.answered).toHaveLength(0);
});
it('round-trips through base64url + decodeAnyPayload as kind=all', () => {
const all = serializeAllAnswers('moi');
const decoded = decodeAnyPayload(answersToBase64url(all));
expect(decoded.kind).toBe('all');
expect(decoded.payload.questionnaires).toHaveLength(8);
expect(decoded.payload.respondent).toBe('moi');
});
it('single-questionnaire payloads still decode as kind=one', () => {
const one = { qid: 'bonheur_conjugal', title: 'x', respondent: 'moi', answered: [{ i: '1', v: 3 }] };
const decoded = decodeAnyPayload(answersToBase64url(one));
expect(decoded.kind).toBe('one');
});
it('garbage returns null, never throws', () => {
expect(decodeAnyPayload('!!!not-base64!!!')).toBeNull();
expect(decodeAnyPayload(answersToBase64url({ random: true }))).toBeNull();
});
});
describe('discussionAdvice — la règle écrite du clinicien, porte telle quelle', () => {
it('bonheur_conjugal : discuter les 3-4, noter les 0-2', () => {
const a = discussionAdvice('bonheur_conjugal');
expect(a).toEqual({ discussFrom: 3, noteBelow: 3 });
});
it('les instruments sans règle écrite n’en inventent pas', () => {
expect(discussionAdvice('satisfaction_sexuelle')).toBeNull();
expect(discussionAdvice('communication')).toBeNull();
});
});
describe('compareAnswers — divergence par item, jamais un score de couple', () => {
it('labels both sides, flags divergence, treats missing as null', () => {
const q = Q('bonheur_conjugal');
const mine = { answers: { 1: 4, 2: 2 }, pos: 2 };
const theirBlock = { qid: 'bonheur_conjugal', answered: [{ i: '1', v: 2, l: 'Peu satisfait' }, { i: '3', v: 4, l: 'Satisfait' }] };
const rows = compareAnswers('bonheur_conjugal', mine, theirBlock, q);
expect(rows).toHaveLength(25);
expect(rows[0].mine).toBeTruthy();
expect(rows[0].theirs).toBeTruthy();
expect(rows[0].divergence).toBe(true);
expect(rows[1].theirs).toBeNull(); // l'autre n'a pas répondu à l'item 2
expect(rows[1].divergence).toBe(false);
});
});
/* Clarté TDAH — moteur : parité de scoring avec le POC vanilla (seuils,
présentation, bandes EF) et persistance sous contrat gelé. */
import {
scoreASRS, scoreDIVA, scoreExecutive, calculateResults,
readState, writeState, STORAGE_KEY, partAShadedCount, scope,
} from '@/components/tdah/engine';
import { TOTAL_QUESTIONS, PART_B_IDS, ALL_QUESTION_IDS } from '@/components/tdah/data';
describe('scoring parity with the vanilla POC', () => {
it('part A shades at thresholds 2/2/2/3/3/3 and screens positive at ≥4', () => {
// 4 réponses exactement au seuil : items 1-3 (2), item 4 (3), items 5-6 (0)
const r = { asrs_1: 2, asrs_2: 2, asrs_3: 2, asrs_4: 3, asrs_5: 0, asrs_6: 0 };
const s = scoreASRS(r);
expect(s.partAShaded).toBe(4);
expect(s.screeningPositive).toBe(true);
// cas limite : 3 cases grisées → dépistage non atteint
const r2 = { asrs_1: 2, asrs_2: 2, asrs_3: 2, asrs_4: 0, asrs_5: 0, asrs_6: 0 };
expect(scoreASRS(r2).screeningPositive).toBe(false);
});
it('edge: exactly 4 shaded with thresholds crossing (val = threshold - 1 does not shade)', () => {
const r = { asrs_1: 1, asrs_2: 2, asrs_3: 2, asrs_4: 2, asrs_5: 3, asrs_6: 0 };
// item 1 (seuil 2, val 1) ne grisera pas ; items 2,3 grisés (seuil 2), item 4 (seuil 3, val 2) non, item 5 (seuil 3, val 3) oui
expect(partAShadedCount(r)).toBe(3);
});
it('DSM: 5 of 9 in both → combined presentation (official keys, not legacy strings)', () => {
const r = {};
for (let i = 1; i <= 5; i++) r['diva_ia_' + i] = 1;
for (let i = 1; i <= 5; i++) r['diva_hi_' + i] = 1;
const d = scoreDIVA(r);
expect(d.meetsInattention).toBe(true);
expect(d.meetsHyperactivity).toBe(true);
expect(d.presentation).toBe('combined');
expect(['combined', 'inattentive', 'hyperactive', 'below']).toContain(d.presentation);
});
it('DSM: 4 of 9 stays below the clinical threshold', () => {
const r = {};
for (let i = 1; i <= 4; i++) r['diva_ia_' + i] = 1;
expect(scoreDIVA(r).meetsInattention).toBe(false);
});
it('EF: bands at 40 and 66 percent, severity keys are low/moderate/high', () => {
// 24 items 0..3 ; 40 % ⇒ total 28.8 → 29
const r29 = {}; let acc = 29;
ALL_QUESTION_IDS.filter((id) => id.startsWith('ef_')).forEach((id) => { const v = Math.min(3, acc); r29[id] = v; acc -= v; });
expect(scoreExecutive(r29).severity).toBe('moderate');
const rHigh = {}; ALL_QUESTION_IDS.filter((id) => id.startsWith('ef_')).forEach((id) => { rHigh[id] = 3; });
expect(scoreExecutive(rHigh).percentage).toBe(100);
expect(scoreExecutive(rHigh).severity).toBe('high');
const rLow = {}; ALL_QUESTION_IDS.filter((id) => id.startsWith('ef_')).forEach((id) => { rLow[id] = 0; });
expect(scoreExecutive(rLow).severity).toBe('low');
});
it('global positive indicators count the three tools', () => {
const r = {};
['asrs_1', 'asrs_2', 'asrs_3', 'asrs_4'].forEach((id) => { r[id] = 3; });
for (let i = 1; i <= 5; i++) r['diva_ia_' + i] = 1;
ALL_QUESTION_IDS.filter((id) => id.startsWith('ef_')).forEach((id) => { r[id] = 3; });
const g = calculateResults(r).global;
expect(g.positiveIndicators).toBe(3);
});
it('structure: 70 questions = 18 ASRS + 28 DSM-5 + 24 EF', () => {
expect(TOTAL_QUESTIONS).toBe(70);
expect(PART_B_IDS).toHaveLength(12);
});
it('scope: 70, or 58 with Part B skipped', () => {
expect(scope(false)).toBe(70);
expect(scope(true)).toBe(58);
});
});
describe('persistence contract (frozen: clarte:v1)', () => {
beforeEach(() => localStorage.clear());
it('round-trips {responses, currentSection, pos, partBSkipped, lastMilestone}', () => {
const st = { responses: { asrs_1: 2 }, currentSection: 'flow', pos: 3, partBSkipped: false, lastMilestone: null };
expect(writeState(st)).toBe(true);
expect(readState()).toEqual(st);
});
it('merges legacy vanilla records {responses, currentSection} without pos', () => {
localStorage.setItem(STORAGE_KEY, JSON.stringify({ responses: { asrs_1: 3 }, currentSection: 'asrs' }));
const s = readState();
expect(s.responses.asrs_1).toBe(3);
expect(s.currentSection).toBe('asrs');
expect(s.pos).toBeUndefined(); // the app layer defaults it
});
it('corrupt storage returns null, not a crash', () => {
localStorage.setItem(STORAGE_KEY, '{not json');
expect(readState()).toBeNull();
});
it('missing responses object returns null', () => {
localStorage.setItem(STORAGE_KEY, '{"currentSection":"home"}');
expect(readState()).toBeNull();
});
});
/* Clarté TDAH — flux : construction des nœuds (porte Partie B, ponts),
jalons de progression, completude du parcours 70 questions. */
import { questionnaireNodes } from '@/components/tdah/ClarteTdah';
describe('flow construction (spec §4, §7)', () => {
it('full flow: 2 intros ASRS-side + gate only under threshold + 70 questions + 2 bridges + recap', () => {
// seuil atteint : pas de porte
const r = { asrs_1: 3, asrs_2: 3, asrs_3: 3, asrs_4: 3, asrs_5: 0, asrs_6: 0 };
const nodes = questionnaireNodes(false, r);
const types = nodes.map((n) => n.type);
expect(types.filter((x) => x === 'question')).toHaveLength(70);
expect(types.filter((x) => x === 'gate')).toHaveLength(0);
expect(types[0]).toBe('intro');
expect(types[types.length - 1]).toBe('recap');
// ponts : A2 après ia_9, brown avant les EF
const ia9 = nodes.findIndex((n) => n.q && n.q.id === 'diva_ia_9');
expect(nodes[ia9 + 1].type).toBe('bridge');
expect(nodes[ia9 + 1].id).toBe('a2');
const imp5 = nodes.findIndex((n) => n.q && n.q.id === 'diva_imp_5');
expect(nodes[imp5 + 1].type).toBe('bridge');
expect(nodes[imp5 + 1].id).toBe('brown');
});
it('gate appears exactly once when Part A is under threshold and Part B untouched', () => {
const r = { asrs_1: 0, asrs_2: 0, asrs_3: 0, asrs_4: 0, asrs_5: 0, asrs_6: 0 };
const nodes = questionnaireNodes(false, r);
expect(nodes.filter((n) => n.type === 'gate')).toHaveLength(1);
const gateIdx = nodes.findIndex((n) => n.type === 'gate');
expect(nodes[gateIdx - 1].q.id).toBe('asrs_6');
expect(nodes[gateIdx + 1].q.id).toBe('asrs_7');
});
it('skipping Part B removes the gate and the 12 questions (scope 58)', () => {
const r = { asrs_1: 0, asrs_2: 0, asrs_3: 0, asrs_4: 0, asrs_5: 0, asrs_6: 0 };
const nodes = questionnaireNodes(true, r);
expect(nodes.filter((n) => n.type === 'gate')).toHaveLength(0);
expect(nodes.filter((n) => n.type === 'question')).toHaveLength(58);
expect(nodes.some((n) => n.q && n.q.id === 'asrs_7')).toBe(false);
});
it('Part B answered (any) removes the gate on re-flow', () => {
const r = { asrs_1: 0, asrs_2: 0, asrs_3: 0, asrs_4: 0, asrs_5: 0, asrs_6: 0, asrs_7: 2 };
const nodes = questionnaireNodes(false, r);
expect(nodes.filter((n) => n.type === 'gate')).toHaveLength(0);
});
});
/* Clarté TDAH — dictionnaires : complétude par rapport aux decks, zéro
tiret cadratin, typographie FR (espace insécable avant :), clés de questions
toutes résolues dans les deux langues. */
import { dict as fr } from '@/components/tdah/i18n/fr';
import { dict as en } from '@/components/tdah/i18n/en';
import { ALL_QUESTION_IDS, EXEC_CLUSTERS } from '@/components/tdah/data';
const REQUIRED = [
'meta.title', 'header.logo.h1', 'header.subtitle',
'home.h2', 'home.intro.p', 'home.card.asrs.p', 'home.card.dsm.p', 'home.card.ef.p',
'home.warning.h4', 'home.warning.li1', 'home.warning.li2', 'home.warning.li3',
'home.warning.li4', 'home.warning.li5', 'home.warning.li6',
'home.consent.label', 'home.start.btn', 'home.storage.line',
'progress.answered',
'intro.asrs.h', 'intro.asrs.p', 'intro.dsm.h', 'intro.dsm.p',
'intro.enfance.h', 'intro.enfance.p', 'intro.retentissement.h', 'intro.retentissement.p',
'intro.continue.btn', 'intro.skip.btn',
'bridge.a2.h', 'bridge.a2.p', 'bridge.brown.h', 'bridge.brown.p',
'note.label', 'note.asrs.adult.hyper', 'note.asrs.avoidance', 'note.ef.activation',
'note.ef.memory', 'note.ef.emotion', 'note.six.months', 'note.dsm.inattention', 'note.dsm.impulsivity',
'gate.title', 'gate.body', 'gate.answer.btn', 'gate.skip.btn',
'recap.title', 'recap.unanswered.chip', 'recap.complete.btn', 'recap.missing.count',
'recap.missing.go', 'recap.missing.skip', 'recap.review.fromresults',
'resume.count', 'resume.primary', 'resume.secondary',
'restart.confirm.body', 'restart.confirm.ok', 'restart.confirm.cancel',
'asrs.h2', 'asrs.source', 'asrs.info.p', 'asrs.partA.h3', 'asrs.partA.hint',
'asrs.partB.h3', 'asrs.partB.hint',
'results.warning.p', 'results.summary.h2', 'results.summary.text.high', 'results.summary.text.low',
'results.summary.text.empty', 'results.summary.bullet.asrs', 'results.summary.bullet.dsm', 'results.summary.bullet.ef',
'results.reco.high', 'results.reco.low',
'results.card1.h3', 'results.card1.status', 'results.card1.label.partA', 'results.card1.label.partB',
'results.card1.inattention', 'results.card1.hyperactivity',
'results.card2.h3', 'results.card3.h3', 'results.card3.status',
'results.presentation.below', 'results.presentation.combined', 'results.presentation.inattentive', 'results.presentation.hyperactive',
'results.download.btn', 'results.download.hint', 'results.restart.btn', 'alert.incomplete',
'asrs.interpretation.positive', 'asrs.interpretation.negative',
'understand.title', 'understand.asrs.p', 'understand.dsm.p', 'understand.ef.p',
'consult.title', 'consult.bring.p', 'consult.sentence.label', 'consult.sentence',
'consult.questions.h', 'consult.q1', 'consult.q2', 'consult.q3',
'glossary.link', 'glossary.title', 'glossary.tdah', 'glossary.asrs', 'glossary.dsm5',
'glossary.inattention', 'glossary.hyperactivite', 'glossary.impulsivite',
'glossary.fonctions.executives', 'glossary.memoire.travail', 'glossary.retentissement', 'glossary.depistage',
'milestone.asrs.done', 'milestone.half', 'milestone.dsm.done', 'milestone.all.done',
'pdf.title', 'pdf.warning.p', 'pdf.section1.h', 'pdf.section2.h', 'pdf.section3.h',
'pdf.annexe.h', 'pdf.unanswered', 'pdf.refs.h', 'pdf.pagefooter', 'pdf.passed.item',
'results.partB.skipped',
];
describe('dictionary completeness vs the decks', () => {
it('every required key exists in BOTH languages, non-empty', () => {
const missing = [];
REQUIRED.forEach((k) => {
if (!fr[k] && fr[k] !== '') missing.push('fr:' + k);
if (!en[k] && en[k] !== '') missing.push('en:' + k);
if (fr[k] === '' && k !== 'header.tagline') missing.push('fr-empty:' + k);
});
expect(missing).toEqual([]);
});
it('all 70 question keys resolve in both languages', () => {
const missing = [];
ALL_QUESTION_IDS.forEach((id) => {
const key = 'asrs.q' + id.split('_')[1] || null;
});
// questions ASRS
for (let i = 1; i <= 18; i++) {
if (!fr['asrs.q' + i]) missing.push('fr:asrs.q' + i);
if (!en['asrs.q' + i]) missing.push('en:asrs.q' + i);
}
// DSM
for (let i = 1; i <= 9; i++) {
if (!fr['diva.ia_' + i]) missing.push('fr:diva.ia_' + i);
if (!en['diva.ia_' + i]) missing.push('en:diva.ia_' + i);
if (!fr['diva.hi_' + i]) missing.push('fr:diva.hi_' + i);
if (!en['diva.hi_' + i]) missing.push('en:diva.hi_' + i);
}
for (let i = 1; i <= 5; i++) {
if (!fr['diva.child_' + i]) missing.push('fr:diva.child_' + i);
if (!fr['diva.imp_' + i]) missing.push('fr:diva.imp_' + i);
if (!en['diva.child_' + i]) missing.push('en:diva.child_' + i);
if (!en['diva.imp_' + i]) missing.push('en:diva.imp_' + i);
}
// EF
EXEC_CLUSTERS.forEach((c) => c.questions.forEach((q) => {
if (!fr[q.t]) missing.push('fr:' + q.t);
if (!en[q.t]) missing.push('en:' + q.t);
}));
expect(missing).toEqual([]);
});
it('no em dashes anywhere in shipped strings', () => {
const offenders = [];
Object.keys(fr).forEach((k) => { if (String(fr[k]).includes('—')) offenders.push('fr:' + k); });
Object.keys(en).forEach((k) => { if (String(en[k]).includes('—')) offenders.push('en:' + k); });
expect(offenders).toEqual([]);
});
it('French typography: NBSP before colons in sampled strings', () => {
const samples = ['recap.missing.count', 'results.summary.bullet.asrs', 'milestone.asrs.done'];
samples.forEach((k) => {
if (fr[k] && fr[k].includes(' :')) {
// si un deux-points existe, il doit être précédé d'une insécable
expect(fr[k]).not.toMatch(/[^ ] :/);
}
});
});
it('ASRS FR items are the official GAEA text (spot checks on the fixed items)', () => {
expect(fr['asrs.q1']).toContain('le gros du travail');
expect(fr['asrs.q1']).not.toContain('stimulantes');
expect(fr['asrs.q2']).toContain('établir les priorités');
expect(fr['asrs.q6']).toContain('comme une dynamo');
expect(fr['diva.ia_1']).toContain('fautes d’étourderie');
expect(en['asrs.q1']).toContain('final details');
expect(en['diva.ia_1']).toContain('careless mistakes');
});
});
...@@ -414,6 +414,13 @@ __metadata: ...@@ -414,6 +414,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@babel/runtime@npm:^7.28.6":
version: 7.29.7
resolution: "@babel/runtime@npm:7.29.7"
checksum: 10c0/ca11572f7146b21e0bde6a9ed4bb6a89eafbee5f0944c7eb54d0d8a2dac962c33638a1d611e14faa71dfbb92b4b5f9236232208568a6b7d5c6f3f39ddb91771e
languageName: node
linkType: hard
"@babel/template@npm:^7.27.2": "@babel/template@npm:^7.27.2":
version: 7.27.2 version: 7.27.2
resolution: "@babel/template@npm:7.27.2" resolution: "@babel/template@npm:7.27.2"
...@@ -2062,6 +2069,13 @@ __metadata: ...@@ -2062,6 +2069,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/pako@npm:^2.0.3":
version: 2.0.4
resolution: "@types/pako@npm:2.0.4"
checksum: 10c0/5765bf8bc7e77ee141c454118f03e544b8f6cb51eb257d82dc5830feeab8cd00818af3a1eabefdfbe8dd3ae9916ed5403937bf1031a0ee51deea27fdf4dccdfb
languageName: node
linkType: hard
"@types/prop-types@npm:*": "@types/prop-types@npm:*":
version: 15.7.15 version: 15.7.15
resolution: "@types/prop-types@npm:15.7.15" resolution: "@types/prop-types@npm:15.7.15"
...@@ -2076,6 +2090,13 @@ __metadata: ...@@ -2076,6 +2090,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/raf@npm:^3.4.0":
version: 3.4.3
resolution: "@types/raf@npm:3.4.3"
checksum: 10c0/dea835f0daa399c51db9137f5337dc08a2b4a5f61f645658966ecabaebbbd0fd59551f384a1141e14e22a1cc5a591da7d4d88c60a525ad1399108b6dd2641d75
languageName: node
linkType: hard
"@types/react@npm:^18.2.61": "@types/react@npm:^18.2.61":
version: 18.3.27 version: 18.3.27
resolution: "@types/react@npm:18.3.27" resolution: "@types/react@npm:18.3.27"
...@@ -2107,6 +2128,13 @@ __metadata: ...@@ -2107,6 +2128,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/trusted-types@npm:^2.0.7":
version: 2.0.7
resolution: "@types/trusted-types@npm:2.0.7"
checksum: 10c0/4c4855f10de7c6c135e0d32ce462419d8abbbc33713b31d294596c0cc34ae1fa6112a2f9da729c8f7a20707782b0d69da3b1f8df6645b0366d08825ca1522e0c
languageName: node
linkType: hard
"@types/unist@npm:*, @types/unist@npm:^3.0.0": "@types/unist@npm:*, @types/unist@npm:^3.0.0":
version: 3.0.3 version: 3.0.3
resolution: "@types/unist@npm:3.0.3" resolution: "@types/unist@npm:3.0.3"
...@@ -2839,6 +2867,13 @@ __metadata: ...@@ -2839,6 +2867,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"base64-arraybuffer@npm:^1.0.2":
version: 1.0.2
resolution: "base64-arraybuffer@npm:1.0.2"
checksum: 10c0/3acac95c70f9406e87a41073558ba85b6be9dbffb013a3d2a710e3f2d534d506c911847d5d9be4de458af6362c676de0a5c4c2d7bdf4def502d00b313368e72f
languageName: node
linkType: hard
"baseline-browser-mapping@npm:^2.8.25": "baseline-browser-mapping@npm:^2.8.25":
version: 2.8.32 version: 2.8.32
resolution: "baseline-browser-mapping@npm:2.8.32" resolution: "baseline-browser-mapping@npm:2.8.32"
...@@ -3010,6 +3045,22 @@ __metadata: ...@@ -3010,6 +3045,22 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"canvg@npm:^3.0.11":
version: 3.0.11
resolution: "canvg@npm:3.0.11"
dependencies:
"@babel/runtime": "npm:^7.12.5"
"@types/raf": "npm:^3.4.0"
core-js: "npm:^3.8.3"
raf: "npm:^3.4.1"
regenerator-runtime: "npm:^0.13.7"
rgbcolor: "npm:^1.0.1"
stackblur-canvas: "npm:^2.0.0"
svg-pathdata: "npm:^6.0.3"
checksum: 10c0/03ea5b69e97a4a476ef5fc8455d0da2718bd68cd8ace3e42718176bf4b7dfd80eb1d0eba47dcd04b34ed9a0c5c8ed849ead0f364dad0e35369254a45d5e098c4
languageName: node
linkType: hard
"ccount@npm:^2.0.0": "ccount@npm:^2.0.0":
version: 2.0.1 version: 2.0.1
resolution: "ccount@npm:2.0.1" resolution: "ccount@npm:2.0.1"
...@@ -3238,6 +3289,13 @@ __metadata: ...@@ -3238,6 +3289,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"core-js@npm:^3.6.0, core-js@npm:^3.8.3":
version: 3.50.0
resolution: "core-js@npm:3.50.0"
checksum: 10c0/1bfcfb1dbdf45b0b1428a9fdcb03697d1fc52c7e9bc336f804a86ff2c2705a301dca7d9d6636d09d49d60298183273b9321681baeaf330c78db5393bc73f3f6a
languageName: node
linkType: hard
"core-util-is@npm:~1.0.0": "core-util-is@npm:~1.0.0":
version: 1.0.3 version: 1.0.3
resolution: "core-util-is@npm:1.0.3" resolution: "core-util-is@npm:1.0.3"
...@@ -3263,6 +3321,15 @@ __metadata: ...@@ -3263,6 +3321,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"css-line-break@npm:^2.1.0":
version: 2.1.0
resolution: "css-line-break@npm:2.1.0"
dependencies:
utrie: "npm:^1.0.2"
checksum: 10c0/b2222d99d5daf7861ecddc050244fdce296fad74b000dcff6bdfb1eb16dc2ef0b9ffe2c1c965e3239bd05ebe9eadb6d5438a91592fa8648d27a338e827cf9048
languageName: node
linkType: hard
"css.escape@npm:^1.5.1": "css.escape@npm:^1.5.1":
version: 1.5.1 version: 1.5.1
resolution: "css.escape@npm:1.5.1" resolution: "css.escape@npm:1.5.1"
...@@ -3437,6 +3504,18 @@ __metadata: ...@@ -3437,6 +3504,18 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"dompurify@npm:^3.3.1":
version: 3.4.15
resolution: "dompurify@npm:3.4.15"
dependencies:
"@types/trusted-types": "npm:^2.0.7"
dependenciesMeta:
"@types/trusted-types":
optional: true
checksum: 10c0/406b5f0f9c9dcbc80016367d33e73585b6a6f440815228d7e9cbe8abe71058183b1f4cb4207b79b8473637de4a106fc055eb6da271ef9db353d29679b891ecbc
languageName: node
linkType: hard
"eastasianwidth@npm:^0.2.0": "eastasianwidth@npm:^0.2.0":
version: 0.2.0 version: 0.2.0
resolution: "eastasianwidth@npm:0.2.0" resolution: "eastasianwidth@npm:0.2.0"
...@@ -3930,6 +4009,17 @@ __metadata: ...@@ -3930,6 +4009,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"fast-png@npm:^6.2.0":
version: 6.4.0
resolution: "fast-png@npm:6.4.0"
dependencies:
"@types/pako": "npm:^2.0.3"
iobuffer: "npm:^5.3.2"
pako: "npm:^2.1.0"
checksum: 10c0/bca27a09d56a5ead536b11c1ddccf4fe44c7c0af88a8faefab6a397527d9c71b00c09b2055d0108807c438c50719dd2acbd7217ef88785b500888db26fa0358a
languageName: node
linkType: hard
"fastq@npm:^1.6.0": "fastq@npm:^1.6.0":
version: 1.19.1 version: 1.19.1
resolution: "fastq@npm:1.19.1" resolution: "fastq@npm:1.19.1"
...@@ -3978,6 +4068,13 @@ __metadata: ...@@ -3978,6 +4068,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"fflate@npm:^0.8.1":
version: 0.8.3
resolution: "fflate@npm:0.8.3"
checksum: 10c0/eab181ca37f5348ae76d4b6f840e0026e30220e33153289ac942222d8b9638237d486507dbcc09878d724095bd354993a2ee48bbee99c8f2c6440d4448719aa7
languageName: node
linkType: hard
"fftjs@npm:0.0.4": "fftjs@npm:0.0.4":
version: 0.0.4 version: 0.0.4
resolution: "fftjs@npm:0.0.4" resolution: "fftjs@npm:0.0.4"
...@@ -4465,6 +4562,16 @@ __metadata: ...@@ -4465,6 +4562,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"html2canvas@npm:^1.0.0-rc.5":
version: 1.4.1
resolution: "html2canvas@npm:1.4.1"
dependencies:
css-line-break: "npm:^2.1.0"
text-segmentation: "npm:^1.0.3"
checksum: 10c0/6de86f75762b00948edf2ea559f16da0a1ec3facc4a8a7d3f35fcec59bb0c5970463478988ae3d9082152e0173690d46ebf4082e7ac803dd4817bae1d355c0db
languageName: node
linkType: hard
"http-cache-semantics@npm:^4.1.1": "http-cache-semantics@npm:^4.1.1":
version: 4.2.0 version: 4.2.0
resolution: "http-cache-semantics@npm:4.2.0" resolution: "http-cache-semantics@npm:4.2.0"
...@@ -4663,6 +4770,13 @@ __metadata: ...@@ -4663,6 +4770,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"iobuffer@npm:^5.3.2":
version: 5.4.0
resolution: "iobuffer@npm:5.4.0"
checksum: 10c0/1b3f9a5ea158bc63f038b374b42832acdb9db13ea9cfa4ed78723f30894986442f6c033dff4ae2fdf34724bf0fe432e3b86c11ca82049568c58b8d7fb47a6ac2
languageName: node
linkType: hard
"ip-address@npm:^10.0.1": "ip-address@npm:^10.0.1":
version: 10.1.0 version: 10.1.0
resolution: "ip-address@npm:10.1.0" resolution: "ip-address@npm:10.1.0"
...@@ -5504,6 +5618,30 @@ __metadata: ...@@ -5504,6 +5618,30 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"jspdf@npm:^4.2.1":
version: 4.2.1
resolution: "jspdf@npm:4.2.1"
dependencies:
"@babel/runtime": "npm:^7.28.6"
canvg: "npm:^3.0.11"
core-js: "npm:^3.6.0"
dompurify: "npm:^3.3.1"
fast-png: "npm:^6.2.0"
fflate: "npm:^0.8.1"
html2canvas: "npm:^1.0.0-rc.5"
dependenciesMeta:
canvg:
optional: true
core-js:
optional: true
dompurify:
optional: true
html2canvas:
optional: true
checksum: 10c0/d905809b374de1060ab39ba4b1603935fcced314bef8f63f7fd272d3f96eff0b50a96e48d3c2063ac8e51b84a1442464ff2b037af13f54b082e456416da470b7
languageName: node
linkType: hard
"kind-of@npm:^6.0.0, kind-of@npm:^6.0.2": "kind-of@npm:^6.0.0, kind-of@npm:^6.0.2":
version: 6.0.3 version: 6.0.3
resolution: "kind-of@npm:6.0.3" resolution: "kind-of@npm:6.0.3"
...@@ -7018,6 +7156,13 @@ __metadata: ...@@ -7018,6 +7156,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"pako@npm:^2.1.0":
version: 2.2.0
resolution: "pako@npm:2.2.0"
checksum: 10c0/e5d31de1940fdd13cc4014296b0026112483d0834bce4856ca29c8af8438d030c6416986a0f6df7d5372cae18d6dd6c57f48b24a49d6c207c0a4cc66af15c07d
languageName: node
linkType: hard
"parse-entities@npm:^2.0.0": "parse-entities@npm:^2.0.0":
version: 2.0.0 version: 2.0.0
resolution: "parse-entities@npm:2.0.0" resolution: "parse-entities@npm:2.0.0"
...@@ -7280,6 +7425,7 @@ __metadata: ...@@ -7280,6 +7425,7 @@ __metadata:
jest: "npm:^30.1.3" jest: "npm:^30.1.3"
jest-environment-jsdom: "npm:^30.1.2" jest-environment-jsdom: "npm:^30.1.2"
jest-pnp-resolver: "npm:^1.2.3" jest-pnp-resolver: "npm:^1.2.3"
jspdf: "npm:^4.2.1"
marked: "npm:^15.0.12" marked: "npm:^15.0.12"
next: "npm:^15.3.0" next: "npm:^15.3.0"
next-router-mock: "npm:^1.0.2" next-router-mock: "npm:^1.0.2"
...@@ -7477,7 +7623,7 @@ __metadata: ...@@ -7477,7 +7623,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"raf@npm:^3.0.0": "raf@npm:^3.0.0, raf@npm:^3.4.1":
version: 3.4.1 version: 3.4.1
resolution: "raf@npm:3.4.1" resolution: "raf@npm:3.4.1"
dependencies: dependencies:
...@@ -7725,6 +7871,13 @@ __metadata: ...@@ -7725,6 +7871,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"regenerator-runtime@npm:^0.13.7":
version: 0.13.11
resolution: "regenerator-runtime@npm:0.13.11"
checksum: 10c0/12b069dc774001fbb0014f6a28f11c09ebfe3c0d984d88c9bced77fdb6fedbacbca434d24da9ae9371bfbf23f754869307fb51a4c98a8b8b18e5ef748677ca24
languageName: node
linkType: hard
"regex-recursion@npm:^6.0.2": "regex-recursion@npm:^6.0.2":
version: 6.0.2 version: 6.0.2
resolution: "regex-recursion@npm:6.0.2" resolution: "regex-recursion@npm:6.0.2"
...@@ -7927,6 +8080,13 @@ __metadata: ...@@ -7927,6 +8080,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"rgbcolor@npm:^1.0.1":
version: 1.0.1
resolution: "rgbcolor@npm:1.0.1"
checksum: 10c0/13af06c523351bac2854b85a22d1dfafd9310efd898e9bd96c8706f9aa09a3ddc8392ab00ae03d12950782164a97677f21834ffd84ffebf76ae106add319f956
languageName: node
linkType: hard
"right-now@npm:^1.0.0": "right-now@npm:^1.0.0":
version: 1.0.0 version: 1.0.0
resolution: "right-now@npm:1.0.0" resolution: "right-now@npm:1.0.0"
...@@ -8279,6 +8439,13 @@ __metadata: ...@@ -8279,6 +8439,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"stackblur-canvas@npm:^2.0.0":
version: 2.7.0
resolution: "stackblur-canvas@npm:2.7.0"
checksum: 10c0/df290d0629056d5bb43d37548d0b24cb8593c79d742650e68489abf61013db578c9980724c2508bb738d107204f2e2494ab94c3cf69d6b725caa9c63b8c7e272
languageName: node
linkType: hard
"stat-mode@npm:0.3.0": "stat-mode@npm:0.3.0":
version: 0.3.0 version: 0.3.0
resolution: "stat-mode@npm:0.3.0" resolution: "stat-mode@npm:0.3.0"
...@@ -8485,6 +8652,13 @@ __metadata: ...@@ -8485,6 +8652,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"svg-pathdata@npm:^6.0.3":
version: 6.0.3
resolution: "svg-pathdata@npm:6.0.3"
checksum: 10c0/1ba4ad2fa81e86df37d6e78d3be9e664bbedf97773b725a863a85db384285be32dc37d9c0d61e477d89594ee95b967d2c53d6bee2d76420aab670ab4124a38b9
languageName: node
linkType: hard
"swiper@npm:^11.2.6": "swiper@npm:^11.2.6":
version: 11.2.10 version: 11.2.10
resolution: "swiper@npm:11.2.10" resolution: "swiper@npm:11.2.10"
...@@ -8574,6 +8748,15 @@ __metadata: ...@@ -8574,6 +8748,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"text-segmentation@npm:^1.0.3":
version: 1.0.3
resolution: "text-segmentation@npm:1.0.3"
dependencies:
utrie: "npm:^1.0.2"
checksum: 10c0/8b9ae8524e3a332371060d0ca62f10ad49a13e954719ea689a6c3a8b8c15c8a56365ede2bb91c322fb0d44b6533785f0da603e066b7554d052999967fb72d600
languageName: node
linkType: hard
"time-span@npm:4.0.0": "time-span@npm:4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "time-span@npm:4.0.0" resolution: "time-span@npm:4.0.0"
...@@ -9033,6 +9216,15 @@ __metadata: ...@@ -9033,6 +9216,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"utrie@npm:^1.0.2":
version: 1.0.2
resolution: "utrie@npm:1.0.2"
dependencies:
base64-arraybuffer: "npm:^1.0.2"
checksum: 10c0/eaffe645bd81a39e4bc3abb23df5895e9961dbdd49748ef3b173529e8b06ce9dd1163e9705d5309a1c61ee41ffcb825e2043bc0fd1659845ffbdf4b1515dfdb4
languageName: node
linkType: hard
"uuid@npm:3.3.2": "uuid@npm:3.3.2":
version: 3.3.2 version: 3.3.2
resolution: "uuid@npm:3.3.2" resolution: "uuid@npm:3.3.2"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment