panelui-native 0.56.0 → 0.59.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 (32) hide show
  1. package/README.md +21 -10
  2. package/lib/module/components/funnel-chart/index.js +828 -0
  3. package/lib/module/components/funnel-chart/index.js.map +1 -0
  4. package/lib/module/components/progress/index.js +67 -13
  5. package/lib/module/components/progress/index.js.map +1 -1
  6. package/lib/module/components/qr-code/index.js +641 -0
  7. package/lib/module/components/qr-code/index.js.map +1 -0
  8. package/lib/module/components/qr-code/qr-encode.js +475 -0
  9. package/lib/module/components/qr-code/qr-encode.js.map +1 -0
  10. package/lib/module/index.js +2 -0
  11. package/lib/module/index.js.map +1 -1
  12. package/lib/module/utils/chart.js +36 -0
  13. package/lib/module/utils/chart.js.map +1 -1
  14. package/lib/typescript/src/components/funnel-chart/index.d.ts +289 -0
  15. package/lib/typescript/src/components/funnel-chart/index.d.ts.map +1 -0
  16. package/lib/typescript/src/components/progress/index.d.ts +9 -1
  17. package/lib/typescript/src/components/progress/index.d.ts.map +1 -1
  18. package/lib/typescript/src/components/qr-code/index.d.ts +221 -0
  19. package/lib/typescript/src/components/qr-code/index.d.ts.map +1 -0
  20. package/lib/typescript/src/components/qr-code/qr-encode.d.ts +39 -0
  21. package/lib/typescript/src/components/qr-code/qr-encode.d.ts.map +1 -0
  22. package/lib/typescript/src/index.d.ts +2 -0
  23. package/lib/typescript/src/index.d.ts.map +1 -1
  24. package/lib/typescript/src/utils/chart.d.ts +19 -0
  25. package/lib/typescript/src/utils/chart.d.ts.map +1 -1
  26. package/package.json +1 -1
  27. package/src/components/funnel-chart/index.tsx +1121 -0
  28. package/src/components/progress/index.tsx +64 -11
  29. package/src/components/qr-code/index.tsx +663 -0
  30. package/src/components/qr-code/qr-encode.ts +548 -0
  31. package/src/index.ts +35 -0
  32. package/src/utils/chart.ts +52 -0
@@ -0,0 +1,548 @@
1
+ /**
2
+ * A QR encoder, in TypeScript, with no dependencies.
3
+ *
4
+ * Vendored rather than installed for the reason the rest of this library has
5
+ * no runtime dependencies: a component that needs an npm package to draw
6
+ * itself is one the CLI has to install on someone's behalf, and every project
7
+ * that copies the source in inherits it. This is a few hundred lines of
8
+ * arithmetic that has not changed since 2000 — it is cheaper to own.
9
+ *
10
+ * Byte mode only. The alphanumeric and numeric modes pack more into the same
11
+ * version, but they only accept their own alphabets, and the thing people
12
+ * encode is a URL. Byte mode takes anything.
13
+ *
14
+ * The output is a square matrix of booleans, dark first. Everything about how
15
+ * it is drawn — module size, colour, quiet zone, the hole in the middle for a
16
+ * logo — belongs to the component, not here.
17
+ */
18
+
19
+ /** How much of the code can be lost and still read. */
20
+ export type ErrorCorrectionLevel = 'L' | 'M' | 'Q' | 'H';
21
+
22
+ /** Format bits per level. Not the same order as the letters. */
23
+ const FORMAT_BITS: Record<ErrorCorrectionLevel, number> = { L: 1, M: 0, Q: 3, H: 2 };
24
+
25
+ /** Error-correction codewords per block, indexed [level][version]. */
26
+ const ECC_CODEWORDS_PER_BLOCK: Record<ErrorCorrectionLevel, Int16Array> = {
27
+ // Version 0 does not exist; the -1 keeps the index honest.
28
+ L: Int16Array.from([-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]),
29
+ M: Int16Array.from([-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28]),
30
+ Q: Int16Array.from([-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]),
31
+ H: Int16Array.from([-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30]),
32
+ };
33
+
34
+ /** How many blocks the data is split into, indexed [level][version]. */
35
+ const NUM_ERROR_CORRECTION_BLOCKS: Record<ErrorCorrectionLevel, Int16Array> = {
36
+ L: Int16Array.from([-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25]),
37
+ M: Int16Array.from([-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49]),
38
+ Q: Int16Array.from([-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68]),
39
+ H: Int16Array.from([-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81]),
40
+ };
41
+
42
+ const MIN_VERSION = 1;
43
+ const MAX_VERSION = 40;
44
+
45
+ /** Total data-and-error modules in a version, before the format areas. */
46
+ function rawDataModules(version: number): number {
47
+ let modules = (16 * version + 128) * version + 64;
48
+ if (version >= 2) {
49
+ const alignmentCount = Math.floor(version / 7) + 2;
50
+ modules -= (25 * alignmentCount - 10) * alignmentCount - 55;
51
+ if (version >= 7) modules -= 36;
52
+ }
53
+ return modules;
54
+ }
55
+
56
+ /** Codewords available for data, once error correction has taken its share. */
57
+ function dataCodewords(version: number, level: ErrorCorrectionLevel): number {
58
+ return (
59
+ Math.floor(rawDataModules(version) / 8) -
60
+ ECC_CODEWORDS_PER_BLOCK[level]![version]! * NUM_ERROR_CORRECTION_BLOCKS[level]![version]!
61
+ );
62
+ }
63
+
64
+ /** Where the alignment patterns go, as coordinates on both axes. */
65
+ function alignmentPositions(version: number): number[] {
66
+ if (version === 1) return [];
67
+
68
+ const count = Math.floor(version / 7) + 2;
69
+ const size = version * 4 + 17;
70
+ // Version 32 is the one exception to the formula, and it is in the spec as
71
+ // an exception rather than as anything derivable.
72
+ const step = version === 32 ? 26 : Math.ceil((version * 4 + 4) / (count * 2 - 2)) * 2;
73
+
74
+ /*
75
+ * Ascending, and that is load-bearing: the caller skips the three
76
+ * coordinate pairs that would land on a finder by index — first and last —
77
+ * so a list in the other order draws an alignment pattern over the top-left
78
+ * finder and leaves the bottom-right one off.
79
+ *
80
+ * They are walked backwards from the far edge because the spacing is even
81
+ * from that end and the first one is pinned at 6, so the uneven gap falls
82
+ * between the first two.
83
+ */
84
+ const positions: number[] = [];
85
+ for (let pos = size - 7; positions.length < count - 1; pos -= step) positions.unshift(pos);
86
+ positions.unshift(6);
87
+ return positions;
88
+ }
89
+
90
+ /* ------------------------------------------------------------------ *
91
+ * GF(256) arithmetic, for Reed–Solomon.
92
+ * ------------------------------------------------------------------ */
93
+
94
+ /** Multiply in GF(256) with the QR primitive polynomial, x^8 + x^4 + x^3 + x^2 + 1. */
95
+ function multiply(a: number, b: number): number {
96
+ let result = 0;
97
+ for (let i = 7; i >= 0; i--) {
98
+ result = (result << 1) ^ ((result >>> 7) * 0x11d);
99
+ result ^= ((b >>> i) & 1) * a;
100
+ }
101
+ return result & 0xff;
102
+ }
103
+
104
+ /** The divisor polynomial for `degree` error-correction codewords. */
105
+ function generatorPolynomial(degree: number): Uint8Array {
106
+ const result = new Uint8Array(degree);
107
+ result[degree - 1] = 1;
108
+
109
+ // Multiply out (x - r^0)(x - r^1)…(x - r^(degree-1)), keeping the monic
110
+ // leading coefficient implicit.
111
+ let root = 1;
112
+ for (let i = 0; i < degree; i++) {
113
+ for (let j = 0; j < degree; j++) {
114
+ result[j] = multiply(result[j]!, root);
115
+ if (j + 1 < degree) result[j]! ^= result[j + 1]!;
116
+ }
117
+ root = multiply(root, 0x02);
118
+ }
119
+ return result;
120
+ }
121
+
122
+ /** The remainder of `data` divided by the generator — the check codewords. */
123
+ function remainder(data: Uint8Array, divisor: Uint8Array): Uint8Array {
124
+ const result = new Uint8Array(divisor.length);
125
+
126
+ for (const byte of data) {
127
+ const factor = byte ^ result[0]!;
128
+ // Shift left by one and drop the leading term, which the factor consumed.
129
+ result.copyWithin(0, 1);
130
+ result[result.length - 1] = 0;
131
+ for (let i = 0; i < divisor.length; i++) result[i]! ^= multiply(divisor[i]!, factor);
132
+ }
133
+
134
+ return result;
135
+ }
136
+
137
+ /* ------------------------------------------------------------------ *
138
+ * The bit stream.
139
+ * ------------------------------------------------------------------ */
140
+
141
+ class BitBuffer {
142
+ readonly bits: number[] = [];
143
+
144
+ append(value: number, length: number): void {
145
+ for (let i = length - 1; i >= 0; i--) this.bits.push((value >>> i) & 1);
146
+ }
147
+ }
148
+
149
+ /** UTF-8 bytes. QR's byte mode is nominally Latin-1; every reader takes UTF-8. */
150
+ function toUtf8(text: string): number[] {
151
+ const bytes: number[] = [];
152
+
153
+ for (const character of text) {
154
+ const point = character.codePointAt(0) as number;
155
+ if (point < 0x80) {
156
+ bytes.push(point);
157
+ } else if (point < 0x800) {
158
+ bytes.push(0xc0 | (point >> 6), 0x80 | (point & 0x3f));
159
+ } else if (point < 0x10000) {
160
+ bytes.push(0xe0 | (point >> 12), 0x80 | ((point >> 6) & 0x3f), 0x80 | (point & 0x3f));
161
+ } else {
162
+ bytes.push(
163
+ 0xf0 | (point >> 18),
164
+ 0x80 | ((point >> 12) & 0x3f),
165
+ 0x80 | ((point >> 6) & 0x3f),
166
+ 0x80 | (point & 0x3f)
167
+ );
168
+ }
169
+ }
170
+
171
+ return bytes;
172
+ }
173
+
174
+ /** The character-count field is wider once there is more to count. */
175
+ function characterCountBits(version: number): number {
176
+ return version < 10 ? 8 : 16;
177
+ }
178
+
179
+ /** The smallest version this data fits in at this level. */
180
+ function smallestVersion(byteLength: number, level: ErrorCorrectionLevel): number {
181
+ for (let version = MIN_VERSION; version <= MAX_VERSION; version++) {
182
+ const capacity = dataCodewords(version, level) * 8;
183
+ // 4 bits of mode indicator, then the character count, then the data.
184
+ const needed = 4 + characterCountBits(version) + byteLength * 8;
185
+ if (needed <= capacity) return version;
186
+ }
187
+
188
+ throw new Error(
189
+ `Too much data for a QR code: ${byteLength} bytes will not fit at error correction level ${level}.`
190
+ );
191
+ }
192
+
193
+ /** Data codewords, padded and interleaved with their error correction. */
194
+ function codewords(bytes: number[], version: number, level: ErrorCorrectionLevel): Uint8Array {
195
+ const capacity = dataCodewords(version, level) * 8;
196
+ const buffer = new BitBuffer();
197
+
198
+ buffer.append(0b0100, 4); // byte mode
199
+ buffer.append(bytes.length, characterCountBits(version));
200
+ for (const byte of bytes) buffer.append(byte, 8);
201
+
202
+ // Terminator, then up to a byte boundary.
203
+ buffer.append(0, Math.min(4, capacity - buffer.bits.length));
204
+ buffer.append(0, (8 - (buffer.bits.length % 8)) % 8);
205
+
206
+ // Then the two alternating pad codewords, until full.
207
+ for (let pad = 0xec; buffer.bits.length < capacity; pad ^= 0xec ^ 0x11) {
208
+ buffer.append(pad, 8);
209
+ }
210
+
211
+ const data = new Uint8Array(buffer.bits.length / 8);
212
+ for (let i = 0; i < data.length; i++) {
213
+ let byte = 0;
214
+ for (let bit = 0; bit < 8; bit++) byte = (byte << 1) | buffer.bits[i * 8 + bit]!;
215
+ data[i] = byte;
216
+ }
217
+
218
+ const blockCount = NUM_ERROR_CORRECTION_BLOCKS[level]![version]!;
219
+ const eccPerBlock = ECC_CODEWORDS_PER_BLOCK[level]![version]!;
220
+ const totalCodewords = Math.floor(rawDataModules(version) / 8);
221
+ const shortBlockLength = Math.floor(totalCodewords / blockCount);
222
+ // The last few blocks are one codeword longer, which is what makes the
223
+ // interleave below uneven.
224
+ const shortBlockCount = blockCount - (totalCodewords % blockCount);
225
+
226
+ const divisor = generatorPolynomial(eccPerBlock);
227
+ const blocks: Uint8Array[] = [];
228
+ const eccBlocks: Uint8Array[] = [];
229
+
230
+ for (let i = 0, offset = 0; i < blockCount; i++) {
231
+ const length = shortBlockLength - eccPerBlock + (i < shortBlockCount ? 0 : 1);
232
+ const block = data.slice(offset, offset + length);
233
+ offset += length;
234
+ blocks.push(block);
235
+ eccBlocks.push(remainder(block, divisor));
236
+ }
237
+
238
+ // Interleave: one codeword from each block in turn, then the same for the
239
+ // error correction. A burst of damage then falls across every block a little
240
+ // rather than destroying one entirely.
241
+ const result: number[] = [];
242
+
243
+ // The last block is the longest, so its length is the number of rounds.
244
+ for (let i = 0; i < blocks[blocks.length - 1]!.length; i++) {
245
+ for (const block of blocks) if (i < block.length) result.push(block[i]!);
246
+ }
247
+ for (let i = 0; i < eccPerBlock; i++) {
248
+ for (const block of eccBlocks) result.push(block[i]!);
249
+ }
250
+
251
+ return Uint8Array.from(result);
252
+ }
253
+
254
+ /* ------------------------------------------------------------------ *
255
+ * The matrix.
256
+ * ------------------------------------------------------------------ */
257
+
258
+ /** The eight mask patterns, by their index in the spec. */
259
+ const MASKS: ((x: number, y: number) => boolean)[] = [
260
+ (x, y) => (x + y) % 2 === 0,
261
+ (_x, y) => y % 2 === 0,
262
+ (x) => x % 3 === 0,
263
+ (x, y) => (x + y) % 3 === 0,
264
+ (x, y) => (Math.floor(y / 2) + Math.floor(x / 3)) % 2 === 0,
265
+ (x, y) => ((x * y) % 2) + ((x * y) % 3) === 0,
266
+ (x, y) => (((x * y) % 2) + ((x * y) % 3)) % 2 === 0,
267
+ (x, y) => (((x + y) % 2) + ((x * y) % 3)) % 2 === 0,
268
+ ];
269
+
270
+ export interface QrMatrix {
271
+ /** `true` is a dark module. Row-major, `[y][x]`. */
272
+ modules: boolean[][];
273
+ /** Modules per side, quiet zone excluded. */
274
+ size: number;
275
+ /** The version chosen for this data, 1–40. */
276
+ version: number;
277
+ }
278
+
279
+ /**
280
+ * Encode `text` as a QR matrix.
281
+ *
282
+ * `version` is chosen to be the smallest that fits unless one is given. A
283
+ * fixed version is worth asking for when the content changes and the code
284
+ * should not visibly change size with it.
285
+ */
286
+ export function encodeQr(
287
+ text: string,
288
+ {
289
+ errorCorrection = 'M',
290
+ version: requestedVersion,
291
+ }: { errorCorrection?: ErrorCorrectionLevel; version?: number } = {}
292
+ ): QrMatrix {
293
+ const bytes = toUtf8(text);
294
+ const version = requestedVersion ?? smallestVersion(bytes.length, errorCorrection);
295
+
296
+ if (version < MIN_VERSION || version > MAX_VERSION) {
297
+ throw new Error(`QR version must be between ${MIN_VERSION} and ${MAX_VERSION}.`);
298
+ }
299
+ if (4 + characterCountBits(version) + bytes.length * 8 > dataCodewords(version, errorCorrection) * 8) {
300
+ throw new Error(`That data does not fit in QR version ${version} at level ${errorCorrection}.`);
301
+ }
302
+
303
+ const size = version * 4 + 17;
304
+ const modules: boolean[][] = Array.from({ length: size }, () =>
305
+ new Array<boolean>(size).fill(false)
306
+ );
307
+ // Function patterns are not masked and take no data, so they are tracked
308
+ // separately rather than inferred from their position afterwards.
309
+ const reserved: boolean[][] = Array.from({ length: size }, () =>
310
+ new Array<boolean>(size).fill(false)
311
+ );
312
+
313
+ const set = (x: number, y: number, dark: boolean) => {
314
+ modules[y]![x] = dark;
315
+ reserved[y]![x] = true;
316
+ };
317
+
318
+ /* Finder patterns, and the separator around each. */
319
+ for (const [originX, originY] of [
320
+ [0, 0],
321
+ [size - 7, 0],
322
+ [0, size - 7],
323
+ ] as const) {
324
+ for (let dy = -1; dy <= 7; dy++) {
325
+ for (let dx = -1; dx <= 7; dx++) {
326
+ const x = originX + dx;
327
+ const y = originY + dy;
328
+ if (x < 0 || x >= size || y < 0 || y >= size) continue;
329
+ // Distance from the finder's centre, as a square ring index: 0 and 1
330
+ // are the dark core, 2 is the light ring inside it, 3 is the dark
331
+ // border, and 4 is the separator — which is light, and is the reason
332
+ // this walks one module further out than the pattern itself.
333
+ const ring = Math.max(Math.abs(dx - 3), Math.abs(dy - 3));
334
+ set(x, y, ring !== 2 && ring <= 3);
335
+ }
336
+ }
337
+ }
338
+
339
+ /* Timing patterns — the dotted lines joining the finders. */
340
+ for (let i = 0; i < size; i++) {
341
+ if (!reserved[6]![i]) set(i, 6, i % 2 === 0);
342
+ if (!reserved[i]![6]) set(6, i, i % 2 === 0);
343
+ }
344
+
345
+ /* Alignment patterns, everywhere except under a finder. */
346
+ const positions = alignmentPositions(version);
347
+ for (let i = 0; i < positions.length; i++) {
348
+ for (let j = 0; j < positions.length; j++) {
349
+ const corner = (i === 0 && j === 0) || (i === 0 && j === positions.length - 1) ||
350
+ (i === positions.length - 1 && j === 0);
351
+ if (corner) continue;
352
+
353
+ for (let dy = -2; dy <= 2; dy++) {
354
+ for (let dx = -2; dx <= 2; dx++) {
355
+ set(positions[j]! + dx, positions[i]! + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
356
+ }
357
+ }
358
+ }
359
+ }
360
+
361
+ /* Reserve the format areas; the bits go in after masking. */
362
+ for (let i = 0; i < 9; i++) {
363
+ if (!reserved[8]![i]) set(i, 8, false);
364
+ if (!reserved[i]![8]) set(8, i, false);
365
+ }
366
+ for (let i = 0; i < 8; i++) {
367
+ if (!reserved[8]![size - 1 - i]) set(size - 1 - i, 8, false);
368
+ if (!reserved[size - 1 - i]![8]) set(8, size - 1 - i, false);
369
+ }
370
+ // The dark module, which is always dark and is not part of anything.
371
+ set(8, size - 8, true);
372
+
373
+ /* Version information, for 7 and up. */
374
+ if (version >= 7) {
375
+ let rest = version;
376
+ for (let i = 0; i < 12; i++) rest = (rest << 1) ^ ((rest >>> 11) * 0x1f25);
377
+ const bits = ((version << 12) | rest) >>> 0;
378
+
379
+ for (let i = 0; i < 18; i++) {
380
+ const dark = ((bits >>> i) & 1) === 1;
381
+ const a = size - 11 + (i % 3);
382
+ const b = Math.floor(i / 3);
383
+ set(a, b, dark);
384
+ set(b, a, dark);
385
+ }
386
+ }
387
+
388
+ /* The data, walked in two-module columns, upwards then downwards. */
389
+ const data = codewords(bytes, version, errorCorrection);
390
+ let bit = 0;
391
+
392
+ for (let right = size - 1; right >= 1; right -= 2) {
393
+ // Column 6 is the vertical timing pattern; the walk skips over it.
394
+ if (right === 6) right = 5;
395
+
396
+ for (let vertical = 0; vertical < size; vertical++) {
397
+ for (let column = 0; column < 2; column++) {
398
+ const x = right - column;
399
+ const upward = ((right + 1) & 2) === 0;
400
+ const y = upward ? size - 1 - vertical : vertical;
401
+
402
+ if (reserved[y]![x]) continue;
403
+
404
+ // Past the end of the data the remainder bits are zero, which the
405
+ // spec allows and every reader expects.
406
+ modules[y]![x] = bit < data.length * 8 && ((data[bit >>> 3]! >>> (7 - (bit & 7))) & 1) === 1;
407
+ bit++;
408
+ }
409
+ }
410
+ }
411
+
412
+ /* Mask, score, keep the best. */
413
+ let bestMask = 0;
414
+ let bestPenalty = Infinity;
415
+
416
+ for (let mask = 0; mask < 8; mask++) {
417
+ applyMask(modules, reserved, mask, size);
418
+ writeFormat(modules, errorCorrection, mask, size);
419
+ const penalty = penaltyScore(modules, size);
420
+ if (penalty < bestPenalty) {
421
+ bestPenalty = penalty;
422
+ bestMask = mask;
423
+ }
424
+ // Masking is its own inverse, so undoing it is applying it again.
425
+ applyMask(modules, reserved, mask, size);
426
+ }
427
+
428
+ applyMask(modules, reserved, bestMask, size);
429
+ writeFormat(modules, errorCorrection, bestMask, size);
430
+
431
+ return { modules, size, version };
432
+ }
433
+
434
+ function applyMask(
435
+ modules: boolean[][],
436
+ reserved: boolean[][],
437
+ mask: number,
438
+ size: number
439
+ ): void {
440
+ const test = MASKS[mask]!;
441
+ for (let y = 0; y < size; y++) {
442
+ for (let x = 0; x < size; x++) {
443
+ if (!reserved[y]![x] && test!(x, y)) modules[y]![x] = !modules[y]![x];
444
+ }
445
+ }
446
+ }
447
+
448
+ /** The 15 format bits, BCH-protected and masked, in both of their places. */
449
+ function writeFormat(
450
+ modules: boolean[][],
451
+ level: ErrorCorrectionLevel,
452
+ mask: number,
453
+ size: number
454
+ ): void {
455
+ const data = (FORMAT_BITS[level] << 3) | mask;
456
+ let rest = data;
457
+ for (let i = 0; i < 10; i++) rest = (rest << 1) ^ ((rest >>> 9) * 0x537);
458
+ const bits = (((data << 10) | rest) ^ 0x5412) >>> 0;
459
+
460
+ const at = (i: number) => ((bits >>> i) & 1) === 1;
461
+
462
+ // The copy around the top-left finder: down column 8, then along row 8.
463
+ // Indices are [y][x], and the two runs change different axes — which is the
464
+ // whole trap here.
465
+ for (let i = 0; i <= 5; i++) modules[i]![8] = at(i);
466
+ modules[7]![8] = at(6);
467
+ modules[8]![8] = at(7);
468
+ modules[8]![7] = at(8);
469
+ for (let i = 9; i < 15; i++) modules[8]![14 - i] = at(i);
470
+
471
+ // And the split copy: along row 8 at the right edge, then down column 8 at
472
+ // the bottom.
473
+ for (let i = 0; i < 8; i++) modules[8]![size - 1 - i] = at(i);
474
+ for (let i = 8; i < 15; i++) modules[size - 15 + i]![8] = at(i);
475
+
476
+ modules[size - 8]![8] = true;
477
+ }
478
+
479
+ /** The four penalty rules. Lower is a code more readers agree about. */
480
+ function penaltyScore(modules: boolean[][], size: number): number {
481
+ let penalty = 0;
482
+
483
+ // 1 & 3 — runs of five or more, and the finder-like 1:1:3:1:1 sequence.
484
+ for (let axis = 0; axis < 2; axis++) {
485
+ for (let a = 0; a < size; a++) {
486
+ let runColour = false;
487
+ let runLength = 0;
488
+ const history = [0, 0, 0, 0, 0, 0, 0];
489
+
490
+ for (let b = 0; b < size; b++) {
491
+ const dark = axis === 0 ? modules[a]![b]! : modules[b]![a]!;
492
+
493
+ if (dark === runColour) {
494
+ runLength++;
495
+ if (runLength === 5) penalty += 3;
496
+ else if (runLength > 5) penalty += 1;
497
+ } else {
498
+ history.pop();
499
+ history.unshift(runLength);
500
+ if (!runColour && hasFinderPattern(history)) penalty += 40;
501
+ runColour = dark;
502
+ runLength = 1;
503
+ }
504
+ }
505
+
506
+ history.pop();
507
+ history.unshift(runLength);
508
+ if (runColour) {
509
+ history.pop();
510
+ history.unshift(0);
511
+ }
512
+ if (hasFinderPattern(history)) penalty += 40;
513
+ }
514
+ }
515
+
516
+ // 2 — solid 2×2 blocks.
517
+ for (let y = 0; y < size - 1; y++) {
518
+ for (let x = 0; x < size - 1; x++) {
519
+ const corner = modules[y]![x];
520
+ if (
521
+ corner === modules[y]![x + 1] &&
522
+ corner === modules[y + 1]![x] &&
523
+ corner === modules[y + 1]![x + 1]
524
+ ) {
525
+ penalty += 3;
526
+ }
527
+ }
528
+ }
529
+
530
+ // 4 — the balance of dark to light across the whole code.
531
+ let dark = 0;
532
+ for (const row of modules) for (const module of row) if (module) dark++;
533
+ const total = size * size;
534
+ const deviation = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1;
535
+ penalty += deviation * 10;
536
+
537
+ return penalty;
538
+ }
539
+
540
+ /** The 1:1:3:1:1 run ratio, with four light modules on one side of it. */
541
+ function hasFinderPattern(history: number[]): boolean {
542
+ const middle = history[1]!;
543
+ if (middle <= 0 || history[2] !== middle || history[4] !== middle || history[5] !== middle) {
544
+ return false;
545
+ }
546
+ if (history[3] !== middle * 3) return false;
547
+ return Math.max(history[0]!, history[6]!) >= middle * 4;
548
+ }
package/src/index.ts CHANGED
@@ -216,6 +216,22 @@ export {
216
216
  type FlowRect,
217
217
  type FlowPoint,
218
218
  } from './components/flow';
219
+ export {
220
+ FunnelChart,
221
+ useFunnelChart,
222
+ type FunnelChartProps,
223
+ type FunnelChartHandle,
224
+ type FunnelChartHeaderProps,
225
+ type FunnelChartStagesProps,
226
+ type FunnelChartLabelsProps,
227
+ type FunnelChartLegendProps,
228
+ type FunnelChartSkeletonProps,
229
+ type FunnelChartStatus,
230
+ type FunnelDatum,
231
+ type FunnelEdges,
232
+ type FunnelLegendLayout,
233
+ type FunnelShare,
234
+ } from './components/funnel-chart';
219
235
  export {
220
236
  Frame,
221
237
  type FrameProps,
@@ -682,6 +698,25 @@ export {
682
698
  type PostVote,
683
699
  } from './components/post';
684
700
  export { Progress, type ProgressProps } from './components/progress';
701
+ export {
702
+ QRCode,
703
+ type QRCodeProps,
704
+ type QRCodeCanvasProps,
705
+ type QRCodeFrameProps,
706
+ type QRCodeHeaderProps,
707
+ type QRCodeTitleProps,
708
+ type QRCodeActionProps,
709
+ type QRCodePanelProps,
710
+ type QRCodeDescriptionProps,
711
+ type QRCodeCaptionProps,
712
+ type QRCodeValueProps,
713
+ type QRCodeLogoProps,
714
+ type QRCodeTriggerProps,
715
+ type QRCodeContentProps,
716
+ type QRCodeSize,
717
+ type QRCodePresentation,
718
+ type ErrorCorrectionLevel,
719
+ } from './components/qr-code';
685
720
  export {
686
721
  Questionnaire,
687
722
  type QuestionnaireProps,
@@ -751,3 +751,55 @@ export function columnValues(
751
751
  return typeof value === 'number' && !Number.isNaN(value) ? value : null;
752
752
  });
753
753
  }
754
+
755
+ /**
756
+ * One length of a funnel: a band symmetrical about a centre line, `head` wide
757
+ * at one end and `tail` wide at the other.
758
+ *
759
+ * Given as two half-extents about a middle rather than as four corners, because
760
+ * that is the shape of the data — a stage knows what it is worth and what the
761
+ * next one is worth, and the taper between them is the drop.
762
+ *
763
+ * `curve` is how far along the band the control points reach, as a fraction of
764
+ * its length. Past `0.5` the two reach beyond each other, which is what turns
765
+ * the join from a diagonal into the S the eye reads as a single continuous
766
+ * funnel rather than a stack of separate trapezoids; `0` gives the straight
767
+ * diagonal. The sides are the only curved part — the ends stay square, so a
768
+ * band's end lines up exactly with the next band's start.
769
+ *
770
+ * The run goes across, so `offset` and `length` are horizontal and the band is
771
+ * `head` and `tail` tall about `middle`.
772
+ */
773
+ export function ribbonPath(
774
+ offset: number,
775
+ length: number,
776
+ head: number,
777
+ tail: number,
778
+ middle: number,
779
+ curve: number
780
+ ): string {
781
+ 'worklet';
782
+ if (length <= 0 || (head <= 0 && tail <= 0)) return '';
783
+
784
+ const start = offset;
785
+ const end = offset + length;
786
+ const topStart = middle - head;
787
+ const topEnd = middle - tail;
788
+ const bottomStart = middle + head;
789
+ const bottomEnd = middle + tail;
790
+
791
+ if (!(curve > 0)) {
792
+ return `M${start},${topStart}L${end},${topEnd}L${end},${bottomEnd}L${start},${bottomStart}Z`;
793
+ }
794
+
795
+ const near = start + length * curve;
796
+ const far = end - length * curve;
797
+
798
+ return (
799
+ `M${start},${topStart}` +
800
+ `C${near},${topStart},${far},${topEnd},${end},${topEnd}` +
801
+ `L${end},${bottomEnd}` +
802
+ `C${far},${bottomEnd},${near},${bottomStart},${start},${bottomStart}` +
803
+ `Z`
804
+ );
805
+ }