import * as React from 'react'
import * as QrCode from './QrCode.js'
/**
* Renders a QR code with a finder pattern, cells, and an `arena` (if provided).
*
* @params {@link Cuer.Props}
* @returns A {@link React.ReactNode}
*/
export function Cuer(props: Cuer.Props) {
const { arena, ...rest } = props
return (
{arena && (
{typeof arena === 'string' ? (
) : (
arena
)}
)}
)
}
export namespace Cuer {
export type Props = React.PropsWithChildren<
QrCode.QrCode.Options & {
/**
* Arena to display in the center of the QR code.
*
* - `string`: will be rendered as an image.
* - `ReactNode`: will be rendered as a node.
*/
arena?: React.ReactNode | string | undefined
/**
* Class name for the root element.
*/
className?: string | undefined
/**
* Foreground color for the QR code.
*
* @default "currentColor"
*/
color?: string | undefined
/**
* Size for the QR code.
*
* @default "100%"
*/
size?: React.CSSProperties['width'] | undefined
/**
* Value to encode in the QR code.
*/
value: string
}
>
export const Context = React.createContext<{
arenaSize: number
cellSize: number
edgeSize: number
finderSize: number
qrcode: QrCode.QrCode
}>(null as never)
/**
* Root component for the QR code.
*
* @params {@link Root.Props}
* @returns A {@link React.ReactNode}
*/
export function Root(props: Root.Props) {
const { children, size = '100%', value, version, ...rest } = props
// Check if the children contain an `Arena` component.
const hasArena = React.useMemo(
() =>
(
React.Children.map(children, (child) => {
if (!React.isValidElement(child)) return null
if (typeof child.type === 'string') return null
if (
'displayName' in child.type &&
child.type.displayName === 'Arena'
)
return true
return null
}) ?? []
).some(Boolean),
[children],
)
// Create the QR code.
const qrcode = React.useMemo(() => {
let errorCorrection = props.errorCorrection
// If the QR code has an arena, use a higher error correction level.
if (hasArena && errorCorrection === 'low') errorCorrection = 'medium'
return QrCode.create(value, {
errorCorrection,
version,
})
}, [value, hasArena, props.errorCorrection, version])
const cellSize = 1
const edgeSize = qrcode.edgeLength * cellSize
const finderSize = (qrcode.finderLength * cellSize) / 2
const arenaSize = hasArena ? Math.floor(edgeSize / 4) : 0
const context = React.useMemo(
() => ({ arenaSize, cellSize, edgeSize, qrcode, finderSize }),
[arenaSize, edgeSize, qrcode, finderSize],
)
return (
)
}
export namespace Root {
export const displayName = 'Root'
export type Props = React.PropsWithChildren<
QrCode.QrCode.Options &
Omit<
React.SVGProps,
'children' | 'width' | 'height' | 'version'
> & {
/**
* Size for the QR code.
*
* @default "100%"
*/
size?: React.CSSProperties['width'] | undefined
/**
* Value to encode in the QR code.
*/
value: string
}
>
}
/**
* Finder component for the QR code. The finder pattern is the squares
* on the top left, top right, and bottom left of the QR code.
*
* @params {@link Finder.Props}
* @returns A {@link React.ReactNode}
*/
export function Finder(props: Finder.Props) {
const { className, fill, innerClassName, radius = 0.25 } = props
const { cellSize, edgeSize, finderSize } = React.useContext(Context)
function Inner({ position }: { position: string }) {
let outerX = finderSize - (finderSize - cellSize) - cellSize / 2
if (position === 'top-right')
outerX = edgeSize - finderSize - (finderSize - cellSize) - cellSize / 2
let outerY = finderSize - (finderSize - cellSize) - cellSize / 2
if (position === 'bottom-left')
outerY = edgeSize - finderSize - (finderSize - cellSize) - cellSize / 2
let innerX = finderSize - cellSize * 1.5
if (position === 'top-right')
innerX = edgeSize - finderSize - cellSize * 1.5
let innerY = finderSize - cellSize * 1.5
if (position === 'bottom-left')
innerY = edgeSize - finderSize - cellSize * 1.5
return (
<>
>
)
}
return (
<>
>
)
}
export namespace Finder {
export const displayName = 'Finder'
export type Props = Pick<
React.SVGProps,
'className' | 'stroke' | 'fill'
> & {
/**
* Class name for the inner rectangle.
*/
innerClassName?: string | undefined
/**
* Radius scale (between 0 and 1) for the finder.
*
* - `0`: no radius
* - `1`: full radius
*
* @default 0.25
*/
radius?: number | undefined
}
}
/**
* Cells for the QR code.
*
* @params {@link Cells.Props}
* @returns A {@link React.ReactNode}
*/
export function Cells(props: Cells.Props) {
const {
className,
fill = 'currentColor',
inset: inset_ = true,
radius = 1,
} = props
const { arenaSize, cellSize, qrcode } = React.useContext(Context)
const { edgeLength, finderLength } = qrcode
const path = React.useMemo(() => {
let path = ''
for (let i = 0; i < qrcode.grid.length; i++) {
const row = qrcode.grid[i]
if (!row) continue
for (let j = 0; j < row.length; j++) {
const cell = row[j]
if (!cell) continue
// Skip rendering dots in arena area.
const start = edgeLength / 2 - arenaSize / 2
const end = start + arenaSize
if (i >= start && i <= end && j >= start && j <= end) continue
// Skip rendering dots in the finder pattern areas
const topLeftFinder = i < finderLength && j < finderLength
const topRightFinder =
i < finderLength && j >= edgeLength - finderLength
const bottomLeftFinder =
i >= edgeLength - finderLength && j < finderLength
if (topLeftFinder || topRightFinder || bottomLeftFinder) continue
// Add inset for padding
const inset = inset_ ? cellSize * 0.1 : 0
const innerSize = (cellSize - inset * 2) / 2
// Calculate center positions
const cx = j * cellSize + cellSize / 2
const cy = i * cellSize + cellSize / 2
// Calculate edge positions
const left = cx - innerSize
const right = cx + innerSize
const top = cy - innerSize
const bottom = cy + innerSize
// Apply corner radius (clamped to maximum of innerSize)
const r = radius * innerSize
path += [
`M ${left + r},${top}`,
`L ${right - r},${top}`,
`A ${r},${r} 0 0,1 ${right},${top + r}`,
`L ${right},${bottom - r}`,
`A ${r},${r} 0 0,1 ${right - r},${bottom}`,
`L ${left + r},${bottom}`,
`A ${r},${r} 0 0,1 ${left},${bottom - r}`,
`L ${left},${top + r}`,
`A ${r},${r} 0 0,1 ${left + r},${top}`,
'z',
].join(' ')
}
}
return path
}, [
arenaSize,
cellSize,
edgeLength,
finderLength,
qrcode.grid,
inset_,
radius,
])
return
}
export namespace Cells {
export const displayName = 'Cells'
export type Props = Pick<
React.SVGProps,
'className' | 'filter' | 'fill'
> & {
/**
* @deprecated @internal
*/
hasArena?: boolean | undefined
/**
* Whether to add an inset to the cells.
*
* @default true
*/
inset?: boolean | undefined
/**
* Radius scale (between 0 and 1) for the cells.
*
* - `0`: no radius
* - `1`: full radius
*
* @default 1
*/
radius?: number | undefined
}
}
/**
* Arena component for the QR code. The arena is the area in the center
* of the QR code that is not part of the finder pattern.
*
* @params {@link Arena.Props}
* @returns A {@link React.ReactNode}
*/
export function Arena(props: Arena.Props) {
const { children } = props
const { arenaSize, cellSize, edgeSize } = React.useContext(Context)
const start = Math.ceil(edgeSize / 2 - arenaSize / 2)
const size = arenaSize + (arenaSize % 2)
const padding = cellSize / 2
return (
{children}
)
}
export namespace Arena {
export const displayName = 'Arena'
export type Props = {
children: React.ReactNode
}
}
}