panelui-native 0.54.0 → 0.57.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 (47) hide show
  1. package/README.md +18 -9
  2. package/lib/module/components/combobox/index.js +61 -7
  3. package/lib/module/components/combobox/index.js.map +1 -1
  4. package/lib/module/components/hex-chart/index.js +765 -0
  5. package/lib/module/components/hex-chart/index.js.map +1 -0
  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/components/sortable/index.js +74 -12
  11. package/lib/module/components/sortable/index.js.map +1 -1
  12. package/lib/module/components/steps/index.js +95 -12
  13. package/lib/module/components/steps/index.js.map +1 -1
  14. package/lib/module/components/tag-input/index.js +435 -0
  15. package/lib/module/components/tag-input/index.js.map +1 -0
  16. package/lib/module/components/tour/index.js +661 -0
  17. package/lib/module/components/tour/index.js.map +1 -0
  18. package/lib/module/index.js +4 -0
  19. package/lib/module/index.js.map +1 -1
  20. package/lib/module/utils/chart.js +209 -0
  21. package/lib/module/utils/chart.js.map +1 -1
  22. package/lib/typescript/src/components/combobox/index.d.ts.map +1 -1
  23. package/lib/typescript/src/components/hex-chart/index.d.ts +277 -0
  24. package/lib/typescript/src/components/hex-chart/index.d.ts.map +1 -0
  25. package/lib/typescript/src/components/qr-code/index.d.ts +221 -0
  26. package/lib/typescript/src/components/qr-code/index.d.ts.map +1 -0
  27. package/lib/typescript/src/components/qr-code/qr-encode.d.ts +39 -0
  28. package/lib/typescript/src/components/qr-code/qr-encode.d.ts.map +1 -0
  29. package/lib/typescript/src/components/sortable/index.d.ts.map +1 -1
  30. package/lib/typescript/src/components/steps/index.d.ts +19 -0
  31. package/lib/typescript/src/components/steps/index.d.ts.map +1 -1
  32. package/lib/typescript/src/components/tag-input/index.d.ts +240 -0
  33. package/lib/typescript/src/components/tag-input/index.d.ts.map +1 -0
  34. package/lib/typescript/src/components/tour/index.d.ts +168 -0
  35. package/lib/typescript/src/components/tour/index.d.ts.map +1 -0
  36. package/lib/typescript/src/index.d.ts +4 -0
  37. package/lib/typescript/src/index.d.ts.map +1 -1
  38. package/lib/typescript/src/utils/chart.d.ts +98 -0
  39. package/lib/typescript/src/utils/chart.d.ts.map +1 -1
  40. package/package.json +1 -1
  41. package/src/components/hex-chart/index.tsx +1009 -0
  42. package/src/components/qr-code/index.tsx +663 -0
  43. package/src/components/qr-code/qr-encode.ts +548 -0
  44. package/src/components/steps/index.tsx +103 -8
  45. package/src/components/tour/index.tsx +933 -0
  46. package/src/index.ts +41 -0
  47. package/src/utils/chart.ts +236 -0
@@ -0,0 +1,663 @@
1
+ /**
2
+ * QRCode — a string, drawn as something a camera can read.
3
+ *
4
+ * ```tsx
5
+ * <QRCode value="https://panelui.dev">
6
+ * <QRCode.Frame>
7
+ * <QRCode.Canvas />
8
+ * </QRCode.Frame>
9
+ * <QRCode.Caption>Scan to open the docs</QRCode.Caption>
10
+ * </QRCode>
11
+ * ```
12
+ *
13
+ * Composition is the API, as everywhere else here: a code in a titled panel is
14
+ * one with a `QRCode.Header` in it, and a code behind a button is one wrapped
15
+ * in a `QRCode.Trigger` and a `QRCode.Content` — not one with a prop turned on.
16
+ *
17
+ * ## What it draws
18
+ *
19
+ * Every dark module is one subpath of a single `<Path>`, not a `<Rect>` of its
20
+ * own. A version 10 code is 3,481 modules; half of them dark is seventeen
21
+ * hundred native views for a picture that never changes, and the difference
22
+ * between that and one node is the difference between a code that appears and
23
+ * one that appears eventually.
24
+ *
25
+ * The colours come from the theme, so a code drawn on a card is legible on
26
+ * every one of them — and light-on-dark is drawn the way scanners expect it,
27
+ * with the quiet zone painted rather than left transparent. A code with
28
+ * nothing behind it reads at about half the distance.
29
+ *
30
+ * ## The hole in the middle
31
+ *
32
+ * `QRCode.Logo` clears a square of modules and puts its children there.
33
+ * Error correction is what makes that survivable — the modules are gone, and
34
+ * the code still reads because there is enough redundancy to reconstruct them.
35
+ * How much is `errorCorrection`, and a logo raises the level it needs: the
36
+ * default `M` tolerates about 15% loss, `H` about 30%. A logo is measured
37
+ * against that budget and the level is raised for you if it has to be.
38
+ */
39
+ import {
40
+ createContext,
41
+ forwardRef,
42
+ useContext,
43
+ useMemo,
44
+ type ReactElement,
45
+ type ReactNode,
46
+ } from 'react';
47
+ import { View, type ViewProps } from 'react-native';
48
+ import Svg, { Path, Rect } from 'react-native-svg';
49
+ import { tv, type VariantProps } from 'tailwind-variants';
50
+ import { useCSSVariable } from 'uniwind';
51
+ import { Text } from '../../primitives/text';
52
+ import { cn } from '../../utils/cn';
53
+ import { Popover, type PopoverContentProps } from '../popover';
54
+ import { encodeQr, type ErrorCorrectionLevel, type QrMatrix } from './qr-encode';
55
+
56
+ export type { ErrorCorrectionLevel };
57
+
58
+ const qrCodeVariants = tv({
59
+ slots: {
60
+ root: 'items-center gap-3',
61
+ // The widget shell every chart in the library is shown in: a titled tray
62
+ // with the card flush inside it. A code is the same kind of thing — one
63
+ // object with a label over it — so it is the same shape.
64
+ frame: 'w-full overflow-hidden rounded-3xl border border-border bg-surface',
65
+ header: 'w-full flex-row items-center justify-between gap-3 px-4 pb-3 pt-2.5',
66
+ // Flush left, right and bottom: the shell's own edge is already there.
67
+ panel: 'relative items-center justify-center overflow-hidden rounded-t-2xl border-t border-border bg-card p-4',
68
+ caption: 'text-center text-sm text-muted-foreground',
69
+ value: 'text-center text-xs text-muted-foreground',
70
+ },
71
+ variants: {
72
+ size: {
73
+ sm: {},
74
+ md: {},
75
+ lg: {},
76
+ },
77
+ },
78
+ defaultVariants: {
79
+ size: 'md',
80
+ },
81
+ });
82
+
83
+ type QrCodeVariantProps = VariantProps<typeof qrCodeVariants>;
84
+
85
+ /** How big the drawn code is. */
86
+ export type QRCodeSize = NonNullable<QrCodeVariantProps['size']>;
87
+
88
+ /** Side length in points per size. The module grid divides into it. */
89
+ const CANVAS_SIZE: Record<QRCodeSize, number> = { sm: 128, md: 180, lg: 240 };
90
+
91
+ /**
92
+ * The quiet zone, in modules. Four is what the specification asks for and what
93
+ * scanners are tuned to; less and a code against a busy background stops being
94
+ * found at all.
95
+ */
96
+ const QUIET_ZONE = 4;
97
+
98
+ /** Roughly how much of the code each level can lose and still be read. */
99
+ const TOLERANCE: Record<ErrorCorrectionLevel, number> = { L: 0.07, M: 0.15, Q: 0.25, H: 0.3 };
100
+
101
+ /** Ascending, so "the next level up" is a step through this. */
102
+ const LEVELS: ErrorCorrectionLevel[] = ['L', 'M', 'Q', 'H'];
103
+
104
+ /** How much of the code a logo of this fraction covers. */
105
+ function coverage(logoFraction: number) {
106
+ return logoFraction * logoFraction;
107
+ }
108
+
109
+ /**
110
+ * The cleared square, in module coordinates.
111
+ *
112
+ * Both the canvas and the logo compute it from this, so the hole and the thing
113
+ * filling it are the same square. Two nearly-equal calculations is how you get
114
+ * a dark ring around a logo, which is the failure the screenshot showed.
115
+ */
116
+ function holeBounds(matrixSize: number, logoFraction: number) {
117
+ const from = Math.floor((matrixSize * (1 - logoFraction)) / 2);
118
+ const to = Math.ceil((matrixSize * (1 + logoFraction)) / 2);
119
+ return { from, to };
120
+ }
121
+
122
+ /**
123
+ * The level actually used: the one asked for, unless a logo would eat more of
124
+ * the code than it can afford, in which case the next one that can.
125
+ */
126
+ function levelFor(requested: ErrorCorrectionLevel, logoFraction: number): ErrorCorrectionLevel {
127
+ const needed = coverage(logoFraction);
128
+ // Half the budget, not all of it: the rest is for the things a code in the
129
+ // world actually loses — glare, a fold, a thumb.
130
+ const affordable = (level: ErrorCorrectionLevel) => TOLERANCE[level] / 2 >= needed;
131
+
132
+ if (affordable(requested)) return requested;
133
+ return LEVELS.slice(LEVELS.indexOf(requested)).find(affordable) ?? 'H';
134
+ }
135
+
136
+ interface QRCodeContextValue {
137
+ matrix: QrMatrix | null;
138
+ /** What went wrong, when there is no matrix. */
139
+ error: string | null;
140
+ value: string;
141
+ size: QRCodeSize;
142
+ /** Side of the logo hole as a fraction of the code, `0` for none. */
143
+ logoFraction: number;
144
+ slots: ReturnType<typeof qrCodeVariants>;
145
+ }
146
+
147
+ const QRCodeContext = createContext<QRCodeContextValue | null>(null);
148
+
149
+ function useQRCode(part: string) {
150
+ const context = useContext(QRCodeContext);
151
+ if (!context) throw new Error(`${part} must be used inside <QRCode>.`);
152
+ return context;
153
+ }
154
+
155
+ /** Where the code is drawn: in place, in a popover, or up from the bottom. */
156
+ export type QRCodePresentation = 'inline' | 'popover' | 'bottom-sheet';
157
+
158
+ export interface QRCodeProps extends Omit<ViewProps, 'children'>, QrCodeVariantProps {
159
+ className?: string;
160
+ /**
161
+ * What the code encodes. Anything: a URL, a WiFi string, a vCard. Encoded
162
+ * as UTF-8, and the version grows to fit it.
163
+ */
164
+ value: string;
165
+ /**
166
+ * How much of the code can be lost and still read — `L` about 7%, `M` 15%,
167
+ * `Q` 25%, `H` 30%. More correction means a denser code at the same size, so
168
+ * `M` is the default. Raised automatically when a `QRCode.Logo` needs it.
169
+ */
170
+ errorCorrection?: ErrorCorrectionLevel;
171
+ /**
172
+ * Fix the QR version, 1–40, instead of taking the smallest that fits. Worth
173
+ * setting when the content changes and the code should not visibly change
174
+ * density with it.
175
+ */
176
+ version?: number;
177
+ /**
178
+ * Where the code appears. `inline` draws it where it sits; the other two
179
+ * put it behind a `QRCode.Trigger` and draw it in a `QRCode.Content`.
180
+ */
181
+ presentation?: QRCodePresentation;
182
+ /** Controlled open state. Ignored while `presentation` is `inline`. */
183
+ open?: boolean;
184
+ onOpenChange?: (open: boolean) => void;
185
+ children?: ReactNode;
186
+ }
187
+
188
+ const QRCodeRoot = forwardRef<View, QRCodeProps>(
189
+ (
190
+ {
191
+ className,
192
+ value,
193
+ errorCorrection = 'M',
194
+ version,
195
+ size = 'md',
196
+ presentation = 'inline',
197
+ open,
198
+ onOpenChange,
199
+ children,
200
+ ...props
201
+ },
202
+ ref
203
+ ) => {
204
+ const slots = qrCodeVariants({ size });
205
+
206
+ /*
207
+ * A logo is a child, and the matrix has to know about it before the child
208
+ * renders — the level it forces changes the matrix itself. Rather than
209
+ * inspecting children, the fraction is a constant the Logo part also uses,
210
+ * and the root asks whether one is present at all.
211
+ */
212
+ const logoFraction = hasLogo(children) ? LOGO_FRACTION : 0;
213
+ const level = levelFor(errorCorrection, logoFraction);
214
+
215
+ const { matrix, error } = useMemo(() => {
216
+ try {
217
+ return { matrix: encodeQr(value, { errorCorrection: level, version }), error: null };
218
+ } catch (cause) {
219
+ // Too much data, or a version too small for it. A throw here would
220
+ // take a screen down over a string, so it draws nothing and says why.
221
+ return { matrix: null, error: (cause as Error).message };
222
+ }
223
+ }, [value, level, version]);
224
+
225
+ const context: QRCodeContextValue = { matrix, error, value, size, logoFraction, slots };
226
+
227
+ if (presentation !== 'inline') {
228
+ // The provider stays outside the Popover, and Content re-provides it
229
+ // inside — portal content mounts under the portal host, which is not in
230
+ // this subtree.
231
+ return (
232
+ <QRCodeContext.Provider value={context}>
233
+ <Popover open={open} onOpenChange={onOpenChange} presentation={presentation}>
234
+ {children}
235
+ </Popover>
236
+ </QRCodeContext.Provider>
237
+ );
238
+ }
239
+
240
+ return (
241
+ <QRCodeContext.Provider value={context}>
242
+ <View ref={ref} className={slots.root({ className })} {...props}>
243
+ {children}
244
+ </View>
245
+ </QRCodeContext.Provider>
246
+ );
247
+ }
248
+ );
249
+ QRCodeRoot.displayName = 'QRCode';
250
+
251
+ /* ------------------------------------------------------------------ *
252
+ * The code itself.
253
+ * ------------------------------------------------------------------ */
254
+
255
+ export interface QRCodeCanvasProps extends Omit<ViewProps, 'children'> {
256
+ className?: string;
257
+ /** Side length in points. Defaults to the size variant's. */
258
+ pixelSize?: number;
259
+ /** Dark modules. See the note below before overriding this. */
260
+ color?: string;
261
+ /** The plate the modules sit on. See the note below. */
262
+ backgroundColor?: string;
263
+ }
264
+
265
+ /**
266
+ * Dark modules on a light plate, whatever the theme is doing.
267
+ *
268
+ * This is the one place in the library that does not follow the tokens, and it
269
+ * is deliberate. A QR code is not a surface — it is a thing a camera has to
270
+ * read, and readers expect dark on light. Inverted, a code is rejected outright
271
+ * by a good share of scanners and found late by most of the rest, which turns a
272
+ * dark theme into a bug report about a code that "sometimes does not work".
273
+ *
274
+ * On a light theme the plate is white on a near-white card, which looks like
275
+ * nothing at all — correct, and the intended outcome. On a dark theme it reads
276
+ * as a light plate holding a code, which is the shape everyone recognises.
277
+ *
278
+ * `color` and `backgroundColor` override both, for a code that is definitely
279
+ * being read by something you control.
280
+ */
281
+ const PLATE = '#ffffff';
282
+ const MODULE = '#111111';
283
+
284
+ const QRCodeCanvas = forwardRef<View, QRCodeCanvasProps>(
285
+ ({ className, pixelSize, color, backgroundColor, ...props }, ref) => {
286
+ const { matrix, error, value, size, logoFraction } = useQRCode('QRCode.Canvas');
287
+
288
+ const side = pixelSize ?? CANVAS_SIZE[size];
289
+ const dark = color ?? MODULE;
290
+ const light = backgroundColor ?? PLATE;
291
+
292
+ /*
293
+ * One path for every dark module.
294
+ *
295
+ * The viewBox is in module units, so the path is written once and scales
296
+ * to whatever `side` is — no arithmetic per module, and no rounding gaps
297
+ * between neighbours at fractional sizes.
298
+ */
299
+ const path = useMemo(() => {
300
+ if (!matrix) return '';
301
+
302
+ const hole = logoFraction ? holeBounds(matrix.size, logoFraction) : null;
303
+
304
+ let d = '';
305
+ for (let y = 0; y < matrix.size; y++) {
306
+ for (let x = 0; x < matrix.size; x++) {
307
+ if (!matrix.modules[y]![x]) continue;
308
+ // Modules under the logo are not drawn at all — drawing them and
309
+ // covering them leaves a dark edge wherever the logo is smaller
310
+ // than the square it cleared.
311
+ if (hole && x >= hole.from && x < hole.to && y >= hole.from && y < hole.to) continue;
312
+ d += `M${x + QUIET_ZONE} ${y + QUIET_ZONE}h1v1h-1z`;
313
+ }
314
+ }
315
+ return d;
316
+ }, [matrix, logoFraction]);
317
+
318
+ if (error) {
319
+ // A box the size the code would have been, so the layout does not jump
320
+ // when the value becomes something that fits.
321
+ return (
322
+ <View
323
+ ref={ref}
324
+ accessibilityRole="image"
325
+ accessibilityLabel="QR code could not be generated"
326
+ className={cn('items-center justify-center rounded-lg bg-muted', className)}
327
+ style={{ width: side, height: side }}
328
+ {...props}
329
+ />
330
+ );
331
+ }
332
+
333
+ if (!matrix) return null;
334
+
335
+ const grid = matrix.size + QUIET_ZONE * 2;
336
+
337
+ return (
338
+ <View
339
+ ref={ref}
340
+ accessibilityRole="image"
341
+ accessibilityLabel={`QR code for ${value}`}
342
+ className={className}
343
+ style={{ width: side, height: side }}
344
+ {...props}
345
+ >
346
+ <Svg width="100%" height="100%" viewBox={`0 0 ${grid} ${grid}`}>
347
+ {/* Painted, not left transparent: a scanner needs the quiet zone to
348
+ be lighter than the code, and "whatever is behind it" is not.
349
+ Rounded, because at this point it is an object on the card rather
350
+ than a rectangle of paper. */}
351
+ <Rect x={0} y={0} width={grid} height={grid} rx={2.5} fill={light} />
352
+ <Path d={path} fill={dark} />
353
+ </Svg>
354
+ </View>
355
+ );
356
+ }
357
+ );
358
+ QRCodeCanvas.displayName = 'QRCode.Canvas';
359
+
360
+ /* ------------------------------------------------------------------ *
361
+ * The shells around it.
362
+ * ------------------------------------------------------------------ */
363
+
364
+ export interface QRCodeFrameProps extends ViewProps {
365
+ className?: string;
366
+ children?: ReactNode;
367
+ }
368
+
369
+ /**
370
+ * The tray the code sits in — the same widget shell the charts use: a titled
371
+ * strip, and a card flush inside it holding the thing itself.
372
+ */
373
+ const QRCodeFrame = forwardRef<View, QRCodeFrameProps>(
374
+ ({ className, children, ...props }, ref) => {
375
+ const { slots } = useQRCode('QRCode.Frame');
376
+ return (
377
+ <View ref={ref} className={slots.frame({ className })} {...props}>
378
+ {children}
379
+ </View>
380
+ );
381
+ }
382
+ );
383
+ QRCodeFrame.displayName = 'QRCode.Frame';
384
+
385
+ export interface QRCodeHeaderProps extends ViewProps {
386
+ className?: string;
387
+ children?: ReactNode;
388
+ }
389
+
390
+ /**
391
+ * The strip across the top of the tray. `QRCode.Title` takes the flexible
392
+ * side and `QRCode.Action` the end, so a long title truncates rather than
393
+ * shoving the trailing slot off the edge.
394
+ */
395
+ const QRCodeHeader = forwardRef<View, QRCodeHeaderProps>(
396
+ ({ className, children, ...props }, ref) => {
397
+ const { slots } = useQRCode('QRCode.Header');
398
+ return (
399
+ <View ref={ref} className={slots.header({ className })} {...props}>
400
+ {children}
401
+ </View>
402
+ );
403
+ }
404
+ );
405
+ QRCodeHeader.displayName = 'QRCode.Header';
406
+
407
+ export interface QRCodeTitleProps {
408
+ className?: string;
409
+ children?: ReactNode;
410
+ }
411
+
412
+ /** What the code is for. Muted — it is a caption on the tray, not a heading. */
413
+ function QRCodeTitle({ className, children }: QRCodeTitleProps) {
414
+ useQRCode('QRCode.Title');
415
+ return (
416
+ <Text size="sm" muted numberOfLines={1} className={cn('min-w-0 shrink', className)}>
417
+ {children}
418
+ </Text>
419
+ );
420
+ }
421
+ QRCodeTitle.displayName = 'QRCode.Title';
422
+
423
+ export interface QRCodeActionProps extends ViewProps {
424
+ className?: string;
425
+ children?: ReactNode;
426
+ }
427
+
428
+ /**
429
+ * The trailing slot on the header row — an expiry, a count, a button. A plain
430
+ * string draws as muted text; anything else draws as itself.
431
+ */
432
+ const QRCodeAction = forwardRef<View, QRCodeActionProps>(
433
+ ({ className, children, ...props }, ref) => {
434
+ useQRCode('QRCode.Action');
435
+ return (
436
+ <View
437
+ ref={ref}
438
+ className={cn('shrink-0 flex-row items-center gap-2', className)}
439
+ {...props}
440
+ >
441
+ {typeof children === 'string' ? (
442
+ <Text size="sm" muted>
443
+ {children}
444
+ </Text>
445
+ ) : (
446
+ children
447
+ )}
448
+ </View>
449
+ );
450
+ }
451
+ );
452
+ QRCodeAction.displayName = 'QRCode.Action';
453
+
454
+ export interface QRCodePanelProps extends ViewProps {
455
+ className?: string;
456
+ children?: ReactNode;
457
+ }
458
+
459
+ /**
460
+ * The card the code is drawn on, flush inside the tray. It is also what the
461
+ * logo positions itself against, so a `QRCode.Logo` belongs in here beside the
462
+ * canvas rather than anywhere else.
463
+ */
464
+ const QRCodePanel = forwardRef<View, QRCodePanelProps>(
465
+ ({ className, children, ...props }, ref) => {
466
+ const { slots } = useQRCode('QRCode.Panel');
467
+ return (
468
+ <View ref={ref} className={slots.panel({ className })} {...props}>
469
+ {children}
470
+ </View>
471
+ );
472
+ }
473
+ );
474
+ QRCodePanel.displayName = 'QRCode.Panel';
475
+
476
+ /* ------------------------------------------------------------------ *
477
+ * The readouts.
478
+ * ------------------------------------------------------------------ */
479
+
480
+ export interface QRCodeDescriptionProps {
481
+ className?: string;
482
+ children?: ReactNode;
483
+ }
484
+
485
+ /** A muted line inside the panel, under the code. */
486
+ function QRCodeDescription({ className, children }: QRCodeDescriptionProps) {
487
+ useQRCode('QRCode.Description');
488
+ return (
489
+ <Text size="sm" muted className={cn('text-center', className)}>
490
+ {children}
491
+ </Text>
492
+ );
493
+ }
494
+ QRCodeDescription.displayName = 'QRCode.Description';
495
+
496
+ export interface QRCodeCaptionProps {
497
+ className?: string;
498
+ children?: ReactNode;
499
+ }
500
+
501
+ /** A line under the code saying what scanning it does. */
502
+ function QRCodeCaption({ className, children }: QRCodeCaptionProps) {
503
+ const { slots } = useQRCode('QRCode.Caption');
504
+ return <Text className={slots.caption({ className })}>{children}</Text>;
505
+ }
506
+ QRCodeCaption.displayName = 'QRCode.Caption';
507
+
508
+ export interface QRCodeValueProps {
509
+ className?: string;
510
+ /** Show the whole string rather than one line of it. */
511
+ full?: boolean;
512
+ }
513
+
514
+ /**
515
+ * The encoded string itself, for someone who cannot scan it — typing a URL out
516
+ * is slower than pointing a camera at it and it is the only way through when
517
+ * the camera is the thing you are setting up.
518
+ */
519
+ function QRCodeValue({ className, full }: QRCodeValueProps) {
520
+ const { value, slots } = useQRCode('QRCode.Value');
521
+ return (
522
+ <Text
523
+ className={slots.value({ className })}
524
+ numberOfLines={full ? undefined : 1}
525
+ selectable
526
+ >
527
+ {value}
528
+ </Text>
529
+ );
530
+ }
531
+ QRCodeValue.displayName = 'QRCode.Value';
532
+
533
+ /* ------------------------------------------------------------------ *
534
+ * The logo.
535
+ * ------------------------------------------------------------------ */
536
+
537
+ /**
538
+ * How much of the code's width the logo hole takes.
539
+ *
540
+ * Constant rather than a prop, because the value has to be known before the
541
+ * matrix is built — it decides the error-correction level — and a prop on a
542
+ * child cannot be read from the parent without inspecting the tree. A quarter
543
+ * costs about 6% of the modules, which every level above `L` can afford.
544
+ */
545
+ const LOGO_FRACTION = 0.25;
546
+
547
+ export interface QRCodeLogoProps extends ViewProps {
548
+ className?: string;
549
+ children?: ReactNode;
550
+ }
551
+
552
+ /** Whether the tree contains a `QRCode.Logo`, without walking it deeply. */
553
+ function hasLogo(children: ReactNode): boolean {
554
+ let found = false;
555
+
556
+ const walk = (node: ReactNode) => {
557
+ if (found || !node) return;
558
+ if (Array.isArray(node)) {
559
+ node.forEach(walk);
560
+ return;
561
+ }
562
+ const element = node as ReactElement<{ children?: ReactNode }> & { type?: unknown };
563
+ if (typeof element !== 'object' || !('type' in element)) return;
564
+ if (element.type === QRCodeLogo) {
565
+ found = true;
566
+ return;
567
+ }
568
+ walk(element.props?.children);
569
+ };
570
+
571
+ walk(children);
572
+ return found;
573
+ }
574
+
575
+ /**
576
+ * Content for the cleared square in the middle — a mark, an avatar, an icon.
577
+ * Its presence is what clears the square, and what raises the error-correction
578
+ * level if the one asked for could not afford it.
579
+ */
580
+ const QRCodeLogo = forwardRef<View, QRCodeLogoProps>(
581
+ ({ className, children, ...props }, ref) => {
582
+ const { size, matrix } = useQRCode('QRCode.Logo');
583
+ if (!matrix) return null;
584
+
585
+ /*
586
+ * Sized from the same square the canvas cleared, in the same units.
587
+ *
588
+ * Taking a flat 25% of the canvas instead is close, but not equal — the
589
+ * hole is a whole number of modules and the canvas is not necessarily
590
+ * divisible by them, so the two disagreed by a module or so and the
591
+ * difference showed as a dark ring around the logo.
592
+ */
593
+ const { from, to } = holeBounds(matrix.size, LOGO_FRACTION);
594
+ const grid = matrix.size + QUIET_ZONE * 2;
595
+ const box = ((to - from) / grid) * CANVAS_SIZE[size];
596
+
597
+ return (
598
+ // `inset-0` and centred, not `absolute` with nothing else: an absolute
599
+ // box with no offsets takes its parent's alignment, which put the logo
600
+ // at the top of the panel rather than over the middle of the code.
601
+ <View
602
+ pointerEvents="none"
603
+ className="absolute bottom-0 left-0 right-0 top-0 items-center justify-center"
604
+ >
605
+ <View
606
+ ref={ref}
607
+ // The plate colour, so the square reads as part of the code rather
608
+ // than as a hole cut in it — and rounded, matching the plate.
609
+ className={cn('items-center justify-center overflow-hidden rounded-md', className)}
610
+ style={{ width: box, height: box, backgroundColor: PLATE }}
611
+ {...props}
612
+ >
613
+ {children}
614
+ </View>
615
+ </View>
616
+ );
617
+ }
618
+ );
619
+ QRCodeLogo.displayName = 'QRCode.Logo';
620
+
621
+ /* ------------------------------------------------------------------ *
622
+ * Folded away.
623
+ * ------------------------------------------------------------------ */
624
+
625
+ export interface QRCodeTriggerProps {
626
+ /** The element that opens it. Must accept `onPress`. */
627
+ children: ReactElement<{ onPress?: (...args: unknown[]) => void }>;
628
+ }
629
+
630
+ /** What opens a `popover` or `bottom-sheet` code. */
631
+ function QRCodeTrigger({ children }: QRCodeTriggerProps) {
632
+ useQRCode('QRCode.Trigger');
633
+ return <Popover.Trigger>{children}</Popover.Trigger>;
634
+ }
635
+ QRCodeTrigger.displayName = 'QRCode.Trigger';
636
+
637
+ export interface QRCodeContentProps extends PopoverContentProps {}
638
+
639
+ /** The panel the code is drawn in, when it is not drawn in place. */
640
+ function QRCodeContent({ className, width = 'content-fit', children, ...props }: QRCodeContentProps) {
641
+ const context = useQRCode('QRCode.Content');
642
+ return (
643
+ <Popover.Content width={width} className={cn('items-center gap-3', className)} {...props}>
644
+ <QRCodeContext.Provider value={context}>{children}</QRCodeContext.Provider>
645
+ </Popover.Content>
646
+ );
647
+ }
648
+ QRCodeContent.displayName = 'QRCode.Content';
649
+
650
+ export const QRCode = Object.assign(QRCodeRoot, {
651
+ Canvas: QRCodeCanvas,
652
+ Frame: QRCodeFrame,
653
+ Header: QRCodeHeader,
654
+ Title: QRCodeTitle,
655
+ Action: QRCodeAction,
656
+ Panel: QRCodePanel,
657
+ Description: QRCodeDescription,
658
+ Caption: QRCodeCaption,
659
+ Value: QRCodeValue,
660
+ Logo: QRCodeLogo,
661
+ Trigger: QRCodeTrigger,
662
+ Content: QRCodeContent,
663
+ });