@mostajs/qrpanel 0.5.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.
- package/CHANGELOG.md +12 -0
- package/browser/qr-scan.js +10144 -0
- package/dist/qr-svg.js +165 -0
- package/llms.txt +20 -103
- package/package.json +28 -43
- package/src/cli.ts +74 -0
- package/src/client.tsx +184 -0
- package/src/composer.ts +164 -0
- package/src/config.ts +213 -0
- package/src/index.js +6 -0
- package/src/index.ts +16 -0
- package/src/qr-decode.js +23 -0
- package/src/qr-figures.js +46 -0
- package/src/qr-image.tsx +105 -0
- package/src/qr-payloads.js +59 -0
- package/src/qr-png.js +40 -0
- package/src/qr-svg.js +185 -0
- package/src/server.ts +212 -0
- package/src/themes.ts +266 -0
package/src/themes.ts
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// @mostajs/qrpanel/themes — built-in theme registry
|
|
2
|
+
// Author: Dr Hamid MADANI <drmdh@msn.com>
|
|
3
|
+
//
|
|
4
|
+
// Chaque thème = un petit motif SVG répliqué aux 4 coins du composite,
|
|
5
|
+
// laissant le centre libre pour le cartouche blanc + QR. Style ligne
|
|
6
|
+
// minimaliste mono-color via `currentColor` (le composer pilote la
|
|
7
|
+
// couleur via CSS sur le <g> wrapper).
|
|
8
|
+
//
|
|
9
|
+
// Création maison — design original, libre de droits.
|
|
10
|
+
// viewBox local du motif : centré sur l'origine, ~14 unités d'envergure.
|
|
11
|
+
|
|
12
|
+
export type ThemeKey =
|
|
13
|
+
| 'baby'
|
|
14
|
+
| 'animals'
|
|
15
|
+
| 'science'
|
|
16
|
+
| 'physics'
|
|
17
|
+
| 'chemistry'
|
|
18
|
+
| 'math'
|
|
19
|
+
| 'nature'
|
|
20
|
+
| 'tech'
|
|
21
|
+
| 'space'
|
|
22
|
+
| 'music'
|
|
23
|
+
| 'book'
|
|
24
|
+
| 'health'
|
|
25
|
+
|
|
26
|
+
export interface ThemeAsset {
|
|
27
|
+
key: ThemeKey
|
|
28
|
+
label: string
|
|
29
|
+
/** Fragment SVG (sans <svg> wrapper) — un motif centré sur (0,0). */
|
|
30
|
+
motif: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Communs à tous les motifs (style ligne fine et propre). */
|
|
34
|
+
const STROKE = 'stroke="currentColor" stroke-width="2.4" fill="none" stroke-linecap="round" stroke-linejoin="round"'
|
|
35
|
+
const FILL = 'fill="currentColor"'
|
|
36
|
+
|
|
37
|
+
export const THEMES: Record<ThemeKey, ThemeAsset> = {
|
|
38
|
+
// ─── 1. baby — sucette + cœur ─────────────────────────────────
|
|
39
|
+
baby: {
|
|
40
|
+
key: 'baby', label: 'Bébé',
|
|
41
|
+
motif: `
|
|
42
|
+
<circle cx="-2" cy="-3" r="4.5" ${STROKE}/>
|
|
43
|
+
<line x1="-2" y1="1.5" x2="-2" y2="6" ${STROKE}/>
|
|
44
|
+
<path d="M 5 -2 c 0.8 -1.2 2.5 -1.2 2.5 0.4 c 0 1.4 -2.5 2.8 -2.5 2.8 c 0 0 -2.5 -1.4 -2.5 -2.8 c 0 -1.6 1.7 -1.6 2.5 -0.4 z" ${FILL} opacity="0.9"/>
|
|
45
|
+
`,
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// ─── 2. animals — empreinte de patte (4 doigts + pad) ─────────
|
|
49
|
+
animals: {
|
|
50
|
+
key: 'animals', label: 'Animaux',
|
|
51
|
+
motif: `
|
|
52
|
+
<ellipse cx="-3.5" cy="-3" rx="1.3" ry="1.8" ${FILL}/>
|
|
53
|
+
<ellipse cx="-1" cy="-5" rx="1.2" ry="1.7" ${FILL}/>
|
|
54
|
+
<ellipse cx="1.8" cy="-5" rx="1.2" ry="1.7" ${FILL}/>
|
|
55
|
+
<ellipse cx="4.3" cy="-3" rx="1.3" ry="1.8" ${FILL}/>
|
|
56
|
+
<ellipse cx="0.5" cy="2" rx="3.5" ry="3.2" ${FILL}/>
|
|
57
|
+
`,
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
// ─── 3. science — éprouvette + bulles ─────────────────────────
|
|
61
|
+
science: {
|
|
62
|
+
key: 'science', label: 'Science',
|
|
63
|
+
motif: `
|
|
64
|
+
<path d="M -2 -6 L -2 4 a 2.5 2.5 0 0 0 5 0 L 3 -6" ${STROKE}/>
|
|
65
|
+
<line x1="-3" y1="-6" x2="4" y2="-6" ${STROKE}/>
|
|
66
|
+
<circle cx="-0.5" cy="2" r="0.7" ${FILL}/>
|
|
67
|
+
<circle cx="1.4" cy="0" r="0.5" ${FILL}/>
|
|
68
|
+
<circle cx="0.4" cy="-2" r="0.4" ${FILL}/>
|
|
69
|
+
`,
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
// ─── 4. physics — atome (noyau + 2 orbites croisées) ──────────
|
|
73
|
+
physics: {
|
|
74
|
+
key: 'physics', label: 'Physique',
|
|
75
|
+
motif: `
|
|
76
|
+
<ellipse cx="0" cy="0" rx="6" ry="2.4" ${STROKE}/>
|
|
77
|
+
<ellipse cx="0" cy="0" rx="6" ry="2.4" transform="rotate(60)" ${STROKE}/>
|
|
78
|
+
<ellipse cx="0" cy="0" rx="6" ry="2.4" transform="rotate(-60)" ${STROKE}/>
|
|
79
|
+
<circle cx="0" cy="0" r="1.4" ${FILL}/>
|
|
80
|
+
`,
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// ─── 5. chemistry — bécher avec graduation et liquide ─────────
|
|
84
|
+
chemistry: {
|
|
85
|
+
key: 'chemistry', label: 'Chimie',
|
|
86
|
+
motif: `
|
|
87
|
+
<path d="M -3.5 -5 L -3.5 4 a 1.5 1.5 0 0 0 1.5 1.5 L 2 5.5 a 1.5 1.5 0 0 0 1.5 -1.5 L 3.5 -5" ${STROKE}/>
|
|
88
|
+
<line x1="-4" y1="-5" x2="4" y2="-5" ${STROKE}/>
|
|
89
|
+
<line x1="-2.7" y1="-2.5" x2="-1.4" y2="-2.5" ${STROKE}/>
|
|
90
|
+
<line x1="-2.7" y1="0" x2="-1.4" y2="0" ${STROKE}/>
|
|
91
|
+
<path d="M -3.5 1.5 L 3.5 1.5 L 3.5 4 a 1.5 1.5 0 0 1 -1.5 1.5 L -2 5.5 a 1.5 1.5 0 0 1 -1.5 -1.5 z" ${FILL} opacity="0.35"/>
|
|
92
|
+
`,
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
// ─── 6. math — symbole π et signe = ───────────────────────────
|
|
96
|
+
math: {
|
|
97
|
+
key: 'math', label: 'Mathématique',
|
|
98
|
+
motif: `
|
|
99
|
+
<line x1="-5" y1="-3.5" x2="5" y2="-3.5" ${STROKE}/>
|
|
100
|
+
<line x1="-2.5" y1="-3.5" x2="-2.5" y2="3.5" ${STROKE}/>
|
|
101
|
+
<line x1="2.5" y1="-3.5" x2="2.5" y2="3.5" ${STROKE}/>
|
|
102
|
+
<line x1="-5" y1="5.5" x2="5" y2="5.5" ${STROKE}/>
|
|
103
|
+
<line x1="-5" y1="7.2" x2="5" y2="7.2" ${STROKE}/>
|
|
104
|
+
`,
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
// ─── 7. nature — feuille + tige ───────────────────────────────
|
|
108
|
+
nature: {
|
|
109
|
+
key: 'nature', label: 'Nature',
|
|
110
|
+
motif: `
|
|
111
|
+
<path d="M 0 6 C -6 4 -6 -4 0 -6 C 6 -4 6 4 0 6 Z" ${STROKE}/>
|
|
112
|
+
<line x1="0" y1="-6" x2="0" y2="6" ${STROKE}/>
|
|
113
|
+
<line x1="0" y1="-3" x2="-3" y2="-1" ${STROKE}/>
|
|
114
|
+
<line x1="0" y1="0" x2="-3.5" y2="2" ${STROKE}/>
|
|
115
|
+
<line x1="0" y1="-3" x2="3" y2="-1" ${STROKE}/>
|
|
116
|
+
<line x1="0" y1="0" x2="3.5" y2="2" ${STROKE}/>
|
|
117
|
+
`,
|
|
118
|
+
},
|
|
119
|
+
|
|
120
|
+
// ─── 8. tech — engrenage 8 dents ──────────────────────────────
|
|
121
|
+
tech: {
|
|
122
|
+
key: 'tech', label: 'Tech',
|
|
123
|
+
motif: `
|
|
124
|
+
<path d="
|
|
125
|
+
M -1 -5.6 L 1 -5.6 L 1.4 -4 L 3 -3.4 L 4.2 -4.5 L 5.5 -3.2 L 4.4 -2 L 5 -0.4 L 6.6 0 L 6.6 2
|
|
126
|
+
L 5 2.4 L 4.4 4 L 5.5 5.2 L 4.2 6.5 L 3 5.4 L 1.4 6 L 1 7.6 L -1 7.6 L -1.4 6 L -3 5.4
|
|
127
|
+
L -4.2 6.5 L -5.5 5.2 L -4.4 4 L -5 2.4 L -6.6 2 L -6.6 0 L -5 -0.4 L -4.4 -2 L -5.5 -3.2
|
|
128
|
+
L -4.2 -4.5 L -3 -3.4 L -1.4 -4 Z" transform="translate(0 -1) scale(0.7)" ${STROKE}/>
|
|
129
|
+
<circle cx="0" cy="-1" r="1.5" transform="scale(0.7)" ${STROKE}/>
|
|
130
|
+
`,
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
// ─── 9. space — planète saturne + étoile ──────────────────────
|
|
134
|
+
space: {
|
|
135
|
+
key: 'space', label: 'Espace',
|
|
136
|
+
motif: `
|
|
137
|
+
<circle cx="-1" cy="0" r="3.2" ${STROKE}/>
|
|
138
|
+
<ellipse cx="-1" cy="0" rx="6" ry="1.4" transform="rotate(-20 -1 0)" ${STROKE}/>
|
|
139
|
+
<path d="M 5 -5 L 5.6 -3.5 L 7.2 -3.5 L 6 -2.6 L 6.5 -1.1 L 5 -2 L 3.5 -1.1 L 4 -2.6 L 2.8 -3.5 L 4.4 -3.5 Z" ${FILL}/>
|
|
140
|
+
`,
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
// ─── 10. music — note + portée ────────────────────────────────
|
|
144
|
+
music: {
|
|
145
|
+
key: 'music', label: 'Musique',
|
|
146
|
+
motif: `
|
|
147
|
+
<ellipse cx="-2" cy="4" rx="2.2" ry="1.7" transform="rotate(-22 -2 4)" ${FILL}/>
|
|
148
|
+
<line x1="0" y1="3" x2="0" y2="-6" ${STROKE}/>
|
|
149
|
+
<path d="M 0 -6 C 3 -5 4 -2 1.5 -1" ${STROKE}/>
|
|
150
|
+
<ellipse cx="3.5" cy="2.5" rx="1.7" ry="1.3" transform="rotate(-22 3.5 2.5)" ${FILL} opacity="0.6"/>
|
|
151
|
+
<line x1="5" y1="1.5" x2="5" y2="-4" ${STROKE} opacity="0.6"/>
|
|
152
|
+
`,
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
// ─── 11. book — livre ouvert ──────────────────────────────────
|
|
156
|
+
book: {
|
|
157
|
+
key: 'book', label: 'Livre',
|
|
158
|
+
motif: `
|
|
159
|
+
<path d="M -6 -4 L 0 -3 L 6 -4 L 6 4 L 0 5 L -6 4 Z" ${STROKE}/>
|
|
160
|
+
<line x1="0" y1="-3" x2="0" y2="5" ${STROKE}/>
|
|
161
|
+
<line x1="-4.5" y1="-2" x2="-1.5" y2="-1.5" ${STROKE} opacity="0.6"/>
|
|
162
|
+
<line x1="-4.5" y1="0" x2="-1.5" y2="0.5" ${STROKE} opacity="0.6"/>
|
|
163
|
+
<line x1="1.5" y1="-1.5" x2="4.5" y2="-2" ${STROKE} opacity="0.6"/>
|
|
164
|
+
<line x1="1.5" y1="0.5" x2="4.5" y2="0" ${STROKE} opacity="0.6"/>
|
|
165
|
+
`,
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
// ─── 12. health — croix médicale + battement ──────────────────
|
|
169
|
+
health: {
|
|
170
|
+
key: 'health', label: 'Santé',
|
|
171
|
+
motif: `
|
|
172
|
+
<rect x="-1.8" y="-6" width="3.6" height="12" rx="0.8" ${FILL}/>
|
|
173
|
+
<rect x="-6" y="-1.8" width="12" height="3.6" rx="0.8" ${FILL}/>
|
|
174
|
+
<path d="M -8 7 L -5 7 L -3.5 4 L -1.5 9 L 0 6 L 2 8 L 4 7 L 8 7" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round" transform="translate(0 2)"/>
|
|
175
|
+
`,
|
|
176
|
+
},
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Liste des clés thèmes natifs (ordre fixe pour itération déterministe). */
|
|
180
|
+
export const THEME_KEYS: ThemeKey[] = [
|
|
181
|
+
'baby', 'animals', 'science', 'physics', 'chemistry', 'math',
|
|
182
|
+
'nature', 'tech', 'space', 'music', 'book', 'health',
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
/** Renvoie la liste des clés thèmes natifs. */
|
|
186
|
+
export function listThemes(): ThemeKey[] {
|
|
187
|
+
return [...THEME_KEYS]
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Récupère un thème par clé. Lance si inconnu. */
|
|
191
|
+
export function getTheme(key: ThemeKey): ThemeAsset {
|
|
192
|
+
const theme = THEMES[key]
|
|
193
|
+
if (!theme) throw new Error(`[qrpanel] unknown theme: "${key}". Available: ${THEME_KEYS.join(', ')}`)
|
|
194
|
+
return theme
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Tire un thème au hasard dans le pool fourni (ou tous les thèmes si pool absent).
|
|
199
|
+
* Utilise Math.random() — pas de seed (déterminisme = à la charge du caller s'il
|
|
200
|
+
* en a besoin).
|
|
201
|
+
*/
|
|
202
|
+
export function pickRandomTheme(pool?: ThemeKey[]): ThemeKey {
|
|
203
|
+
const candidates = pool && pool.length > 0 ? pool : THEME_KEYS
|
|
204
|
+
return candidates[Math.floor(Math.random() * candidates.length)]!
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Construit le fragment SVG du cadre thématique :
|
|
209
|
+
* 4 instances du motif aux 4 coins, monochrome via CSS color, opacité.
|
|
210
|
+
*
|
|
211
|
+
* Le fragment est destiné à être inséré dans un SVG composite global
|
|
212
|
+
* (viewBox 0 0 width width). Coordonnées dans l'espace [0..width].
|
|
213
|
+
*/
|
|
214
|
+
export interface BuildFrameOpts {
|
|
215
|
+
/** Largeur du canvas SVG englobant en unités utilisateur. */
|
|
216
|
+
width: number
|
|
217
|
+
/** Couleur monochrome appliquée via CSS. Default '#1e293b'. */
|
|
218
|
+
color?: string
|
|
219
|
+
/** Opacité 0..1. Default 1. */
|
|
220
|
+
opacity?: number
|
|
221
|
+
/**
|
|
222
|
+
* Proportion du cartouche blanc central / canvas (0..1). Détermine la
|
|
223
|
+
* largeur de la zone-marge où sont placés les motifs.
|
|
224
|
+
* Default 0.62.
|
|
225
|
+
*/
|
|
226
|
+
centerWhiteRatio?: number
|
|
227
|
+
/**
|
|
228
|
+
* Position du motif dans la zone-marge (0..1) :
|
|
229
|
+
* 0 = motif collé au bord du canvas
|
|
230
|
+
* 0.5 = motif centré dans la zone-marge (default, optimal)
|
|
231
|
+
* 1 = motif collé contre le cartouche blanc
|
|
232
|
+
*/
|
|
233
|
+
framePadding?: number
|
|
234
|
+
/**
|
|
235
|
+
* Échelle du motif (override de l'auto-calc).
|
|
236
|
+
* Auto = (marginZone × 0.65 / 14) où 14 ≈ envergure locale du motif.
|
|
237
|
+
*/
|
|
238
|
+
motifScale?: number
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Envergure approximative d'un motif local (en unités SVG). */
|
|
242
|
+
const MOTIF_BASE_SIZE = 14
|
|
243
|
+
|
|
244
|
+
export function buildThemeFrameSvg(theme: ThemeAsset, opts: BuildFrameOpts): string {
|
|
245
|
+
const w = opts.width
|
|
246
|
+
const ratio = opts.centerWhiteRatio ?? 0.62
|
|
247
|
+
const marginZone = (w - w * ratio) / 2 // largeur libre entre cartouche et bord
|
|
248
|
+
const padFactor = opts.framePadding ?? 0.5
|
|
249
|
+
const offset = marginZone * padFactor // distance du bord au centre du motif
|
|
250
|
+
const scale = opts.motifScale ?? (marginZone * 0.65 / MOTIF_BASE_SIZE)
|
|
251
|
+
const color = opts.color ?? '#1e293b'
|
|
252
|
+
const opacity = opts.opacity ?? 1
|
|
253
|
+
|
|
254
|
+
const corners: [number, number][] = [
|
|
255
|
+
[offset, offset], // top-left
|
|
256
|
+
[w - offset, offset], // top-right
|
|
257
|
+
[offset, w - offset], // bottom-left
|
|
258
|
+
[w - offset, w - offset], // bottom-right
|
|
259
|
+
]
|
|
260
|
+
|
|
261
|
+
const groups = corners.map(([cx, cy]) =>
|
|
262
|
+
`<g transform="translate(${cx} ${cy}) scale(${scale})" style="color:${color};opacity:${opacity}">${theme.motif}</g>`
|
|
263
|
+
).join('')
|
|
264
|
+
|
|
265
|
+
return groups
|
|
266
|
+
}
|