@mostajs/qrpanel 0.4.0 → 0.6.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.
@@ -0,0 +1,32 @@
1
+ export interface QrDataUrlOptions {
2
+ /** Côté de l'image en pixels. Défaut 200. */
3
+ size?: number;
4
+ /** Marge blanche autour du QR (en modules). Défaut 2. */
5
+ margin?: number;
6
+ /** Correction d'erreur : L 7 % | M 15 % | Q 25 % | H 30 %. Défaut 'M'. */
7
+ errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H';
8
+ /** Couleur des modules sombres. Défaut '#0f172a'. */
9
+ darkColor?: string;
10
+ /** Couleur du fond. Défaut '#ffffff'. */
11
+ lightColor?: string;
12
+ }
13
+ export interface UseQrDataUrlResult {
14
+ /** Data URL `data:image/png;base64,…` du QR, ou '' tant que non prêt. */
15
+ src: string;
16
+ /** True pendant la génération. */
17
+ loading: boolean;
18
+ /** Message d'erreur si la génération a échoué, sinon null. */
19
+ error: string | null;
20
+ }
21
+ /** Hook : génère le data URL d'un QR localement (navigateur). */
22
+ export declare function useQrDataUrl(data: string, opts?: QrDataUrlOptions): UseQrDataUrlResult;
23
+ export interface QrImageProps extends QrDataUrlOptions {
24
+ /** Donnée encodée dans le QR (URL, texte…). */
25
+ data: string;
26
+ alt?: string;
27
+ style?: React.CSSProperties;
28
+ className?: string;
29
+ }
30
+ /** QR code généré localement et rendu en `<img>` data-URL (COEP-safe). */
31
+ export default function QrImage({ data, alt, style, className, ...opts }: QrImageProps): import("react/jsx-runtime").JSX.Element;
32
+ //# sourceMappingURL=qr-image.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qr-image.d.ts","sourceRoot":"","sources":["../src/qr-image.tsx"],"names":[],"mappings":"AAuBA,MAAM,WAAW,gBAAgB;IAC/B,6CAA6C;IAC7C,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,yDAAyD;IACzD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,0EAA0E;IAC1E,oBAAoB,CAAC,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;IAC5C,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,yCAAyC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,yEAAyE;IACzE,GAAG,EAAE,MAAM,CAAA;IACX,kCAAkC;IAClC,OAAO,EAAE,OAAO,CAAA;IAChB,8DAA8D;IAC9D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CACrB;AAED,iEAAiE;AACjE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,gBAAqB,GAAG,kBAAkB,CAwB1F;AAED,MAAM,WAAW,YAAa,SAAQ,gBAAgB;IACpD,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAA;IACZ,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,KAAK,CAAC,aAAa,CAAA;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED,0EAA0E;AAC1E,MAAM,CAAC,OAAO,UAAU,OAAO,CAAC,EAC9B,IAAI,EAAE,GAAe,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,IAAI,EACjD,EAAE,YAAY,2CAqBd"}
@@ -0,0 +1,68 @@
1
+ // @mostajs/qrpanel/qr-image — composant QR généré côté navigateur
2
+ // Author: Dr Hamid MADANI <drmdh@msn.com>
3
+ //
4
+ // QR code généré 100% dans le navigateur (data URL via `qrcode`), sans
5
+ // appel réseau ni round-trip serveur. Complète les deux briques
6
+ // existantes de qrpanel :
7
+ // - `<QrPanel>` (./client) attend un `qrSrc` déjà fourni par l'app.
8
+ // - `generateQr*` (./server) génère côté Node uniquement (@resvg natif).
9
+ // `<QrImage>` couvre le cas manquant : génération locale, côté client.
10
+ //
11
+ // Cas d'usage clé — COEP `require-corp` : sous cross-origin isolation
12
+ // (requise par ex. par ffmpeg.wasm / SharedArrayBuffer), une image QR
13
+ // servie par un service externe est bloquée par la politique. La
14
+ // génération locale est alors la seule option côté client.
15
+ //
16
+ // Sub-export dédié (`@mostajs/qrpanel/qr-image`) : `qrcode` n'est
17
+ // embarqué dans le bundle client que pour les consumers qui l'importent.
18
+ 'use client';
19
+ import { jsx as _jsx } from "react/jsx-runtime";
20
+ import { useEffect, useState } from 'react';
21
+ import QRCode from 'qrcode';
22
+ /** Hook : génère le data URL d'un QR localement (navigateur). */
23
+ export function useQrDataUrl(data, opts = {}) {
24
+ const { size = 200, margin = 2, errorCorrectionLevel = 'M', darkColor = '#0f172a', lightColor = '#ffffff', } = opts;
25
+ const [src, setSrc] = useState('');
26
+ const [loading, setLoading] = useState(true);
27
+ const [error, setError] = useState(null);
28
+ useEffect(() => {
29
+ let alive = true;
30
+ setLoading(true);
31
+ setError(null);
32
+ setSrc('');
33
+ QRCode.toDataURL(data, {
34
+ width: size, margin, errorCorrectionLevel,
35
+ color: { dark: darkColor, light: lightColor },
36
+ })
37
+ .then((url) => { if (alive) {
38
+ setSrc(url);
39
+ setLoading(false);
40
+ } })
41
+ .catch((e) => {
42
+ if (alive) {
43
+ setError(e?.message || 'QR generation failed');
44
+ setLoading(false);
45
+ }
46
+ });
47
+ return () => { alive = false; };
48
+ }, [data, size, margin, errorCorrectionLevel, darkColor, lightColor]);
49
+ return { src, loading, error };
50
+ }
51
+ /** QR code généré localement et rendu en `<img>` data-URL (COEP-safe). */
52
+ export default function QrImage({ data, alt = 'QR code', style, className, ...opts }) {
53
+ const size = opts.size ?? 200;
54
+ const { src, loading, error } = useQrDataUrl(data, opts);
55
+ const box = {
56
+ width: size, height: size, display: 'flex',
57
+ alignItems: 'center', justifyContent: 'center',
58
+ background: '#fff', borderRadius: 6, border: '1px solid #e2e8f0',
59
+ fontSize: 11, color: '#94a3b8', textAlign: 'center',
60
+ ...style,
61
+ };
62
+ if (error)
63
+ return _jsx("div", { style: box, className: className, children: "QR indisponible" });
64
+ if (loading || !src)
65
+ return _jsx("div", { style: box, className: className, "aria-busy": "true", children: "\u2026" });
66
+ return (_jsx("img", { src: src, alt: alt, width: size, height: size, style: { background: '#fff', borderRadius: 6, display: 'block', ...style }, className: className }));
67
+ }
68
+ //# sourceMappingURL=qr-image.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"qr-image.js","sourceRoot":"","sources":["../src/qr-image.tsx"],"names":[],"mappings":"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"}
package/dist/qr-svg.js ADDED
@@ -0,0 +1,165 @@
1
+ // @mostajs/qrpanel/qr-svg — ARTEFACT GÉNÉRÉ depuis src/qr-svg.js (pur JS, copie 1:1) — ne pas éditer ici. Author: Dr Hamid MADANI <drmdh@msn.com>
2
+ // Complète qrpanel (qr-image = navigateur via `qrcode` ; server.js = @resvg natif) par un encodeur PUR JS server-side, idéal apps zéro-dép.
3
+ // Modes numérique & octet (byte/UTF-8), niveau de correction M, versions 1→10 (auto). Reed-Solomon GF(256), 8 masques + pénalité.
4
+ // Vérifié contre le vecteur de référence publié (« 01234567 », v1-M → 10 mots de code EC connus). Sortie : matrice booléenne + SVG.
5
+
6
+ // ── GF(256) — corps de Galois, primitif 0x11d (x^8+x^4+x^3+x^2+1). ──
7
+ const EXP = new Array(512), LOG = new Array(256);
8
+ (() => { let x = 1; for (let i = 0; i < 255; i++) { EXP[i] = x; LOG[x] = i; x <<= 1; if (x & 0x100) x ^= 0x11d; } for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255]; })();
9
+ const gmul = (a, b) => (a === 0 || b === 0) ? 0 : EXP[LOG[a] + LOG[b]];
10
+
11
+ // Polynôme générateur Reed-Solomon de degré `n`.
12
+ function rsGenPoly(n) {
13
+ let g = [1];
14
+ for (let i = 0; i < n; i++) { const ng = new Array(g.length + 1).fill(0); for (let j = 0; j < g.length; j++) { ng[j] ^= gmul(g[j], 1); ng[j + 1] ^= gmul(g[j], EXP[i]); } g = ng; }
15
+ return g;
16
+ }
17
+ // Mots de code de correction d'erreur pour `data` (octets) avec `ecLen` symboles.
18
+ function rsEncode(data, ecLen) {
19
+ const gen = rsGenPoly(ecLen); const res = new Array(ecLen).fill(0); // gen a ecLen+1 coeffs (gen[0]=1, ignoré dans la division)
20
+ for (const d of data) { const factor = d ^ res[0]; res.shift(); res.push(0); if (factor !== 0) for (let i = 0; i < ecLen; i++) res[i] ^= gmul(gen[i + 1], factor); }
21
+ return res;
22
+ }
23
+
24
+ // ── Tables niveau M, versions 1→10 : [totalDataCodewords, ecPerBlock, [ [nbBlocks, dataPerBlock], ... ] ]. ──
25
+ const VERSIONS_M = {
26
+ 1: [16, 10, [[1, 16]]], 2: [28, 16, [[1, 28]]], 3: [44, 26, [[1, 44]]], 4: [64, 18, [[2, 32]]],
27
+ 5: [86, 24, [[2, 43]]], 6: [108, 16, [[4, 27]]], 7: [124, 18, [[4, 31]]], 8: [154, 22, [[2, 38], [2, 39]]],
28
+ 9: [182, 22, [[3, 36], [2, 37]]], 10: [216, 26, [[4, 43], [1, 44]]],
29
+ };
30
+ // Positions des motifs d'alignement (centres) par version.
31
+ const ALIGN = { 1: [], 2: [6, 18], 3: [6, 22], 4: [6, 26], 5: [6, 30], 6: [6, 34], 7: [6, 22, 38], 8: [6, 24, 42], 9: [6, 26, 46], 10: [6, 28, 50] };
32
+ const sizeOf = (v) => 17 + 4 * v;
33
+
34
+ const isNumeric = (s) => /^[0-9]+$/.test(s);
35
+
36
+ // ── Encodage des données (bitstream) selon le mode. ──
37
+ function encodeData(text, version, totalDataCw) {
38
+ const bits = [];
39
+ const push = (val, len) => { for (let i = len - 1; i >= 0; i--) bits.push((val >> i) & 1); };
40
+ if (isNumeric(text)) {
41
+ push(0b0001, 4); // mode numérique
42
+ push(text.length, version < 10 ? 10 : 12);
43
+ for (let i = 0; i < text.length; i += 3) { const chunk = text.substr(i, 3); push(parseInt(chunk, 10), chunk.length === 3 ? 10 : chunk.length === 2 ? 7 : 4); }
44
+ } else {
45
+ const bytes = Buffer.from(text, 'utf8');
46
+ push(0b0100, 4); // mode octet
47
+ push(bytes.length, version < 10 ? 8 : 16);
48
+ for (const b of bytes) push(b, 8);
49
+ }
50
+ const cap = totalDataCw * 8;
51
+ for (let i = 0; i < 4 && bits.length < cap; i++) bits.push(0); // terminateur
52
+ while (bits.length % 8 !== 0) bits.push(0); // alignement octet
53
+ const pads = [0xec, 0x11]; let p = 0;
54
+ while (bits.length < cap) { push(pads[p % 2], 8); p++; }
55
+ const cw = []; for (let i = 0; i < bits.length; i += 8) { let b = 0; for (let j = 0; j < 8; j++) b = (b << 1) | bits[i + j]; cw.push(b); }
56
+ return cw;
57
+ }
58
+
59
+ // Choisit la plus petite version (≤10) qui contient `text`.
60
+ function pickVersion(text) {
61
+ const len = isNumeric(text) ? null : Buffer.from(text, 'utf8').length;
62
+ for (let v = 1; v <= 10; v++) {
63
+ const totalData = VERSIONS_M[v][0];
64
+ // estimation grossière de capacité : entête (~4 + count bits) puis données
65
+ const headerBits = 4 + (v < 10 ? (isNumeric(text) ? 10 : 8) : (isNumeric(text) ? 12 : 16));
66
+ const dataBits = isNumeric(text) ? Math.ceil(text.length / 3) * 10 : (len * 8);
67
+ if (headerBits + dataBits <= totalData * 8) return v;
68
+ }
69
+ throw new Error('contenu trop long pour QR v≤10 (raccourcir l\'URL/jeton)');
70
+ }
71
+
72
+ // Entrelace données + EC par blocs (spec QR).
73
+ function interleave(dataCw, version) {
74
+ const [, ecLen, groups] = VERSIONS_M[version];
75
+ const blocks = []; let idx = 0;
76
+ for (const [nb, dpb] of groups) for (let i = 0; i < nb; i++) { const d = dataCw.slice(idx, idx + dpb); idx += dpb; blocks.push({ d, e: rsEncode(d, ecLen) }); }
77
+ const maxD = Math.max(...blocks.map((b) => b.d.length));
78
+ const out = [];
79
+ for (let i = 0; i < maxD; i++) for (const b of blocks) if (i < b.d.length) out.push(b.d[i]);
80
+ for (let i = 0; i < ecLen; i++) for (const b of blocks) out.push(b.e[i]);
81
+ return out;
82
+ }
83
+
84
+ // ── Matrice : motifs de repérage, séparateurs, timing, alignement, module sombre, réservé format. ──
85
+ function buildMatrix(version) {
86
+ const n = sizeOf(version);
87
+ const m = Array.from({ length: n }, () => new Array(n).fill(null)); // null = libre
88
+ const fn = Array.from({ length: n }, () => new Array(n).fill(false)); // true = fonction (non masquable)
89
+ const set = (r, c, v) => { m[r][c] = v ? 1 : 0; fn[r][c] = true; };
90
+ const finder = (r, c) => { for (let i = -1; i <= 7; i++) for (let j = -1; j <= 7; j++) { const rr = r + i, cc = c + j; if (rr < 0 || rr >= n || cc < 0 || cc >= n) continue; const inb = i >= 0 && i <= 6 && j >= 0 && j <= 6; const ring = i === 0 || i === 6 || j === 0 || j === 6; const core = i >= 2 && i <= 4 && j >= 2 && j <= 4; set(rr, cc, inb && (ring || core)); } };
91
+ finder(0, 0); finder(0, n - 7); finder(n - 7, 0);
92
+ for (let i = 0; i < n; i++) { if (m[6][i] === null) set(6, i, i % 2 === 0); if (m[i][6] === null) set(i, 6, i % 2 === 0); } // timing
93
+ const ac = ALIGN[version];
94
+ for (const r of ac) for (const c of ac) { if ((r <= 7 && c <= 7) || (r <= 7 && c >= n - 8) || (r >= n - 8 && c <= 7)) continue; for (let i = -2; i <= 2; i++) for (let j = -2; j <= 2; j++) set(r + i, c + j, Math.max(Math.abs(i), Math.abs(j)) !== 1); }
95
+ set(n - 8, 8, 1); // module sombre
96
+ // Réserve les zones d'info de format (sans valeur définitive) → marquées fonction.
97
+ for (let i = 0; i < 9; i++) { if (m[8][i] === null) { m[8][i] = 0; fn[8][i] = true; } if (m[i][8] === null) { m[i][8] = 0; fn[i][8] = true; } }
98
+ for (let i = 0; i < 8; i++) { if (m[8][n - 1 - i] === null) { m[8][n - 1 - i] = 0; fn[8][n - 1 - i] = true; } if (m[n - 1 - i][8] === null) { m[n - 1 - i][8] = 0; fn[n - 1 - i][8] = true; } }
99
+ return { m, fn, n };
100
+ }
101
+
102
+ // Place le bitstream en zigzag (colonnes de 2, de droite à gauche, en sautant la colonne 6).
103
+ function placeData(m, fn, n, bytes) {
104
+ let bitIdx = 0; const bitsArr = []; for (const b of bytes) for (let i = 7; i >= 0; i--) bitsArr.push((b >> i) & 1);
105
+ let up = true;
106
+ for (let col = n - 1; col > 0; col -= 2) {
107
+ if (col === 6) col--;
108
+ for (let k = 0; k < n; k++) { const row = up ? n - 1 - k : k; for (const c of [col, col - 1]) { if (!fn[row][c]) { m[row][c] = bitIdx < bitsArr.length ? bitsArr[bitIdx] : 0; bitIdx++; } } }
109
+ up = !up;
110
+ }
111
+ }
112
+
113
+ const MASKS = [
114
+ (r, c) => (r + c) % 2 === 0, (r) => r % 2 === 0, (r, c) => c % 3 === 0, (r, c) => (r + c) % 3 === 0,
115
+ (r, c) => (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0, (r, c) => ((r * c) % 2) + ((r * c) % 3) === 0,
116
+ (r, c) => (((r * c) % 2) + ((r * c) % 3)) % 2 === 0, (r, c) => (((r + c) % 2) + ((r * c) % 3)) % 2 === 0,
117
+ ];
118
+ function applyMask(m, fn, n, maskFn) { const o = m.map((row) => row.slice()); for (let r = 0; r < n; r++) for (let c = 0; c < n; c++) if (!fn[r][c] && maskFn(r, c)) o[r][c] ^= 1; return o; }
119
+
120
+ // Info de format (niveau M = bits 00) + masque → 15 bits BCH, placée aux deux emplacements.
121
+ function placeFormat(m, n, maskId) {
122
+ const data = (0b00 << 3) | maskId; let bch = data << 10;
123
+ for (let i = 14; i >= 10; i--) if ((bch >> i) & 1) bch ^= 0b10100110111 << (i - 10);
124
+ const bits = (((data << 10) | bch) ^ 0b101010000010010);
125
+ const get = (i) => (bits >> i) & 1;
126
+ for (let i = 0; i <= 5; i++) m[8][i] = get(i);
127
+ m[8][7] = get(6); m[8][8] = get(7); m[7][8] = get(8);
128
+ for (let i = 9; i <= 14; i++) m[14 - i][8] = get(i);
129
+ for (let i = 0; i <= 7; i++) m[n - 1 - i][8] = get(i);
130
+ for (let i = 8; i <= 14; i++) m[8][n - 15 + i] = get(i);
131
+ }
132
+
133
+ function penalty(m, n) {
134
+ let p = 0;
135
+ for (let r = 0; r < n; r++) for (const line of [m[r], m.map((x) => x[r])]) { let run = 1; for (let c = 1; c < n; c++) { if (line[c] === line[c - 1]) { run++; if (run === 5) p += 3; else if (run > 5) p++; } else run = 1; } }
136
+ for (let r = 0; r < n - 1; r++) for (let c = 0; c < n - 1; c++) if (m[r][c] === m[r][c + 1] && m[r][c] === m[r + 1][c] && m[r][c] === m[r + 1][c + 1]) p += 3;
137
+ let dark = 0; for (let r = 0; r < n; r++) for (let c = 0; c < n; c++) dark += m[r][c]; const ratio = (dark * 100) / (n * n); p += Math.floor(Math.abs(ratio - 50) / 5) * 10;
138
+ return p;
139
+ }
140
+
141
+ /** Encode `text` en matrice QR (booléens true=module sombre). Niveau M, version auto ≤10. */
142
+ export function qrMatrix(text) {
143
+ const version = pickVersion(text);
144
+ const totalData = VERSIONS_M[version][0];
145
+ const dataCw = encodeData(text, version, totalData);
146
+ const finalCw = interleave(dataCw, version);
147
+ const { m, fn, n } = buildMatrix(version);
148
+ placeData(m, fn, n, finalCw);
149
+ let best = null, bestScore = Infinity, bestMask = 0;
150
+ for (let mask = 0; mask < 8; mask++) { const cand = applyMask(m, fn, n, MASKS[mask]); placeFormat(cand, n, mask); const s = penalty(cand, n); if (s < bestScore) { bestScore = s; best = cand; bestMask = mask; } }
151
+ return { matrix: best.map((row) => row.map((v) => v === 1)), size: n, version, mask: bestMask };
152
+ }
153
+
154
+ /** Rend `text` en SVG QR (scannable). `scale` px/module, `margin` modules de zone calme. */
155
+ export function qrSvg(text, { scale = 4, margin = 4, dark = '#000', light = '#fff' } = {}) {
156
+ const { matrix, size } = qrMatrix(text);
157
+ const dim = (size + margin * 2) * scale;
158
+ let rects = '';
159
+ for (let r = 0; r < size; r++) for (let c = 0; c < size; c++) if (matrix[r][c]) rects += `<rect x="${(c + margin) * scale}" y="${(r + margin) * scale}" width="${scale}" height="${scale}"/>`;
160
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${dim}" height="${dim}" viewBox="0 0 ${dim} ${dim}" shape-rendering="crispEdges" role="img" aria-label="QR code"><rect width="${dim}" height="${dim}" fill="${light}"/><g fill="${dark}">${rects}</g></svg>`;
161
+ }
162
+
163
+ // Exposés pour les tests (vérification contre le vecteur de référence publié).
164
+ export const _internal = { rsEncode, encodeData, interleave, VERSIONS_M };
165
+ export default { qrMatrix, qrSvg };
package/llms.txt ADDED
@@ -0,0 +1,20 @@
1
+ # @mostajs/qrpanel
2
+ Encodeur QR pur JS (zéro-dép) + figures + payloads typés. Versions 1→10, niveau M, v≥7 avec INFO DE VERSION (sinon QR rejeté). Reed-Solomon GF(256), 8 masques.
3
+ ## exports (. = src/index.js)
4
+ qrMatrix(text) → {matrix,size}. qrSvg(text,{scale=4,margin=4,dark,light}) → <svg>. qrLive(text,caption?,{scale=6,margin=3,maxWidth=240,dark,light}) → <figure> scannable + légende.
5
+ ## ./payloads (chaînes scan-and-act)
6
+ qrWifi({ssid,password,security=WPA,hidden}) · qrTel(num) · qrSms(num,body) · qrEmail(to,{subject,body}) · qrGeo(lat,lng) · qrUrl(u) · qrMeCard({name,tel,email,url,note}) · qrVCard({name,org,title,tel,email,url,adr,note}) · qrEvent({title,start,end,location,description}) [VEVENT]
7
+ ## ./figures
8
+ qrLogo(text,{logo,logoScale=.2,scale,margin,dark,light}) → SVG + logo central (⚠ payload court). qrCard(text,{title,subtitle,logo}) · qrBadge(text,{name,role,event}) · qrSheet([{data,caption}],{cols=3})
9
+ ## ./png (Node, node:zlib)
10
+ qrPng(text,{scale=6,margin=4}) → data:image/png;base64 (emails/billets où le SVG est bloqué)
11
+ ## ./decode (Node, dépend de jsqr) — DÉCODAGE (qrpanel encode ET décode, v0.6)
12
+ decodeQr(imageData|{data,width,height}[, w, h]) → string|null (texte du QR depuis une image RGBA)
13
+ ## ./browser-scan (asset navigateur) — scanner caméra prêt à servir (v0.6)
14
+ Fichier JS autonome à servir tel quel (<script src>), moteur jsQR. Expose window.jsQR ET
15
+ window.QRPanel.scan(videoEl, {onResult, onStatus, onError, continuous?, debounceMs?}) → {stop()}.
16
+ L'hôte fournit SON élément <video> (getUserMedia explicite — démarrage fiable sur geste
17
+ utilisateur) ; décodage jsQR à CHAQUE frame (BarcodeDetector en fast-path optionnel non bloquant).
18
+ ⚠ Servir avec Cache-Control: no-cache (sinon un asset périmé désynchronise avec le HTML hôte).
19
+ Décodage Node séparé : `./decode` (decodeQr, jsqr).
20
+ ## limite : versions ≤10 (~ M v10). Garder payloads raisonnables.
package/package.json CHANGED
@@ -1,46 +1,32 @@
1
1
  {
2
2
  "name": "@mostajs/qrpanel",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "QR code panel — server-side PNG/SVG generator with 12 built-in themes (image-as-frame composite, ECC=H), config-driven (.qrconfig.json), cross-OS no chromium, + React <QrPanel> client with copy/share/mailto.",
5
5
  "author": "Dr Hamid MADANI <drmdh@msn.com>",
6
6
  "license": "AGPL-3.0-or-later",
7
7
  "type": "module",
8
- "main": "dist/index.js",
8
+ "main": "src/index.js",
9
9
  "types": "dist/index.d.ts",
10
10
  "bin": {
11
11
  "qrpanel": "dist/cli.js"
12
12
  },
13
13
  "exports": {
14
- ".": {
15
- "types": "./dist/index.d.ts",
16
- "import": "./dist/index.js",
17
- "default": "./dist/index.js"
18
- },
19
- "./server": {
20
- "types": "./dist/server.d.ts",
21
- "import": "./dist/server.js",
22
- "default": "./dist/server.js"
23
- },
24
- "./client": {
25
- "types": "./dist/client.d.ts",
26
- "import": "./dist/client.js",
27
- "default": "./dist/client.js"
28
- },
29
- "./themes": {
30
- "types": "./dist/themes.d.ts",
31
- "import": "./dist/themes.js",
32
- "default": "./dist/themes.js"
33
- },
34
- "./config": {
35
- "types": "./dist/config.d.ts",
36
- "import": "./dist/config.js",
37
- "default": "./dist/config.js"
38
- }
14
+ ".": "./src/index.js",
15
+ "./qr-svg": "./src/qr-svg.js",
16
+ "./payloads": "./src/qr-payloads.js",
17
+ "./figures": "./src/qr-figures.js",
18
+ "./png": "./src/qr-png.js",
19
+ "./decode": "./src/qr-decode.js",
20
+ "./browser-scan": "./browser/qr-scan.js"
39
21
  },
40
22
  "files": [
41
23
  "dist",
24
+ "browser",
42
25
  "LICENSE",
43
- "README.md"
26
+ "README.md",
27
+ "CHANGELOG.md",
28
+ "llms.txt",
29
+ "src"
44
30
  ],
45
31
  "keywords": [
46
32
  "qrcode",
@@ -52,27 +38,37 @@
52
38
  "magic-link",
53
39
  "config-driven"
54
40
  ],
55
- "scripts": {
56
- "build": "tsc && chmod +x dist/cli.js 2>/dev/null || true",
57
- "dev": "tsc --watch",
58
- "prepublishOnly": "npm run build"
59
- },
60
41
  "dependencies": {
61
42
  "qrcode": "^1.5.4",
62
- "@mostajs/auth": "^3.2.0",
63
- "@resvg/resvg-js": "^2.6.2"
43
+ "jsqr": "^1.4.0"
64
44
  },
65
45
  "peerDependencies": {
66
- "react": ">=18 <20"
46
+ "react": ">=18 <20",
47
+ "@mostajs/auth": ">=3.2.0",
48
+ "@resvg/resvg-js": ">=2.6.2"
67
49
  },
68
50
  "peerDependenciesMeta": {
69
- "react": { "optional": true }
51
+ "react": {
52
+ "optional": true
53
+ },
54
+ "@mostajs/auth": {
55
+ "optional": true
56
+ },
57
+ "@resvg/resvg-js": {
58
+ "optional": true
59
+ }
70
60
  },
71
61
  "devDependencies": {
72
62
  "@types/node": "^22.0.0",
73
63
  "@types/qrcode": "^1.5.5",
74
64
  "@types/react": "^19.0.0",
65
+ "@mostajs/auth": "^3.2.0",
66
+ "@resvg/resvg-js": "^2.6.2",
75
67
  "react": "^19.0.0",
76
68
  "typescript": "^5.6.0"
69
+ },
70
+ "scripts": {
71
+ "build": "tsc && chmod +x dist/cli.js 2>/dev/null || true",
72
+ "dev": "tsc --watch"
77
73
  }
78
- }
74
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env node
2
+ // @mostajs/qrpanel — CLI
3
+ // Author: Dr Hamid MADANI <drmdh@msn.com>
4
+ //
5
+ // Commandes disponibles :
6
+ // qrpanel init → écrit .qrconfig.json à cwd avec les defaults
7
+ // qrpanel themes → liste les 12 thèmes natifs
8
+ // qrpanel --version → version du package
9
+
10
+ import { ensureQrConfig, DEFAULT_CONFIG } from './config.js'
11
+ import { listThemes, THEMES } from './themes.js'
12
+
13
+ const args = process.argv.slice(2)
14
+ const cmd = args[0] ?? 'help'
15
+
16
+ function help() {
17
+ console.log(`@mostajs/qrpanel — CLI
18
+
19
+ Usage:
20
+ qrpanel init [path] Generate .qrconfig.json at cwd (or [path]).
21
+ Idempotent — does not overwrite existing config.
22
+ qrpanel themes List built-in theme keys.
23
+ qrpanel --version | -v Print version.
24
+ qrpanel help | -h This help.
25
+
26
+ Config file (.qrconfig.json) drives all generators. Master toggle:
27
+ "genimage": false → bypass theme pipeline, QR-only output.
28
+ "theme": "random" → randomize from themePool at each generation.
29
+ `)
30
+ }
31
+
32
+ async function main() {
33
+ switch (cmd) {
34
+ case 'init': {
35
+ const cwd = args[1] ?? process.cwd()
36
+ const path = ensureQrConfig(cwd)
37
+ console.log(`✓ qrpanel config ready at: ${path}`)
38
+ console.log(` Edit it to customize defaults — master toggle "genimage" toggles the whole pipeline.`)
39
+ break
40
+ }
41
+ case 'themes': {
42
+ console.log(`Built-in themes (${listThemes().length}):`)
43
+ for (const key of listThemes()) {
44
+ console.log(` - ${key.padEnd(10)} (${THEMES[key].label})`)
45
+ }
46
+ console.log(`\nDefaults config :`)
47
+ console.log(` themePool : ${DEFAULT_CONFIG.default.themePool.join(', ')}`)
48
+ break
49
+ }
50
+ case '--version':
51
+ case '-v': {
52
+ // Lecture programmatique du package.json sans import JSON (cross-runtime safe)
53
+ const { readFileSync } = await import('node:fs')
54
+ const { fileURLToPath } = await import('node:url')
55
+ const { dirname, join } = await import('node:path')
56
+ const here = dirname(fileURLToPath(import.meta.url))
57
+ const pkgPath = join(here, '..', 'package.json')
58
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { version: string }
59
+ console.log(pkg.version)
60
+ break
61
+ }
62
+ case 'help':
63
+ case '--help':
64
+ case '-h':
65
+ default:
66
+ help()
67
+ break
68
+ }
69
+ }
70
+
71
+ main().catch((e) => {
72
+ console.error('[qrpanel] error:', (e as Error).message)
73
+ process.exit(1)
74
+ })