Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/jpeg-download-background.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lglab/react-qr-code': patch
---

Fix JPEG downloads of QR codes without a background: transparent areas were exported as black, making the code unscannable. JPEG downloads now use a white background; PNG downloads stay transparent.
5 changes: 5 additions & 0 deletions .changeset/qr-code-frames.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lglab/react-qr-code': minor
---

Add `frameSettings` to draw a frame with a call-to-action label around the QR code. Available styles: `banner-bottom`, `banner-top`, `ticket`, `bubble` and `border`.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ A highly customizable and lightweight QR code generator for React applications.
## Quick Features

- **Highly Customizable** - Style the finder patterns, modules, and colors exactly how you want.
- **Frames** - Wrap the QR code in a frame with a call-to-action label like "Scan me!".
- **Performance Optimized** - Generates QR codes efficiently without sacrificing quality.
- **SVG-Based Rendering** - Crisp and scalable output for web and print
- **Developer-Friendly** - Built with TypeScript, easy to use, and well-documented.
Expand Down
30 changes: 30 additions & 0 deletions apps/docs/public/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ The main component exported by the library.
| `finderPatternOuterSettings` | `FinderPatternOuterSettings` | - | Configuration for outer finder patterns. |
| `finderPatternInnerSettings` | `FinderPatternInnerSettings` | - | Configuration for inner finder patterns. |
| `imageSettings` | `ImageSettings` | - | Settings for an embedded logo or image. |
| `frameSettings` | `FrameSettings` | - | Draws a frame with a call-to-action label around the QR code. `size` then applies to the frame width; the height grows to fit the label. |
| `svgProps` | `React.SVGProps<SVGSVGElement>` | - | Additional props passed to the root SVG element. |

---
Expand Down Expand Up @@ -75,6 +76,21 @@ Includes all `FinderPatternOuterStyle` plus: `'diamond'`, `'star'`, `'heart'`, `
| `opacity` | `number` | `1` | Image opacity (0-1). |
| `crossOrigin` | `CrossOrigin` | - | CORS setting for the image. |

### FrameSettings

| Property | Type | Default | Description |
| --- | --- | --- | --- |
| `style` | `FrameStyle` | - | **Required.** Style of the frame. |
| `text` | `string` | `'Scan me!'` | Label rendered in the frame. Long labels are condensed to fit. An empty string hides it and the frame wraps the code evenly. |
| `color` | `string` | `'#000000'` | Frame color. |
| `textColor` | `string` | - | Label color. Defaults to white on filled frames and to `color` on `'border'`. |
| `fontFamily` | `string` | `'sans-serif'` | Label font family (CSS font-family list). Web fonts loaded by the page work on screen, but downloads only keep the name: SVG files need the font installed where they are opened, and PNG/JPEG exports cannot use page web fonts, only fonts installed on the device. End the list with a generic family. |

**Available Styles (`FrameStyle`):**
`'banner-bottom'`, `'banner-top'`, `'ticket'`, `'bubble'`, `'border'`

When a frame is set, the QR code sits on a light panel (the `background` color when it is a string, white otherwise) with a quiet zone of at least 2 modules, so it stays scannable on dark frames.

### GradientSettings

| Property | Type | Default | Description |
Expand Down Expand Up @@ -137,3 +153,17 @@ import { ReactQRCode } from '@lglab/react-qr-code';
/>
```

### Frame
```tsx
<ReactQRCode
value="https://reactqrcode.com"
size={256}
marginSize={2}
frameSettings={{
style: 'ticket',
text: 'Scan me!',
color: '#000000'
}}
/>
```

1 change: 1 addition & 0 deletions apps/docs/public/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ This site serves as the central hub for the `@lglab/react-qr-code` library, prov
- [Finder Pattern Outer Settings](https://reactqrcode.com/finder-pattern-outer-settings)
- [Gradient Settings](https://reactqrcode.com/gradient-settings)
- [Image Settings](https://reactqrcode.com/image-settings)
- [Frame Settings](https://reactqrcode.com/frame-settings)
- [Examples](https://reactqrcode.com/examples)

## External Links
Expand Down
43 changes: 43 additions & 0 deletions apps/docs/src/app/examples/frame/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Metadata } from 'next'
import Link from 'next/link'

import {
DemoCustom,
DemoStyles,
customCodeBlock,
stylesCodeBlock,
} from '@/components/demos/frame'
import { PageHeading } from '@/components/page-heading'
import { ExampleTabs } from '@/components/ui/example-tabs'
import { TypographyH3, TypographyP } from '@/components/ui/typography'

export const metadata: Metadata = {
title: 'Frame Example',
description:
'Example showing how to add a frame with a "Scan me!" call-to-action label around QR codes using the FrameSettings options in @lglab/react-qr-code.',
}

export default function Page() {
return (
<>
<PageHeading heading='Frame example' />
<TypographyP>
The @lglab/react-qr-code library can draw a frame with a call-to-action label
around your QR codes, making them easier to spot on print and screens. Frames are
part of the SVG, so they are included when you download the QR code. See{' '}
<Link className='underline' href='/frame-settings'>
Frame Settings
</Link>{' '}
api reference for more information.
</TypographyP>

<TypographyH3>Styles</TypographyH3>
<ExampleTabs codeBlock={stylesCodeBlock} preview={<DemoStyles />} />

<hr className='border-0' />

<TypographyH3>Custom label and colors</TypographyH3>
<ExampleTabs codeBlock={customCodeBlock} preview={<DemoCustom />} />
</>
)
}
94 changes: 94 additions & 0 deletions apps/docs/src/app/frame-settings/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type { Metadata } from 'next'

import { PageHeading } from '@/components/page-heading'
import { PropsTable } from '@/components/props-table'
import {
TypographyH2,
TypographyInlineCode,
TypographyP,
} from '@/components/ui/typography'
import type { Prop } from '@/types/props'

export const metadata: Metadata = {
title: 'FrameSettings',
description:
'Configuration options for FrameSettings in @lglab/react-qr-code, used to draw a frame with a call-to-action label such as "Scan me!" around QR codes.',
}

const props: Prop[] = [
{
name: 'style',
type: 'FrameStyle',
description: 'The style of the frame.',
required: true,
possibleValues: ['banner-bottom', 'banner-top', 'border', 'bubble', 'ticket'],
},
{
name: 'text',
type: 'string',
description:
'The label rendered in the frame. Long labels are condensed to fit the frame width. Pass an empty string to hide the label, the frame then wraps the QR Code evenly.',
defaultValue: 'Scan me!',
},
{
name: 'color',
type: 'string',
description: 'The frame color.',
defaultValue: '#000000',
},
{
name: 'textColor',
type: 'string',
description:
'The label color. Defaults to white on filled frames (banner-bottom, banner-top, ticket, bubble) and to the frame color on border.',
},
{
name: 'fontFamily',
type: 'string',
description:
'The label font family, as a CSS font-family value. See the font family section below for how it behaves in downloads.',
defaultValue: 'sans-serif',
},
]

export default function Page() {
return (
<>
<PageHeading heading='FrameSettings' />
<TypographyP>
These are the properties you can use to draw a frame with a call-to-action label
around the QR Code. When a frame is set, the size prop applies to the frame width
and the height grows to fit the label. The QR Code sits on a light panel (the
background color, white by default) with a quiet zone of at least 2 modules, so it
stays scannable on dark frames.
</TypographyP>
<PropsTable props={props} />

<TypographyH2>Font family</TypographyH2>
<TypographyP>
The fontFamily value is set as the font-family of the SVG label, so it accepts any
CSS font-family list such as{' '}
<TypographyInlineCode>&apos;Georgia, serif&apos;</TypographyInlineCode>. The label
is always bold. Which font is actually used depends on where the QR Code is
displayed:
</TypographyP>
<TypographyP>
On the page, the label can use system fonts and any web font your page has already
loaded, for example through Google Fonts, next/font or @font-face, as long as the
name matches.
</TypographyP>
<TypographyP>
Downloads only keep the font name, not the font file. An SVG download shows the
font if it is installed where the file is opened. PNG and JPEG downloads are
rendered as an isolated image that cannot see your page&apos;s web fonts, so they
use the font only if it is installed on the user&apos;s device.
</TypographyP>
<TypographyP>
In every case a missing font falls back to the next font in the list, so always
end it with a generic family such as sans-serif or serif. For labels that must
look the same everywhere, prefer widely installed fonts like Arial, Helvetica,
Georgia or Verdana.
</TypographyP>
</>
)
}
13 changes: 13 additions & 0 deletions apps/docs/src/app/react-qr-code-props/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,19 @@ const props: Prop[] = [
</>
),
},
{
name: 'frameSettings',
type: 'FrameSettings',
description: (
<>
Draws a frame with a call-to-action label around the QR code. When set, size
applies to the frame width and the height grows to fit the label. See{' '}
<Link className='underline' href='/frame-settings'>
FrameSettings
</Link>
</>
),
},
{
name: 'level',
type: 'ErrorCorrectionLevel',
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/src/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ const PAGES = [
{ path: '/finder-pattern-inner-settings', priority: 0.7 },
{ path: '/gradient-settings', priority: 0.7 },
{ path: '/image-settings', priority: 0.7 },
{ path: '/frame-settings', priority: 0.7 },
{ path: '/ref-api', priority: 0.7 },
{ path: '/examples/basic', priority: 0.7 },
{ path: '/examples/data-modules', priority: 0.7 },
{ path: '/examples/finder-patterns', priority: 0.7 },
{ path: '/examples/background', priority: 0.7 },
{ path: '/examples/image', priority: 0.7 },
{ path: '/examples/frame', priority: 0.7 },
{ path: '/examples/download', priority: 0.7 },
]

Expand Down
14 changes: 14 additions & 0 deletions apps/docs/src/components/demo/demo.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client'

import {
type FrameSettings,
ReactQRCode,
type ReactQRCodeProps,
type ReactQRCodeRef,
Expand All @@ -19,6 +20,7 @@ import { Colors } from './colors'
import { DataModules } from './data-modules'
import { FinderPatternInner } from './finder-pattern-inner'
import { FinderPatternOuter } from './finder-pattern-outer'
import { FrameSettingsDemo } from './frame-settings'
import { ImageSettingsDemo } from './image-settings'
import { MainSettings } from './main-settings'

Expand All @@ -32,6 +34,12 @@ export const defaultImageSettings = {
y: undefined,
}

export const defaultFrameSettings: FrameSettings = {
style: 'ticket',
text: 'Scan me!',
color: '#000000',
}

export const defaultBgColor = '#FFFFFF'
export const defaultBgGradient = {
type: 'linear',
Expand Down Expand Up @@ -120,6 +128,12 @@ export const Demo = () => {
<ImageSettingsDemo qrProps={qrProps} setQrProps={setQrProps} />
</AccContent>
</AccordionItem>
<AccordionItem value='frame-settings'>
<AccordionTrigger>Frame</AccordionTrigger>
<AccContent>
<FrameSettingsDemo qrProps={qrProps} setQrProps={setQrProps} />
</AccContent>
</AccordionItem>
</Accordion>
</div>
<div className='flex flex-col items-center gap-3 sm:col-span-2 [&>svg]:h-auto'>
Expand Down
104 changes: 104 additions & 0 deletions apps/docs/src/components/demo/frame-settings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {
type FrameSettings,
type FrameStyle,
type ReactQRCodeProps,
} from '@lglab/react-qr-code'
import { type Dispatch, useState } from 'react'

import { FormCheckbox, FormField } from '@/components/ui/form-fields'
import { Input } from '@/components/ui/input'

import { Button } from '../ui/button'
import { ColorPicker } from '../ui/color-picker'
import { defaultFrameSettings } from './demo'

interface FrameSettingsDemoProps {
qrProps: ReactQRCodeProps
setQrProps: Dispatch<React.SetStateAction<ReactQRCodeProps>>
}

const styles: FrameStyle[] = ['banner-bottom', 'banner-top', 'border', 'bubble', 'ticket']

export const FrameSettingsDemo = ({ qrProps, setQrProps }: FrameSettingsDemoProps) => {
const [includeFrame, setIncludeFrame] = useState(!!qrProps.frameSettings)
const [customTextColor, setCustomTextColor] = useState(
!!qrProps.frameSettings?.textColor,
)

const onValueChange = (key: keyof FrameSettings, value: string | undefined) => {
setQrProps((prevProps) => ({
...prevProps,
frameSettings: {
...(prevProps.frameSettings ?? defaultFrameSettings),
[key]: value,
} as FrameSettings,
}))
}

const onIncludeFrameChange = (checked: boolean) => {
setIncludeFrame(checked)
setQrProps((prevProps) => ({
...prevProps,
frameSettings: checked ? defaultFrameSettings : undefined,
}))
}

const onCustomTextColorChange = (checked: boolean) => {
setCustomTextColor(checked)
onValueChange('textColor', checked ? '#FFFFFF' : undefined)
}

return (
<>
<FormCheckbox
label='Include frame'
checked={includeFrame}
onCheckedChange={(checked) => onIncludeFrameChange(checked as boolean)}
/>
<FormField label='Style'>
<div className='flex flex-wrap gap-2'>
{styles.map((style) => (
<Button
key={style}
size='sm'
disabled={!includeFrame}
variant={qrProps.frameSettings?.style === style ? 'default' : 'outline'}
onClick={() => onValueChange('style', style)}
>
{style}
</Button>
))}
</div>
</FormField>
<FormField label='Text'>
<Input
disabled={!includeFrame}
type='text'
placeholder='Scan me!'
value={qrProps.frameSettings?.text ?? ''}
onChange={(e) => onValueChange('text', e.target.value)}
/>
</FormField>
<FormField label='Frame color'>
<ColorPicker
disabled={!includeFrame}
defaultColor={qrProps.frameSettings?.color}
onChange={(value) => onValueChange('color', value)}
/>
</FormField>
<FormCheckbox
disabled={!includeFrame}
label='Custom text color'
checked={customTextColor}
onCheckedChange={(checked) => onCustomTextColorChange(checked as boolean)}
/>
<FormField label='Text color'>
<ColorPicker
disabled={!includeFrame || !customTextColor}
defaultColor={qrProps.frameSettings?.textColor ?? '#FFFFFF'}
onChange={(value) => onValueChange('textColor', value)}
/>
</FormField>
</>
)
}
Loading
Loading