@jonathankablan/noraivoire-fx-text 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/LICENSE +64 -0
  2. package/README.md +15 -2
  3. package/package.json +30 -4
  4. package/src/index.js +122 -0
package/LICENSE ADDED
@@ -0,0 +1,64 @@
1
+ NORAIVOIRE — LICENCE PROPRIÉTAIRE
2
+ Copyright (c) 2026 Jonathan KABLAN. Tous droits réservés.
3
+
4
+ Le présent logiciel, incluant sans limitation son code source, ses spécifications
5
+ (fichiers .nora.json), son compilateur, son moteur d'effets, ses adaptateurs, ses
6
+ modèles (templates), sa documentation, ses jeux de tests et ses éléments graphiques
7
+ (ci-après « le Logiciel »), est la propriété exclusive de Jonathan KABLAN
8
+ (ci-après « le Titulaire »).
9
+
10
+ 1. ABSENCE DE LICENCE OPEN SOURCE
11
+ Le Logiciel n'est pas distribué sous une licence open source. Aucun droit
12
+ n'est concédé implicitement, par estoppel ou autrement. La simple mise à
13
+ disposition du Logiciel, y compris dans un dépôt privé ou un registre de
14
+ paquets privé, ne vaut pas concession de licence.
15
+
16
+ 2. USAGE AUTORISÉ
17
+ Tout usage du Logiciel est subordonné à un contrat de licence écrit et signé
18
+ par le Titulaire (ci-après « Contrat de Licence »). En l'absence d'un tel
19
+ contrat, seul le Titulaire est autorisé à utiliser le Logiciel.
20
+
21
+ 3. INTERDICTIONS
22
+ Sauf autorisation écrite et préalable du Titulaire, il est interdit de :
23
+ a) copier, reproduire, modifier, adapter, traduire ou créer des œuvres
24
+ dérivées du Logiciel, en tout ou partie ;
25
+ b) distribuer, publier, sous-licencier, louer, prêter, vendre ou mettre à
26
+ disposition le Logiciel à des tiers, y compris sous forme de service
27
+ (SaaS) ou intégré dans un autre produit distribué ;
28
+ c) décompiler, désassembler ou procéder à de l'ingénierie inverse du
29
+ Logiciel, sauf dans les limites strictement prévues par l'article
30
+ L.122-6-1 du Code de la propriété intellectuelle ;
31
+ d) supprimer ou altérer les mentions de propriété, de copyright ou la
32
+ présente licence ;
33
+ e) utiliser le Logiciel, ses spécifications ou sa documentation pour
34
+ entraîner, affiner ou évaluer un modèle d'intelligence artificielle
35
+ destiné à des tiers, ou pour concevoir un produit concurrent.
36
+
37
+ 4. PRODUITS GÉNÉRÉS
38
+ Les fichiers produits par le compilateur NoraIvoire pour le compte d'un
39
+ licencié (CSS, Dart, TypeScript générés) peuvent être intégrés dans les
40
+ applications de ce licencié dans les conditions fixées par son Contrat de
41
+ Licence. Ils ne peuvent pas être redistribués en tant que bibliothèque,
42
+ framework ou kit de design autonome.
43
+
44
+ 5. CONFIDENTIALITÉ
45
+ Le Logiciel contient des informations confidentielles et des secrets
46
+ d'affaires du Titulaire. Toute personne y ayant accès s'engage à en
47
+ préserver la confidentialité.
48
+
49
+ 6. ABSENCE DE GARANTIE
50
+ Sauf stipulation contraire du Contrat de Licence, le Logiciel est fourni
51
+ « EN L'ÉTAT », sans garantie d'aucune sorte, expresse ou implicite. Dans
52
+ les limites autorisées par la loi, le Titulaire ne saurait être tenu
53
+ responsable d'aucun dommage résultant de l'utilisation du Logiciel.
54
+
55
+ 7. COMPOSANTS TIERS
56
+ Le Logiciel peut référencer des composants tiers soumis à leurs propres
57
+ licences, listés dans THIRD_PARTY_NOTICES.md. La présente licence ne
58
+ modifie pas ces licences.
59
+
60
+ 8. DROIT APPLICABLE
61
+ La présente licence est régie par le droit français. Tout litige relève de
62
+ la compétence des tribunaux du ressort de la Cour d'appel de Paris.
63
+
64
+ Contact licences : jonathan.kablan@gmail.com
package/README.md CHANGED
@@ -1,3 +1,16 @@
1
- # Temporary Holding Version
1
+ # @jonathankablan/noraivoire-fx-text
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Effets de texte optionnels de NoraIvoire 0.1.0 : `split`, `scramble`. Chargés à la demande, texte complet lu une seule fois par les lecteurs d'écran, DOM restauré au démontage, mouvement réduit respecté.
4
+
5
+ ```html
6
+ <link rel="stylesheet" href="node_modules/@jonathankablan/noraivoire-core/dist/fx/fx-text.css">
7
+ <script type="module">
8
+ import * as moteur from '@jonathankablan/noraivoire-core/engine';
9
+ import { install } from '@jonathankablan/noraivoire-fx-text';
10
+ install(moteur);
11
+ moteur.start();
12
+ </script>
13
+ <h1 data-fx="split" data-fx-split="words:true;stagger:60">Bonjour Abidjan</h1>
14
+ ```
15
+
16
+ Logiciel propriétaire © 2026 Jonathan KABLAN, tous droits réservés (voir LICENSE). La mise à disposition sur npm ne vaut pas concession de licence : tout usage est subordonné à un contrat de licence écrit.
package/package.json CHANGED
@@ -1,6 +1,32 @@
1
1
  {
2
2
  "name": "@jonathankablan/noraivoire-fx-text",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "license": "UNLICENSED",
5
+ "description": "Effets de texte optionnels NoraIvoire (split, scramble), chargés à la demande. Propriétaire.",
6
+ "author": "Jonathan KABLAN",
7
+ "type": "module",
8
+ "main": "src/index.js",
9
+ "exports": {
10
+ ".": "./src/index.js"
11
+ },
12
+ "sideEffects": false,
13
+ "engines": {
14
+ "node": ">=20"
15
+ },
16
+ "dependencies": {},
17
+ "keywords": [
18
+ "noraivoire",
19
+ "text",
20
+ "animation",
21
+ "effects"
22
+ ],
23
+ "files": [
24
+ "src",
25
+ "LICENSE",
26
+ "README.md"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public",
30
+ "registry": "https://registry.npmjs.org/"
31
+ }
32
+ }
package/src/index.js ADDED
@@ -0,0 +1,122 @@
1
+ // NoraIvoire — Effets de texte optionnels : split, scramble.
2
+ // © 2026 Jonathan KABLAN — Propriétaire, tous droits réservés.
3
+ //
4
+ // Arbitrage documenté (docs/SECURITY.md §moteur) : découper ou brouiller un texte
5
+ // impose de créer des nœuds. Ce paquet étend donc, et SEULEMENT lui, le contrat
6
+ // du moteur, de façon contrôlée et vérifiée par `nora audit` :
7
+ // - création de <span> par document.createElement + textContent (jamais de HTML) ;
8
+ // - attributs posés limités à aria-hidden et data-fx-* ;
9
+ // - styles limités aux variables --fx-* ;
10
+ // - les nœuds d'origine sont conservés et remis en place au démontage
11
+ // (mêmes objets : un framework qui les référence les retrouve).
12
+ // N'agit que sur un élément qui ne contient QUE du texte (≤ 500 caractères).
13
+ // Le texte complet reste lisible une seule fois par les lecteurs d'écran
14
+ // (copie visuellement masquée data-fx-sr), la version animée est aria-hidden.
15
+
16
+ const MAX_TEXT = 500;
17
+ const GLYPHS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789#%&*+=<>';
18
+
19
+ /** Découpe pure : graphèmes (ou mots) ; les espaces restent des segments à part. */
20
+ export function segments(text, words = false) {
21
+ if (words) return text.split(/(\s+)/).filter(Boolean);
22
+ if (typeof Intl !== 'undefined' && typeof Intl.Segmenter === 'function') {
23
+ return [...new Intl.Segmenter(undefined, { granularity: 'grapheme' }).segment(text)].map((s) => s.segment);
24
+ }
25
+ return Array.from(text);
26
+ }
27
+
28
+ /** Texte brouillé : les `progress` premiers pourcents sont révélés, espaces conservés. */
29
+ export function scrambleText(text, progress, rand = Math.random) {
30
+ const chars = Array.from(text);
31
+ const shown = Math.floor(chars.length * Math.min(1, Math.max(0, progress)));
32
+ return chars.map((c, i) => (i < shown || /\s/.test(c) ? c : GLYPHS[Math.floor(rand() * GLYPHS.length)])).join('');
33
+ }
34
+
35
+ /** Élément texte seul, non vide et borné ; sinon l'effet ne touche à rien. */
36
+ function plainText(el) {
37
+ for (const n of el.childNodes) if (n.nodeType !== 3) return null;
38
+ const text = el.textContent ?? '';
39
+ return text.trim() && text.length <= MAX_TEXT ? text : null;
40
+ }
41
+
42
+ function span(attr, text) {
43
+ const s = document.createElement('span');
44
+ s.setAttribute(attr, '');
45
+ if (text !== undefined) s.textContent = text;
46
+ return s;
47
+ }
48
+
49
+ /** Remplace le contenu par [copie lue | version animée aria-hidden] ; renvoie la restauration. */
50
+ function stage(el, text) {
51
+ const original = [...el.childNodes];
52
+ const visual = span('data-fx-visual');
53
+ visual.setAttribute('aria-hidden', 'true');
54
+ el.replaceChildren(span('data-fx-sr', text), visual);
55
+ return { visual, restore: () => el.replaceChildren(...original) };
56
+ }
57
+
58
+ export const effects = Object.freeze({
59
+ split: {
60
+ options: {
61
+ words: { type: 'boolean', default: false },
62
+ stagger: { type: 'number', default: 30, min: 0, max: 200 },
63
+ },
64
+ mount(el, opts, ctx) {
65
+ const text = plainText(el);
66
+ if (ctx.reducedMotion || text === null) return;
67
+ const { visual, restore } = stage(el, text);
68
+ let i = 0;
69
+ for (const seg of segments(text, opts.words)) {
70
+ if (/^\s+$/.test(seg)) { visual.append(seg); continue; }
71
+ const piece = span('data-fx-piece', seg);
72
+ piece.style.setProperty('--fx-i', String(i++));
73
+ visual.append(piece);
74
+ }
75
+ ctx.set('--fx-stagger', `${opts.stagger}ms`);
76
+ const show = () => el.setAttribute('data-fx-visible', '');
77
+ if (typeof IntersectionObserver === 'undefined') show();
78
+ else ctx.onVisible((v) => { if (v) show(); });
79
+ return () => {
80
+ restore();
81
+ el.style.removeProperty('--fx-stagger');
82
+ el.removeAttribute('data-fx-visible');
83
+ };
84
+ },
85
+ },
86
+
87
+ scramble: {
88
+ options: { duration: { type: 'number', default: 800, min: 0, max: 5000 } },
89
+ mount(el, opts, ctx) {
90
+ const text = plainText(el);
91
+ if (ctx.reducedMotion || text === null || opts.duration === 0) return;
92
+ let staged = null;
93
+ let played = false;
94
+ const finish = () => { if (staged) { staged.restore(); staged = null; } };
95
+ const play = () => {
96
+ if (played) return;
97
+ played = true;
98
+ staged = stage(el, text);
99
+ const t0 = performance.now();
100
+ const tick = () => {
101
+ if (!staged) return;
102
+ const p = (performance.now() - t0) / opts.duration;
103
+ if (p >= 1) { finish(); return; }
104
+ staged.visual.textContent = scrambleText(text, p);
105
+ ctx.frame(tick);
106
+ };
107
+ tick();
108
+ };
109
+ if (typeof IntersectionObserver === 'undefined') play();
110
+ else ctx.onVisible((v) => { if (v) play(); });
111
+ return finish;
112
+ },
113
+ },
114
+ });
115
+
116
+ /** Enregistre split et scramble dans le moteur fourni. Idempotent. */
117
+ export function install(engine) {
118
+ if (!engine || typeof engine.defineEffect !== 'function') throw new TypeError('NoraIvoire : install() attend le module du moteur');
119
+ for (const [name, def] of Object.entries(effects)) {
120
+ if (!engine.hasEffect?.(name)) engine.defineEffect(name, def);
121
+ }
122
+ }