@mostajs/qrpanel 0.2.0 → 0.3.1

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.
package/README.md CHANGED
@@ -1,12 +1,12 @@
1
1
  # @mostajs/qrpanel
2
2
 
3
- QR code panel — générateur server-side *(PNG / SVG / data URL via la lib `qrcode`)* et composant React `<QrPanel>` *(QR + lien hypertexte copiable + actions copier / ouvrir / mailto)*.
3
+ QR code panel — générateur server-side *(PNG / SVG / data URL via la lib `qrcode`)* avec **12 thèmes natifs** *(image-as-frame composite, ECC=H par défaut)*, piloté par un fichier de config `.qrconfig.json` éditable. Composant React `<QrPanel>` *(QR + lien hypertexte copiable + actions copier / ouvrir / mailto)*.
4
4
 
5
5
  **Auteur** : Dr Hamid MADANI <drmdh@msn.com>
6
6
 
7
7
  ## Cross-OS
8
8
 
9
- Aucune dépendance native. La lib `qrcode` est pure JS fonctionne identiquement sur **Linux, macOS, Windows**, en runtime Node ou Edge. Pas de chromium, pas de node-gyp.
9
+ Aucune dépendance Chromium / Puppeteer / node-gyp. Pure-JS pour `qrcode`, prebuilt-binaries Rust pour le rasterizer (`@resvg/resvg-js`). Fonctionne identiquement sur **Linux, macOS, Windows** *(Node ≥18)*.
10
10
 
11
11
  ## Installation
12
12
 
@@ -14,23 +14,102 @@ Aucune dépendance native. La lib `qrcode` est pure JS — fonctionne identiquem
14
14
  npm install @mostajs/qrpanel
15
15
  ```
16
16
 
17
- ## Usage côté serveur
17
+ ## Quickstart
18
18
 
19
19
  ```ts
20
- import { generateQrPng, generateQrSvg, generateQrDataUrl } from '@mostajs/qrpanel/server'
20
+ import { generateQrPng, ensureQrConfig } from '@mostajs/qrpanel/server'
21
+
22
+ // (Optionnel) écrire .qrconfig.json à la racine — lifecycle init / boot.
23
+ ensureQrConfig()
24
+
25
+ // Génère un PNG composite (cadre thème "random" tiré dans themePool).
26
+ const png = await generateQrPng('https://example.com/invite/abc')
27
+ ```
28
+
29
+ ## v0.3 — thèmes & config
30
+
31
+ Le module embarque **12 thèmes vectoriels** *(SVG inline mono-color, ~2-3 Ko chacun, libres de droits — création maison)*, répliqués aux 4 coins d'un cadre décoratif. Le QR reste centré sur un cartouche blanc plein, ECC=H par défaut → scannable même avec cadre coloré.
32
+
33
+ ### Thèmes natifs
34
+
35
+ | Clé | Label | Clé | Label |
36
+ |---|---|---|---|
37
+ | `baby` | Bébé | `nature` | Nature |
38
+ | `animals` | Animaux | `tech` | Tech |
39
+ | `science` | Science | `space` | Espace |
40
+ | `physics` | Physique | `music` | Musique |
41
+ | `chemistry` | Chimie | `book` | Livre |
42
+ | `math` | Mathématique | `health` | Santé |
43
+
44
+ ### Fichier `.qrconfig.json`
45
+
46
+ Généré à `process.cwd()` via `ensureQrConfig()` ou `npx qrpanel init`. Pilote tous les générateurs au runtime *(cache invalidé par `mtime` — édite le fichier, le prochain appel le relit)*.
47
+
48
+ ```jsonc
49
+ {
50
+ "default": {
51
+ "genimage": true, // ← master toggle (false = QR pur, bypass total)
52
+ "format": "svg",
53
+ "width": 600,
54
+ "margin": 2,
55
+ "errorCorrectionLevel": "H", // overlay-safe (30% redondance)
56
+ "darkColor": "#0f172a",
57
+ "lightColor": "#ffffff",
58
+ "theme": "random", // 'random' | clé thème | 'none'
59
+ "themePool": [
60
+ "baby", "animals", "science", "physics",
61
+ "chemistry", "math", "nature", "tech",
62
+ "space", "music", "book", "health"
63
+ ],
64
+ "framePadding": 0.13,
65
+ "centerWhiteRatio": 0.62,
66
+ "themeOpacity": 1.0,
67
+ "themeColor": "#1e293b"
68
+ }
69
+ }
70
+ ```
71
+
72
+ ### Cascade de résolution
73
+
74
+ ```
75
+ defaults compilés < .qrconfig.json (cwd) < options passées à l'appel
76
+ ```
77
+
78
+ ### Master toggle `genimage`
79
+
80
+ ```jsonc
81
+ { "default": { "genimage": false } } // OU :
82
+ generateQrPng(url, { genimage: false }) // override ponctuel
83
+ ```
84
+
85
+ → court-circuite tout le pipeline composite, retombe sur le **QR pur** *(comportement v0.2.x identique)*. Utile en debug / CI / mode "print neutre".
86
+
87
+ ## Usage côté serveur
21
88
 
22
- // Buffer PNG pour streamer dans une route
23
- const png = await generateQrPng('https://example.com/invite/abc', {
89
+ ```ts
90
+ import {
91
+ generateQrPng, generateQrSvg, generateQrDataUrl,
92
+ listThemes, ensureQrConfig,
93
+ } from '@mostajs/qrpanel/server'
94
+
95
+ // Buffer PNG composite (cadre thème science)
96
+ const png = await generateQrPng('https://example.com/x', {
97
+ theme: 'science',
24
98
  width: 600,
25
- errorCorrectionLevel: 'M',
26
- darkColor: '#2563eb',
99
+ themeColor: '#0ea5e9',
100
+ })
101
+
102
+ // SVG composite avec random tiré dans un sous-set
103
+ const svg = await generateQrSvg('https://example.com/x', {
104
+ theme: 'random',
105
+ themePool: ['math', 'physics', 'chemistry'],
27
106
  })
28
107
 
29
- // SVG pour embed inline
30
- const svg = await generateQrSvg('https://...', { width: 200 })
108
+ // DataURL PNG composite (image inline)
109
+ const dataUrl = await generateQrDataUrl('https://example.com/x')
31
110
 
32
- // Data URL pour <img src> direct
33
- const dataUrl = await generateQrDataUrl('https://...')
111
+ // Override ponctuel désactive le composite ne serait-ce qu'une fois
112
+ const plain = await generateQrPng('https://example.com/x', { genimage: false })
34
113
  ```
35
114
 
36
115
  ### Endpoint Next.js (App Router)
@@ -44,13 +123,23 @@ export const runtime = 'nodejs'
44
123
 
45
124
  export async function GET(req: Request) {
46
125
  const url = new URL(req.url).searchParams.get('text') ?? ''
47
- const png = await generateQrPng(url)
126
+ const theme = (new URL(req.url).searchParams.get('theme') ?? 'random') as any
127
+ const png = await generateQrPng(url, { theme })
48
128
  return new NextResponse(png as any, {
49
129
  headers: { 'Content-Type': 'image/png' },
50
130
  })
51
131
  }
52
132
  ```
53
133
 
134
+ ### Thème custom inline
135
+
136
+ ```ts
137
+ const png = await generateQrPng(url, {
138
+ theme: { svg: '<circle cx="0" cy="0" r="5" fill="currentColor"/>', label: 'My logo' },
139
+ themeColor: '#ff0000',
140
+ })
141
+ ```
142
+
54
143
  ## Usage côté client (React)
55
144
 
56
145
  ```tsx
@@ -62,14 +151,14 @@ import { QrPanel } from '@mostajs/qrpanel/client'
62
151
  key: 'direct',
63
152
  label: 'Direct',
64
153
  url: 'https://app.example.com/projet-x',
65
- qrSrc: '/api/qr?text=' + encodeURIComponent('https://app.example.com/projet-x'),
154
+ qrSrc: '/api/qr?text=' + encodeURIComponent('https://app.example.com/projet-x') + '&theme=science',
66
155
  description: 'Le QR mène directement au questionnaire.',
67
156
  },
68
157
  {
69
158
  key: 'invite',
70
159
  label: 'Invite token',
71
160
  url: 'https://app.example.com/invite/eyJ...',
72
- qrSrc: '/api/qr?text=' + encodeURIComponent('https://app.example.com/invite/eyJ...'),
161
+ qrSrc: '/api/qr?text=...&theme=random',
73
162
  description: 'Token signé valable 60 jours, idéal pour mailing.',
74
163
  },
75
164
  ]}
@@ -78,15 +167,30 @@ import { QrPanel } from '@mostajs/qrpanel/client'
78
167
  />
79
168
  ```
80
169
 
81
- ### Avec un seul mode
170
+ ## CLI
82
171
 
83
- ```tsx
84
- <QrPanel modes={[
85
- { key: 'd', label: 'Direct', url, qrSrc },
86
- ]} />
172
+ ```bash
173
+ npx qrpanel init # écrit .qrconfig.json (idempotent)
174
+ npx qrpanel themes # liste les 12 thèmes
175
+ npx qrpanel --version
87
176
  ```
88
177
 
89
- Le toggle de modes est masqué automatiquement quand `modes.length === 1`.
178
+ ## Helper combiné `buildInviteUrls`
179
+
180
+ Inchangé depuis v0.2.
181
+
182
+ ```ts
183
+ import { buildInviteUrls } from '@mostajs/qrpanel/server'
184
+
185
+ const { directUrl, inviteUrl, inviteToken } = buildInviteUrls({
186
+ baseUrl: 'https://app.example.com',
187
+ directPath: '/projet-x',
188
+ inviteSecret: process.env.INVITE_SECRET!,
189
+ inviteId: project.id,
190
+ ttlMs: 60 * 24 * 3600 * 1000,
191
+ inviteMeta: { kind: 'cohort-invite' },
192
+ })
193
+ ```
90
194
 
91
195
  ## API
92
196
 
@@ -94,47 +198,57 @@ Le toggle de modes est masqué automatiquement quand `modes.length === 1`.
94
198
 
95
199
  | Fonction | Retour | Cas |
96
200
  |----------|--------|-----|
97
- | `generateQrPng(text, opts)` | `Promise<Buffer>` | Stream PNG dans une route |
98
- | `generateQrSvg(text, opts)` | `Promise<string>` | Embed inline, scalable |
99
- | `generateQrDataUrl(text, opts)` | `Promise<string>` | `<img src>` direct |
201
+ | `generateQrPng(text, opts?)` | `Promise<Buffer>` | PNG composite ou QR pur si genimage=false |
202
+ | `generateQrSvg(text, opts?)` | `Promise<string>` | SVG composite ou QR pur |
203
+ | `generateQrDataUrl(text, opts?)` | `Promise<string>` | DataURL PNG |
204
+ | `loadQrConfig(cwd?)` | `QrConfig` | Lit `.qrconfig.json` *(cached mtime)* |
205
+ | `ensureQrConfig(cwd?, overrides?)` | `string` | Crée `.qrconfig.json` si absent (idempotent) |
206
+ | `listThemes()` | `ThemeKey[]` | Liste des 12 clés natives |
207
+ | `buildInviteUrls(opts)` | `InviteUrls` | URL builder + invite-token signé |
100
208
 
101
- ### Options communes
209
+ ### `QrOptions`
102
210
 
103
211
  ```ts
104
212
  interface QrOptions {
105
- width?: number // px, default 600
106
- margin?: number // modules, default 2
107
- errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H' // default 'M'
108
- darkColor?: string // hex, default '#0f172a'
109
- lightColor?: string // hex, default '#ffffff'
213
+ // Communs
214
+ width?: number
215
+ margin?: number
216
+ errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H'
217
+ darkColor?: string
218
+ lightColor?: string
219
+
220
+ // v0.3 thématique
221
+ genimage?: boolean // master toggle (override config)
222
+ theme?: ThemeKey | 'random' | 'none' | { svg: string; label?: string }
223
+ themePool?: ThemeKey[]
224
+ framePadding?: number // 0..0.5
225
+ centerWhiteRatio?: number // 0..1
226
+ themeOpacity?: number // 0..1
227
+ themeColor?: string // CSS color
110
228
  }
111
229
  ```
112
230
 
113
- ### Client `<QrPanel>`
231
+ ### Client `<QrPanel>` — inchangé v0.2
114
232
 
115
233
  ```ts
116
- interface QrPanelMode {
117
- key: string // identifiant interne
118
- label: string // bouton
119
- url: string // URL textuelle copiable
120
- qrSrc: string // src de l'image QR
121
- description?: string
122
- }
123
-
124
234
  interface QrPanelProps {
125
235
  modes: QrPanelMode[]
126
- initialModeIndex?: number // default 0
236
+ initialModeIndex?: number
127
237
  title?: string
128
- mailSubject?: string // default 'Invitation'
129
- mailBodyTemplate?: string // {url} = placeholder
130
- qrSize?: number // px, default 260
238
+ mailSubject?: string
239
+ mailBodyTemplate?: string
240
+ qrSize?: number
131
241
  className?: string
132
242
  }
133
243
  ```
134
244
 
135
- ## Style
245
+ ## Dépendances
136
246
 
137
- Le composant utilise des classes Tailwind v3+. L'app consumer doit avoir Tailwind dans son pipeline. Les classes sont des chaînes, le composant reste portable *(utilisable même si l'app override le CSS)*.
247
+ | Package | Rôle | Pourquoi pas X |
248
+ |---|---|---|
249
+ | `qrcode` | Génère le QR pur (SVG/PNG) | Pure-JS, cross-OS, déjà éprouvé |
250
+ | `@resvg/resvg-js` | Rasterise le SVG composite en PNG | Rust prebuilt binaries (no node-gyp, no chromium) |
251
+ | `@mostajs/auth` | `signInviteToken` pour `buildInviteUrls` | Cohérence écosystème mostajs |
138
252
 
139
253
  ## Licence
140
254
 
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,70 @@
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
+ import { ensureQrConfig, DEFAULT_CONFIG } from './config.js';
10
+ import { listThemes, THEMES } from './themes.js';
11
+ const args = process.argv.slice(2);
12
+ const cmd = args[0] ?? 'help';
13
+ function help() {
14
+ console.log(`@mostajs/qrpanel — CLI
15
+
16
+ Usage:
17
+ qrpanel init [path] Generate .qrconfig.json at cwd (or [path]).
18
+ Idempotent — does not overwrite existing config.
19
+ qrpanel themes List built-in theme keys.
20
+ qrpanel --version | -v Print version.
21
+ qrpanel help | -h This help.
22
+
23
+ Config file (.qrconfig.json) drives all generators. Master toggle:
24
+ "genimage": false → bypass theme pipeline, QR-only output.
25
+ "theme": "random" → randomize from themePool at each generation.
26
+ `);
27
+ }
28
+ async function main() {
29
+ switch (cmd) {
30
+ case 'init': {
31
+ const cwd = args[1] ?? process.cwd();
32
+ const path = ensureQrConfig(cwd);
33
+ console.log(`✓ qrpanel config ready at: ${path}`);
34
+ console.log(` Edit it to customize defaults — master toggle "genimage" toggles the whole pipeline.`);
35
+ break;
36
+ }
37
+ case 'themes': {
38
+ console.log(`Built-in themes (${listThemes().length}):`);
39
+ for (const key of listThemes()) {
40
+ console.log(` - ${key.padEnd(10)} (${THEMES[key].label})`);
41
+ }
42
+ console.log(`\nDefaults config :`);
43
+ console.log(` themePool : ${DEFAULT_CONFIG.default.themePool.join(', ')}`);
44
+ break;
45
+ }
46
+ case '--version':
47
+ case '-v': {
48
+ // Lecture programmatique du package.json sans import JSON (cross-runtime safe)
49
+ const { readFileSync } = await import('node:fs');
50
+ const { fileURLToPath } = await import('node:url');
51
+ const { dirname, join } = await import('node:path');
52
+ const here = dirname(fileURLToPath(import.meta.url));
53
+ const pkgPath = join(here, '..', 'package.json');
54
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
55
+ console.log(pkg.version);
56
+ break;
57
+ }
58
+ case 'help':
59
+ case '--help':
60
+ case '-h':
61
+ default:
62
+ help();
63
+ break;
64
+ }
65
+ }
66
+ main().catch((e) => {
67
+ console.error('[qrpanel] error:', e.message);
68
+ process.exit(1);
69
+ });
70
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,yBAAyB;AACzB,0CAA0C;AAC1C,EAAE;AACF,0BAA0B;AAC1B,uEAAuE;AACvE,qDAAqD;AACrD,6CAA6C;AAE7C,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,aAAa,CAAA;AAC5D,OAAO,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEhD,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AAClC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,MAAM,CAAA;AAE7B,SAAS,IAAI;IACX,OAAO,CAAC,GAAG,CAAC;;;;;;;;;;;;CAYb,CAAC,CAAA;AACF,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,QAAQ,GAAG,EAAE,CAAC;QACZ,KAAK,MAAM,CAAC,CAAC,CAAC;YACZ,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,GAAG,EAAE,CAAA;YACpC,MAAM,IAAI,GAAG,cAAc,CAAC,GAAG,CAAC,CAAA;YAChC,OAAO,CAAC,GAAG,CAAC,8BAA8B,IAAI,EAAE,CAAC,CAAA;YACjD,OAAO,CAAC,GAAG,CAAC,wFAAwF,CAAC,CAAA;YACrG,MAAK;QACP,CAAC;QACD,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,OAAO,CAAC,GAAG,CAAC,oBAAoB,UAAU,EAAE,CAAC,MAAM,IAAI,CAAC,CAAA;YACxD,KAAK,MAAM,GAAG,IAAI,UAAU,EAAE,EAAE,CAAC;gBAC/B,OAAO,CAAC,GAAG,CAAC,OAAO,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAA;YAC7D,CAAC;YACD,OAAO,CAAC,GAAG,CAAC,qBAAqB,CAAC,CAAA;YAClC,OAAO,CAAC,GAAG,CAAC,iBAAiB,cAAc,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;YAC3E,MAAK;QACP,CAAC;QACD,KAAK,WAAW,CAAC;QACjB,KAAK,IAAI,CAAC,CAAC,CAAC;YACV,+EAA+E;YAC/E,MAAM,EAAE,YAAY,EAAE,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAA;YAChD,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,MAAM,CAAC,UAAU,CAAC,CAAA;YAClD,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,CAAA;YACnD,MAAM,IAAI,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;YACpD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,cAAc,CAAC,CAAA;YAChD,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,OAAO,EAAE,OAAO,CAAC,CAAwB,CAAA;YAC7E,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YACxB,MAAK;QACP,CAAC;QACD,KAAK,MAAM,CAAC;QACZ,KAAK,QAAQ,CAAC;QACd,KAAK,IAAI,CAAC;QACV;YACE,IAAI,EAAE,CAAA;YACN,MAAK;IACT,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE;IACjB,OAAO,CAAC,KAAK,CAAC,kBAAkB,EAAG,CAAW,CAAC,OAAO,CAAC,CAAA;IACvD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AACjB,CAAC,CAAC,CAAA"}
@@ -0,0 +1,43 @@
1
+ import { type ThemeKey, type ThemeAsset } from './themes.js';
2
+ import type { QrConfigDefaults } from './config.js';
3
+ export interface ComposeOptions {
4
+ width: number;
5
+ errorCorrectionLevel: 'L' | 'M' | 'Q' | 'H';
6
+ darkColor: string;
7
+ lightColor: string;
8
+ theme: ThemeKey | 'random' | 'none' | {
9
+ svg: string;
10
+ label?: string;
11
+ };
12
+ themePool: ThemeKey[];
13
+ framePadding: number;
14
+ centerWhiteRatio: number;
15
+ themeOpacity: number;
16
+ themeColor: string;
17
+ }
18
+ /**
19
+ * Résout la prop `theme` en thème concret.
20
+ * - 'random' → tire dans themePool
21
+ * - 'none' → null (caller doit fallback sur QR pur)
22
+ * - { svg } → thème inline custom
23
+ * - clé → thème natif via registry
24
+ */
25
+ export declare function resolveTheme(theme: ComposeOptions['theme'], pool: ThemeKey[]): ThemeAsset | null;
26
+ /**
27
+ * Génère le SVG composite (theme frame + center white card + QR).
28
+ * Si theme='none', retourne null — le caller doit fallback sur le QR pur.
29
+ */
30
+ export declare function composeThemedSvg(text: string, opts: ComposeOptions): Promise<string | null>;
31
+ /**
32
+ * Génère le PNG composite via @resvg/resvg-js.
33
+ * Si theme='none', retourne null — le caller doit fallback.
34
+ */
35
+ export declare function composeThemedPng(text: string, opts: ComposeOptions): Promise<Buffer | null>;
36
+ /**
37
+ * Génère un Data URL PNG du composite.
38
+ * Si theme='none', retourne null — le caller doit fallback.
39
+ */
40
+ export declare function composeThemedDataUrl(text: string, opts: ComposeOptions): Promise<string | null>;
41
+ /** Helper de merge config + opts pour les fonctions de génération. */
42
+ export declare function mergeComposeOpts(cfg: QrConfigDefaults, opts?: Partial<ComposeOptions>): ComposeOptions;
43
+ //# sourceMappingURL=composer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composer.d.ts","sourceRoot":"","sources":["../src/composer.ts"],"names":[],"mappings":"AAcA,OAAO,EACL,KAAK,QAAQ,EAAE,KAAK,UAAU,EAE/B,MAAM,aAAa,CAAA;AACpB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAEnD,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,oBAAoB,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;IAC3C,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;IACrE,SAAS,EAAE,QAAQ,EAAE,CAAA;IACrB,YAAY,EAAE,MAAM,CAAA;IACpB,gBAAgB,EAAE,MAAM,CAAA;IACxB,YAAY,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,MAAM,CAAA;CACnB;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,cAAc,CAAC,OAAO,CAAC,EAC9B,IAAI,EAAE,QAAQ,EAAE,GACf,UAAU,GAAG,IAAI,CAOnB;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAqDxB;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAUxB;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAIxB;AAED,sEAAsE;AACtE,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,gBAAgB,EACrB,IAAI,GAAE,OAAO,CAAC,cAAc,CAAM,GACjC,cAAc,CAahB"}
@@ -0,0 +1,123 @@
1
+ // @mostajs/qrpanel/composer — themed composite generator
2
+ // Author: Dr Hamid MADANI <drmdh@msn.com>
3
+ //
4
+ // Compose un SVG global :
5
+ // 1. background blanc full canvas
6
+ // 2. cadre thématique (4 motifs aux coins) via themes.buildThemeFrameSvg
7
+ // 3. cartouche blanc central (rect)
8
+ // 4. QR centré dans le cartouche, ECC élevé recommandé
9
+ //
10
+ // Le PNG composite est obtenu en rasterisant le SVG via @resvg/resvg-js
11
+ // (rust prebuilt-binaries cross-OS, no chromium, no node-gyp).
12
+ import QRCode from 'qrcode';
13
+ import { Resvg } from '@resvg/resvg-js';
14
+ import { getTheme, pickRandomTheme, buildThemeFrameSvg, } from './themes.js';
15
+ /**
16
+ * Résout la prop `theme` en thème concret.
17
+ * - 'random' → tire dans themePool
18
+ * - 'none' → null (caller doit fallback sur QR pur)
19
+ * - { svg } → thème inline custom
20
+ * - clé → thème natif via registry
21
+ */
22
+ export function resolveTheme(theme, pool) {
23
+ if (theme === 'none')
24
+ return null;
25
+ if (typeof theme === 'object' && theme && 'svg' in theme) {
26
+ return { key: 'custom', label: theme.label ?? 'Custom', motif: theme.svg };
27
+ }
28
+ const key = theme === 'random' ? pickRandomTheme(pool) : theme;
29
+ return getTheme(key);
30
+ }
31
+ /**
32
+ * Génère le SVG composite (theme frame + center white card + QR).
33
+ * Si theme='none', retourne null — le caller doit fallback sur le QR pur.
34
+ */
35
+ export async function composeThemedSvg(text, opts) {
36
+ const themeAsset = resolveTheme(opts.theme, opts.themePool);
37
+ if (!themeAsset)
38
+ return null;
39
+ const w = opts.width;
40
+ // 1. QR SVG inline — margin=0, on lui donne sa propre marge via le cartouche
41
+ const qrRawSvg = await QRCode.toString(text, {
42
+ type: 'svg',
43
+ margin: 0,
44
+ errorCorrectionLevel: opts.errorCorrectionLevel,
45
+ color: { dark: opts.darkColor, light: opts.lightColor },
46
+ });
47
+ // 2. Extrait le viewBox et le contenu interne du QR SVG
48
+ const vbMatch = qrRawSvg.match(/viewBox="([^"]+)"/);
49
+ const qrViewBox = vbMatch?.[1] ?? '0 0 21 21';
50
+ const qrInner = qrRawSvg
51
+ .replace(/<\?xml[^>]*\?>\s*/, '')
52
+ .replace(/<svg[^>]*>/, '')
53
+ .replace(/<\/svg>\s*$/, '');
54
+ // 3. Géométrie : cartouche blanc central + QR à l'intérieur (avec
55
+ // une marge de respiration entre le QR et le bord du cartouche)
56
+ const centerSize = w * opts.centerWhiteRatio;
57
+ const centerX = (w - centerSize) / 2;
58
+ const qrInsideMargin = centerSize * 0.06; // 6% de la taille cartouche
59
+ const qrSize = centerSize - 2 * qrInsideMargin;
60
+ const qrX = centerX + qrInsideMargin;
61
+ const qrY = centerX + qrInsideMargin;
62
+ // 4. Cadre thématique (4 coins) — scale et positions auto-calculés
63
+ // depuis centerWhiteRatio (zone marge entre cartouche et bord).
64
+ const frame = buildThemeFrameSvg(themeAsset, {
65
+ width: w,
66
+ color: opts.themeColor,
67
+ opacity: opts.themeOpacity,
68
+ framePadding: opts.framePadding,
69
+ centerWhiteRatio: opts.centerWhiteRatio,
70
+ });
71
+ // 5. SVG composite final
72
+ const svg = `<?xml version="1.0" encoding="UTF-8"?>
73
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${w} ${w}" width="${w}" height="${w}">
74
+ <rect width="${w}" height="${w}" fill="${opts.lightColor}"/>
75
+ ${frame}
76
+ <rect x="${centerX}" y="${centerX}" width="${centerSize}" height="${centerSize}" fill="${opts.lightColor}"/>
77
+ <svg x="${qrX}" y="${qrY}" width="${qrSize}" height="${qrSize}" viewBox="${qrViewBox}" shape-rendering="crispEdges">
78
+ ${qrInner}
79
+ </svg>
80
+ </svg>`;
81
+ return svg;
82
+ }
83
+ /**
84
+ * Génère le PNG composite via @resvg/resvg-js.
85
+ * Si theme='none', retourne null — le caller doit fallback.
86
+ */
87
+ export async function composeThemedPng(text, opts) {
88
+ const svg = await composeThemedSvg(text, opts);
89
+ if (!svg)
90
+ return null;
91
+ const resvg = new Resvg(svg, {
92
+ fitTo: { mode: 'width', value: opts.width },
93
+ background: opts.lightColor,
94
+ });
95
+ const rendered = resvg.render();
96
+ return Buffer.from(rendered.asPng());
97
+ }
98
+ /**
99
+ * Génère un Data URL PNG du composite.
100
+ * Si theme='none', retourne null — le caller doit fallback.
101
+ */
102
+ export async function composeThemedDataUrl(text, opts) {
103
+ const png = await composeThemedPng(text, opts);
104
+ if (!png)
105
+ return null;
106
+ return `data:image/png;base64,${png.toString('base64')}`;
107
+ }
108
+ /** Helper de merge config + opts pour les fonctions de génération. */
109
+ export function mergeComposeOpts(cfg, opts = {}) {
110
+ return {
111
+ width: opts.width ?? cfg.width,
112
+ errorCorrectionLevel: opts.errorCorrectionLevel ?? cfg.errorCorrectionLevel,
113
+ darkColor: opts.darkColor ?? cfg.darkColor,
114
+ lightColor: opts.lightColor ?? cfg.lightColor,
115
+ theme: opts.theme ?? cfg.theme,
116
+ themePool: opts.themePool ?? cfg.themePool,
117
+ framePadding: opts.framePadding ?? cfg.framePadding,
118
+ centerWhiteRatio: opts.centerWhiteRatio ?? cfg.centerWhiteRatio,
119
+ themeOpacity: opts.themeOpacity ?? cfg.themeOpacity,
120
+ themeColor: opts.themeColor ?? cfg.themeColor,
121
+ };
122
+ }
123
+ //# sourceMappingURL=composer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"composer.js","sourceRoot":"","sources":["../src/composer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ import type { ThemeKey } from './themes.js';
2
+ export type QrFormat = 'svg' | 'png' | 'dataUrl';
3
+ export type QrEcc = 'L' | 'M' | 'Q' | 'H';
4
+ /** Valeurs par défaut éditables dans .qrconfig.json. */
5
+ export interface QrConfigDefaults {
6
+ /** Master toggle — false = QR pur (legacy), bypass tout le pipeline thématique. */
7
+ genimage: boolean;
8
+ /** Format préféré quand l'app n'en spécifie pas un. */
9
+ format: QrFormat;
10
+ /** Largeur/hauteur du canvas SVG/PNG en pixels. */
11
+ width: number;
12
+ /** Marge blanche autour du QR (en modules). */
13
+ margin: number;
14
+ /** Niveau de correction d'erreur. 'H' recommandé pour composite (ECC=30%). */
15
+ errorCorrectionLevel: QrEcc;
16
+ /** Couleur des modules sombres du QR. */
17
+ darkColor: string;
18
+ /** Couleur du fond (cartouche central). */
19
+ lightColor: string;
20
+ /** 'random' = tirage dans themePool, ou clé thème, ou 'none' (= image off ponctuel). */
21
+ theme: ThemeKey | 'random' | 'none';
22
+ /** Sous-set des thèmes utilisés quand theme='random'. */
23
+ themePool: ThemeKey[];
24
+ /**
25
+ * Position du motif dans la zone-marge (0..1) :
26
+ * 0 = collé au bord
27
+ * 0.5 = centre de la marge (optimal — default)
28
+ * 1 = collé contre le cartouche
29
+ */
30
+ framePadding: number;
31
+ /** Taille du cartouche blanc central (proportion du canvas, 0..1). */
32
+ centerWhiteRatio: number;
33
+ /** Opacité du cadre image (0..1). */
34
+ themeOpacity: number;
35
+ /** Couleur monochrome du cadre image (CSS color). */
36
+ themeColor: string;
37
+ }
38
+ export interface QrConfig {
39
+ default: QrConfigDefaults;
40
+ /** Thèmes custom — clé arbitraire, override ou ajout. */
41
+ customThemes?: Record<string, {
42
+ svg: string;
43
+ label?: string;
44
+ }>;
45
+ }
46
+ export declare const DEFAULT_CONFIG: QrConfig;
47
+ /**
48
+ * Lit la config depuis `cwd` (default `process.cwd()`).
49
+ * Retourne `DEFAULT_CONFIG` si aucun fichier trouvé.
50
+ * Cache invalidé par mtime du fichier.
51
+ */
52
+ export declare function loadQrConfig(cwd?: string): QrConfig;
53
+ /**
54
+ * Crée `.qrconfig.json` à `cwd` s'il n'existe pas. Idempotent.
55
+ * Retourne le path écrit (ou path existant si déjà là).
56
+ *
57
+ * @param overrides — valeurs par défaut à patcher dans le fichier généré.
58
+ */
59
+ export declare function ensureQrConfig(cwd?: string, overrides?: Partial<QrConfigDefaults>): string;
60
+ /** Vide le cache mémoire (utile pour les tests). */
61
+ export declare function clearConfigCache(): void;
62
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAA;AAI3C,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,KAAK,GAAG,SAAS,CAAA;AAChD,MAAM,MAAM,KAAK,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAA;AAEzC,wDAAwD;AACxD,MAAM,WAAW,gBAAgB;IAC/B,mFAAmF;IACnF,QAAQ,EAAE,OAAO,CAAA;IACjB,uDAAuD;IACvD,MAAM,EAAE,QAAQ,CAAA;IAChB,mDAAmD;IACnD,KAAK,EAAE,MAAM,CAAA;IACb,+CAA+C;IAC/C,MAAM,EAAE,MAAM,CAAA;IACd,8EAA8E;IAC9E,oBAAoB,EAAE,KAAK,CAAA;IAC3B,yCAAyC;IACzC,SAAS,EAAE,MAAM,CAAA;IACjB,2CAA2C;IAC3C,UAAU,EAAE,MAAM,CAAA;IAClB,wFAAwF;IACxF,KAAK,EAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAA;IACnC,yDAAyD;IACzD,SAAS,EAAE,QAAQ,EAAE,CAAA;IACrB;;;;;OAKG;IACH,YAAY,EAAE,MAAM,CAAA;IACpB,sEAAsE;IACtE,gBAAgB,EAAE,MAAM,CAAA;IACxB,qCAAqC;IACrC,YAAY,EAAE,MAAM,CAAA;IACpB,qDAAqD;IACrD,UAAU,EAAE,MAAM,CAAA;CACnB;AAED,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,gBAAgB,CAAA;IACzB,yDAAyD;IACzD,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;CAC/D;AAID,eAAO,MAAM,cAAc,EAAE,QAmB5B,CAAA;AAyBD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAsB,GAAG,QAAQ,CAgClE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,GAAG,GAAE,MAAsB,EAC3B,SAAS,GAAE,OAAO,CAAC,gBAAgB,CAAM,GACxC,MAAM,CAoBR;AAED,oDAAoD;AACpD,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC"}