{"_id":"@dahhouilyas/survey","name":"@dahhouilyas/survey","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@dahhouilyas/survey","version":"0.1.0","description":"Module de sondage : modèle JSON, moteur de rendu React et builder drag & drop — sans dépendance réseau.","license":"MIT","author":{"name":"Ilyas Dahhou"},"homepage":"https://github.com/dahhou-ilyas/dyanmique_survy#readme","repository":{"type":"git","url":"git+https://github.com/dahhou-ilyas/dyanmique_survy.git","directory":"packages/survey"},"bugs":{"url":"https://github.com/dahhou-ilyas/dyanmique_survy/issues"},"keywords":["survey","form","form-builder","survey-builder","questionnaire","react","json-schema","drag-and-drop","conditional-logic","accessibility"],"publishConfig":{"access":"public"},"type":"module","sideEffects":["*.css"],"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./core":{"import":{"types":"./dist/core.d.ts","default":"./dist/core.js"},"require":{"types":"./dist/core.d.cts","default":"./dist/core.cjs"}},"./runtime":{"import":{"types":"./dist/runtime.d.ts","default":"./dist/runtime.js"},"require":{"types":"./dist/runtime.d.cts","default":"./dist/runtime.cjs"}},"./builder":{"import":{"types":"./dist/builder.d.ts","default":"./dist/builder.js"},"require":{"types":"./dist/builder.d.cts","default":"./dist/builder.cjs"}},"./themes/*.css":"./dist/themes/*.css","./package.json":"./package.json"},"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","typesVersions":{"*":{"core":["./dist/core.d.ts"],"runtime":["./dist/runtime.d.ts"],"builder":["./dist/builder.d.ts"]}},"engines":{"node":">=18"},"scripts":{"build":"tsup && node scripts/copy-styles.mjs","dev":"tsup --watch","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","typecheck":"tsc --noEmit","verify:pkg":"publint && attw --pack ."},"peerDependencies":{"react":"^18.2.0","react-dom":"^18.2.0"},"dependencies":{"@dnd-kit/core":"^6.3.1","@dnd-kit/sortable":"^10.0.0","@dnd-kit/utilities":"^3.2.2","dompurify":"^3.4.13","immer":"^11.1.16","nanoid":"^6.0.1","zod":"^4.4.3","zustand":"^5.0.15"},"devDependencies":{"@arethetypeswrong/cli":"^0.18.5","@testing-library/dom":"^10.4.1","@testing-library/jest-dom":"^7.0.1","@testing-library/react":"^16.3.2","@testing-library/user-event":"^14.6.4","@types/node":"^26.2.0","@types/react":"^18.3.31","@types/react-dom":"^18.3.7","@vitest/coverage-v8":"^4.1.10","axe-core":"^4.13.0","jsdom":"^29.1.1","publint":"^0.3.23","react":"^18.3.1","react-dom":"^18.3.1","tsup":"^8.5.1","typescript":"~6.0.3","vitest":"^4.1.10"},"gitHead":"811e233e0ddf3d6896531c2e3b0fe67579bae1da","_id":"@dahhouilyas/survey@0.1.0","_nodeVersion":"22.23.2","_npmVersion":"12.0.2","dist":{"integrity":"sha512-xeRhq+r+P91+5nz1uNMdQdssw8NNaCHjT3tJ3ldAyjPoNk1cje41Vvi2Vo1qhRAJCFWWdXZ7FMXaP5AC+tTCgw==","shasum":"e2333d66dda0d68f1cf7bfb2a733b7f2e5f4af3c","tarball":"https://registry.npmjs.org/@dahhouilyas/survey/-/survey-0.1.0.tgz","fileCount":61,"unpackedSize":2488466,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQDQMRdBZ4iFjhya+rDb+FOQgZrcIUkoyyIh/hR4drtVfgIhAKjrm2bkead3da5X+IaQm3op3gBx2EqTiw3hXHLEsV9o"}]},"_npmUser":{"name":"dahhouilyas","email":"dahhou.ilyass@gmail.com"},"directories":{},"maintainers":[{"name":"dahhouilyas","email":"dahhou.ilyass@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/survey_0.1.0_1786816724837_0.8797509745272272"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-15T17:58:44.676Z","0.1.0":"2026-08-15T17:58:45.022Z","modified":"2026-08-15T17:58:45.206Z"},"maintainers":[{"name":"dahhouilyas","email":"dahhou.ilyass@gmail.com"}],"description":"Module de sondage : modèle JSON, moteur de rendu React et builder drag & drop — sans dépendance réseau.","homepage":"https://github.com/dahhou-ilyas/dyanmique_survy#readme","keywords":["survey","form","form-builder","survey-builder","questionnaire","react","json-schema","drag-and-drop","conditional-logic","accessibility"],"repository":{"type":"git","url":"git+https://github.com/dahhou-ilyas/dyanmique_survy.git","directory":"packages/survey"},"author":{"name":"Ilyas Dahhou"},"bugs":{"url":"https://github.com/dahhou-ilyas/dyanmique_survy/issues"},"license":"MIT","readme":"# @dahhouilyas/survey\n\nModule de sondage pour applications React : **modèle JSON**, **moteur de rendu** et\n**builder en glisser-déposer**, dans un seul package à points d'entrée séparés.\n\nConçu pour le module de sondage décrit dans « Expression de Besoins — Module de sondage » :\nquestionnaires créés sans développeur depuis le back-office, stockés en JSON, interprétés\ndynamiquement à l'affichage.\n\n## Principe\n\n> Le package ne réalise **aucun appel réseau** et ne connaît ni l'authentification, ni les\n> campagnes, ni le ciblage, ni la règle du « one-shot ». Il expose un modèle, un moteur de\n> rendu et un builder ; l'application hôte branche ses API sur des callbacks.\n\nC'est la séparation « moteur de rendu (front) / logique métier (backend) » exigée par le\ncahier des charges. Concrètement : campagnes, priorités, ciblage par numéro client, compteur\n« plus tard » et piste d'audit restent côté application et backend.\n\n## Installation\n\n```bash\nnpm install @dahhouilyas/survey\n```\n\n`react` et `react-dom` sont des **peer dependencies** (`^18.2.0`) : le package n'embarque\njamais sa propre copie de React.\n\n## Points d'entrée\n\n| Import                             | Pour qui                                   | Contenu                                             |\n| ---------------------------------- | ------------------------------------------ | --------------------------------------------------- |\n| `@dahhouilyas/survey/runtime`      | l'application qui **affiche** les sondages | `SurveyRenderer`, hooks, composants de champ        |\n| `@dahhouilyas/survey/builder`      | le **back-office**                         | `SurveyBuilder`, store, éditeurs                    |\n| `@dahhouilyas/survey/core`         | les deux, et le serveur                    | modèle, expressions, validation, `SurveyEngine`     |\n| `@dahhouilyas/survey`              | commodité                                  | `core` + `runtime` (jamais le builder)              |\n| `@dahhouilyas/survey/themes/*.css` | —                                          | `default`, `builder`, `compact`, `dark-auto`, `edb` |\n\nLe builder n'est pas ré-exporté depuis la racine : une application qui n'importe que\n`/runtime` ne télécharge ni `@dnd-kit`, ni les éditeurs de propriétés.\n\n**Poids mesuré** : `/runtime` ajoute ≈ **29 Ko gzip** au bundle, hors React\n(74,3 Ko gzip pour l'application complète, dont 45,1 Ko pour React seul).\n\n## Afficher un sondage\n\n```tsx\nimport { SurveyRenderer } from '@dahhouilyas/survey/runtime';\nimport '@dahhouilyas/survey/themes/default.css';\n\n<SurveyRenderer\n  definition={campagne.definition} // le JSON stocké en base\n  data={reponsesPartielles} // reprise d'un sondage entamé\n  locale=\"fr\"\n  onValueChanged={({ name, value }) => autosave(name, value)}\n  onComplete={(result) => api.post('/sondages/reponses', result)}\n/>;\n```\n\n`onComplete` reçoit un `SurveyResult` prêt pour le reporting :\n\n```jsonc\n{\n  \"surveyId\": \"srv_satisfaction\",\n  \"schemaVersion\": 1,\n  \"locale\": \"fr\",\n  \"startedAt\": \"2026-08-14T09:12:03.100Z\",\n  \"completedAt\": \"2026-08-14T09:14:38.900Z\",\n  \"durationMs\": 155800,\n  \"data\": { \"satisfaction\": 2, \"canaux\": [\"app\", \"web\"] },\n  \"answers\": [\n    {\n      \"questionId\": \"q1\",\n      \"questionName\": \"satisfaction\",\n      \"type\": \"rating\",\n      \"title\": \"Votre satisfaction globale\",\n      \"value\": 2,\n      \"displayValue\": \"2/5\",\n    },\n  ],\n}\n```\n\n`answers` est la forme « clé / valeur » attendue par le cahier des charges : à plat, avec le\nlibellé résolu au moment de la passation — un rapport reste lisible même après refonte du\nquestionnaire. Les questions **masquées** par une condition en sont exclues ; leur réponse\nreste néanmoins en mémoire, pour être restaurée si l'utilisateur revient en arrière.\n\n### Options utiles\n\n| Prop                                                              | Effet                                                             |\n| ----------------------------------------------------------------- | ----------------------------------------------------------------- |\n| `mode=\"display\"`                                                  | tout en lecture seule — relecture d'une réponse en back-office    |\n| `theme=\"dark\"`                                                    | force le thème sombre sur ce sondage                              |\n| `messages={{ next: 'Continuer' }}`                                | surcharge partielle des libellés d'interface                      |\n| `components={{ ...defaultFieldComponents, signature: MonChamp }}` | ajoute ou remplace un type                                        |\n| `functions={{ soldeClient: () => solde }}`                        | fonctions d'expression propres à l'hôte                           |\n| `onEngineReady={(engine) => …}`                                   | accès au moteur (navigation programmatique, sauvegarde partielle) |\n\n## Construire un sondage\n\n```tsx\nimport { SurveyBuilder, createSurvey } from '@dahhouilyas/survey/builder';\nimport '@dahhouilyas/survey/themes/builder.css';\n\nconst [survey, setSurvey] = useState(createSurvey());\n\n<SurveyBuilder\n  value={survey}\n  onChange={setSurvey}\n  onSave={(definition) => api.put(`/campagnes/${id}`, definition)}\n/>;\n```\n\nCinq vues : **Concepteur** (palette, canevas, propriétés), **Aperçu**, **Logique**,\n**Traductions**, **JSON**.\n\n- Glisser-déposer à la souris **et au clavier** (`@dnd-kit`), annulation par `Ctrl+Z`.\n- Le panneau de propriétés est **généré** à partir des descripteurs de types.\n- L'aperçu utilise le même `SurveyRenderer` que la production : aucune divergence possible.\n- Les contrôles de conception (noms dupliqués, références cassées, options vides) sont\n  affichés en continu sous le canevas.\n\n## Le modèle JSON\n\nLa forme suit celle de SurveyJS (`pages[].elements[]`, `type`, `name`, `title`, `isRequired`,\n`visibleIf`, `choices`), ce qui aligne le vocabulaire sur le document remis au client et\nlaisse une porte de sortie dans les deux sens.\n\n```jsonc\n{\n  \"schemaVersion\": 1,\n  \"id\": \"srv_satisfaction\",\n  \"title\": { \"fr\": \"Satisfaction client\", \"en\": \"Customer satisfaction\" },\n  \"settings\": {\n    \"locale\": \"fr\",\n    \"showProgressBar\": \"top\",\n    \"showQuestionNumbers\": \"off\",\n    \"showPageTitles\": true,\n    \"checkErrorsMode\": \"onNextPage\",\n  },\n  \"calculatedValues\": [\n    { \"name\": \"estDetracteur\", \"expression\": \"{recommandation} < 7\", \"includeIntoResult\": true },\n  ],\n  \"pages\": [\n    {\n      \"id\": \"p1\",\n      \"name\": \"page1\",\n      \"elements\": [\n        {\n          \"id\": \"q1\",\n          \"name\": \"satisfaction\",\n          \"type\": \"rating\",\n          \"title\": \"Votre satisfaction\",\n          \"rateMax\": 5,\n          \"isRequired\": true,\n        },\n        {\n          \"id\": \"q2\",\n          \"name\": \"commentaire\",\n          \"type\": \"comment\",\n          \"title\": \"Que dire de plus ?\",\n          \"visibleIf\": \"{satisfaction} <= 3\",\n          \"isRequired\": true,\n        },\n      ],\n    },\n  ],\n}\n```\n\n### Types de questions\n\n`text` (avec `inputType`: text/email/tel/url/password) · `comment` · `radiogroup` ·\n`checkbox` · `dropdown` · `date` · `number` · `rating` (étoiles / nombres / émoticônes) ·\n`nps` · `boolean` (radio ou interrupteur) · `html` (bloc de contenu, sans réponse) ·\n`panel` (groupe, imbriquable).\n\nLe `name` est l'identifiant unique de la question : c'est la clé de la réponse et le jeton\nutilisé dans les expressions. Le builder en garantit l'unicité.\n\n### Logique conditionnelle\n\n`visibleIf`, `enableIf`, `requiredIf`, `choicesVisibleIf`, plus les validateurs de type\n`expression`. Syntaxe compatible SurveyJS :\n\n```\n{satisfaction} <= 3\n{canaux} anyof ['app', 'web'] and {segment} = 'pro'\nage({date_naissance}) >= 18\n{email} notempty\niif({montant} > 10000, 'eleve', 'standard')\n```\n\nOpérateurs : `= == != <> < > <= >= && and || or ! not contains notcontains anyof allof\nnoneof empty notempty + - * / % ^`.\nFonctions : `iif today now age year month day weekday dateAdd dateDiff sum min max avg count\nabs ceil floor round trunc len upper lower trim concat isEmpty isNotEmpty`.\n\nLes expressions sont analysées par un parseur dédié (lexer → parseur à montée en précédence\n→ AST → interpréteur). **Ni `eval`, ni `new Function`** : les expressions viennent\nd'utilisateurs back-office et ne doivent jamais atteindre un interpréteur JavaScript.\n\n### Déclencheurs\n\n`setvalue`, `copyvalue`, `skip`, `complete`. Ils se déclenchent au **passage** de la\ncondition à vrai, jamais tant qu'elle le reste : une valeur posée par une règle puis\ncorrigée par l'utilisateur n'est pas réécrasée.\n\n## Thèmes\n\nLes jetons sont posés sur `.edbs-root`, jamais sur `:root` : le package est une dépendance,\nil ne teinte pas les variables globales de l'application. Personnaliser une charte revient à\nredéfinir des variables, jamais à surcharger un sélecteur.\n\n```css\n.edbs-root {\n  --edbs-color-primary: #00539b;\n  --edbs-color-primary-contrast: #ffffff;\n  --edbs-radius-md: 4px;\n  --edbs-font-family: 'Inter', sans-serif;\n}\n```\n\n`themes/edb.css` sert de gabarit. Tous les sélecteurs du package sont mono-classe et\npréfixés `edbs-` : une surcharge côté hôte n'a jamais besoin de `!important`.\n\nLe thème sombre est **explicite** (`theme=\"dark\"` ou `data-edbs-theme=\"dark\"`). Pour suivre\nla préférence système, importer `themes/dark-auto.css` — à réserver aux applications qui\ngèrent elles aussi le mode sombre.\n\n## Accessibilité\n\nContrôlée automatiquement par `axe-core` (WCAG 2.1 A et AA) sur le renderer et le builder,\nà chaque exécution des tests.\n\n- `<fieldset>` / `<legend>` pour les groupes de choix et les échelles ; le `<legend>` est le\n  premier enfant du `<fieldset>`, seule position où il devient le nom accessible du groupe.\n- Erreurs reliées au champ par `aria-describedby`, annoncées via `role=\"alert\"`.\n- Le focus va sur la première question en erreur quand la navigation échoue, et sur le titre\n  de la page à chaque changement d'étape.\n- Les échelles de notation reposent sur de vrais `input[type=radio]` masqués visuellement :\n  navigation par flèches et annonce de position natives.\n- Glisser-déposer utilisable au clavier, avec annonces vocales en français.\n- `prefers-reduced-motion` et `forced-colors` pris en compte.\n\n## Sécurité\n\n- Aucune évaluation dynamique de code, nulle part (`no-eval` appliqué par ESLint).\n- Le HTML des blocs de contenu est assaini par DOMPurify avec une liste blanche stricte :\n  ni script, ni iframe, ni attribut événementiel, ni `style`. En rendu serveur, où DOMPurify\n  n'a pas de DOM, le contenu est rendu en texte brut plutôt qu'injecté.\n- Aucun appel réseau : le package ne peut pas exfiltrer de données.\n- Aucune ressource externe (icônes en SVG inline) : compatible avec une CSP stricte.\n\n## Étendre le package\n\nUn type de question = un **descripteur** et un **composant**. Le builder et le renderer ne\nlisent que le registre : ni l'un ni l'autre n'a besoin d'être modifié.\n\n```tsx\nimport { questionRegistry } from '@dahhouilyas/survey/core';\nimport { defaultFieldComponents } from '@dahhouilyas/survey/runtime';\n\nquestionRegistry.register({\n  type: 'signature',\n  category: 'text',\n  label: { fr: 'Signature', en: 'Signature' },\n  icon: 'text',\n  hasValue: true,\n  supportsChoices: false,\n  createDefault: (name, id) => ({ id, name, type: 'signature', title: 'Signature' }),\n  // Ces propriétés génèrent automatiquement le panneau d'édition du builder.\n  properties: [{ name: 'penColor', label: 'Couleur du trait', editor: 'text', group: 'options' }],\n  isEmpty: (value) => !value,\n  getDisplayValue: (value) => (value ? 'Signé' : ''),\n});\n\nconst components = { ...defaultFieldComponents, signature: SignatureField };\n```\n\n## Migration du schéma\n\nLes définitions sont stockées en base et survivent aux montées de version. Toute évolution\nincompatible incrémente `CURRENT_SCHEMA_VERSION` et ajoute une étape dans `MIGRATIONS` ; le\nrenderer et le builder appellent `migrateSurvey` avant toute autre opération. Une définition\nproduite par une version **plus récente** est refusée plutôt que traitée au mieux — la faire\ntourner sur un moteur qui ignore ses nouveautés produirait des réponses silencieusement\nincomplètes.\n\n## Ce que le backend doit prévoir\n\nLe package ne s'en occupe pas, mais expose les types que le backend doit refléter :\n\n| Élément                                               | Type                              |\n| ----------------------------------------------------- | --------------------------------- |\n| Définition d'un sondage (colonne JSON de la campagne) | `SurveyDefinition`                |\n| Réponse d'un utilisateur                              | `SurveyResult`                    |\n| Contrôle d'un JSON reçu par API                       | `parseSurvey` / `safeParseSurvey` |\n| Contrôle de conception avant publication              | `validateSurveyDefinition`        |\n\n`@dahhouilyas/survey/core` n'a aucune dépendance à React : ces fonctions s'utilisent telles quelles\ndans un service Node.\n\n## Développement\n\n```bash\npnpm install\npnpm dev              # bac à sable : builder + rendu, exemples satisfaction / KYC / conditions\npnpm test             # 545 tests\npnpm test:coverage    # seuils : 90 % statements, 85 % branches sur src/core\npnpm verify           # lint + typecheck + tests + build + publint + attw\n```\n","readmeFilename":"README.md","_rev":"1-0ba6bc38f8458d5bff43d32767e015ea"}