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/feat-data-modules-size.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@lglab/react-qr-code': minor
---

Add `size` option to `dataModulesSettings`. Sets a fixed module size multiplier (1 = full size) for the fillable data module styles (square, pinched-square, circle, diamond, heart, star, hashtag). Ignored when `randomSize` is true.
1 change: 1 addition & 0 deletions apps/docs/public/llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ The main component exported by the library.
| `color` | `string` | - | Color of the data modules (overridden by `gradient`). |
| `style` | `DataModulesStyle` | `'square'` | Shape of the data modules. |
| `randomSize` | `boolean` | `false` | If true, modules will have slightly varied sizes. |
| `size` | `number` | `1` | Fixed module size multiplier (1 = full size). Keep between 0.75 and 1 for best results — lower values may degrade scannability. Only applies to fillable styles (square, pinched-square, circle, diamond, heart, star, hashtag). Ignored when `randomSize` is true. |

**Available Styles (`DataModulesStyle`):**
`'square'`, `'square-sm'`, `'pinched-square'`, `'rounded'`, `'leaf'`, `'vertical-line'`, `'horizontal-line'`, `'circuit-board'`, `'circle'`, `'diamond'`, `'star'`, `'heart'`, `'hashtag'`
Expand Down
20 changes: 18 additions & 2 deletions apps/docs/src/app/data-modules-settings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,29 @@ const props: Prop[] = [
description: (
<>
If true, the modules will have random sizes. Can only be used with styles{' '}
<Bold>square</Bold>, <Bold>circle</Bold>, <Bold>diamond</Bold>, <Bold>star</Bold>,{' '}
<Bold>heart</Bold> and <Bold>hashtag</Bold>.
<Bold>square</Bold>, <Bold>pinched-square</Bold>, <Bold>circle</Bold>,{' '}
<Bold>diamond</Bold>, <Bold>star</Bold>, <Bold>heart</Bold> and{' '}
<Bold>hashtag</Bold>.
</>
),
defaultValue: 'false',
possibleValues: ['true', 'false'],
},
{
name: 'size',
type: 'number',
description: (
<>
Fixed size multiplier applied to each data module (1 = full size). Keep between{' '}
<Bold>0.75</Bold> and <Bold>1</Bold> for best results — lower values may degrade
scannability. Only applies to styles <Bold>square</Bold>,{' '}
<Bold>pinched-square</Bold>, <Bold>circle</Bold>, <Bold>diamond</Bold>,{' '}
<Bold>star</Bold>, <Bold>heart</Bold> and <Bold>hashtag</Bold>. Ignored when{' '}
<Bold>randomSize</Bold> is true.
</>
),
defaultValue: '1',
},
]

export default function Page() {
Expand Down
39 changes: 32 additions & 7 deletions apps/docs/src/components/demo/data-modules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { type DataModulesStyle, type ReactQRCodeProps } from '@lglab/react-qr-co
import { type Dispatch } from 'react'

import { FormCheckbox, FormField } from '@/components/ui/form-fields'
import { Slider } from '@/components/ui/slider'

import { Button } from '../ui/button'

Expand Down Expand Up @@ -57,6 +58,9 @@ export const DataModules = ({ qrProps, setQrProps }: DataModulesProps) => {
'hashtag',
].includes(qrProps.dataModulesSettings?.style ?? '')

const size = qrProps.dataModulesSettings?.size ?? 1
const randomSize = qrProps.dataModulesSettings?.randomSize ?? false

return (
<>
<FormField label='Style'>
Expand All @@ -76,13 +80,34 @@ export const DataModules = ({ qrProps, setQrProps }: DataModulesProps) => {
</div>
</FormField>
{canBeRandomSize && (
<FormCheckbox
label='Random size'
checked={qrProps.dataModulesSettings?.randomSize}
onCheckedChange={(checked) =>
onCheckboxChange(checked as boolean, 'randomSize')
}
/>
<>
<FormCheckbox
label='Random size'
checked={randomSize}
onCheckedChange={(checked) =>
onCheckboxChange(checked as boolean, 'randomSize')
}
/>
<FormField label={`Size (${size.toFixed(2)})`}>
<Slider
value={[size]}
onValueChange={([value]) =>
setQrProps((prevProps) => ({
...prevProps,
dataModulesSettings: {
...prevProps.dataModulesSettings,
size: value,
},
}))
}
min={0.75}
max={1}
step={0.01}
disabled={randomSize}
className={randomSize ? 'opacity-50' : undefined}
/>
</FormField>
</>
)}
</>
)
Expand Down
6 changes: 3 additions & 3 deletions packages/react-qr-code/src/components/data-modules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const DataModules = ({
gradient,
gradientId,
}: DataModulesProps): ReactNode => {
const { color, style, randomSize } = useMemo(
const { color, style, randomSize, size } = useMemo(
() => sanitizeDataModulesSettings(settings),
[settings],
)
Expand All @@ -51,8 +51,8 @@ export const DataModules = ({
const isRandom = dataModuleCanBeRandomSize(style) && randomSize

const scaleFactor = useCallback(
() => getScaleFactor(style, isRandom),
[style, isRandom],
() => getScaleFactor(style, isRandom, size),
[style, isRandom, size],
)

modules.forEach((row, y) => {
Expand Down
8 changes: 8 additions & 0 deletions packages/react-qr-code/src/types/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,14 @@ export interface DataModulesSettings {
color?: string
style?: DataModulesStyle
randomSize?: boolean
/**
* Fixed size multiplier applied to each data module (1 = full size). Keep
* between 0.75 and 1 for best results — lower values may degrade
* scannability. Only applies to fillable styles (square, pinched-square,
* circle, diamond, heart, star, hashtag). Ignored when `randomSize` is true.
* @defaultValue 1
*/
size?: number
}

export type FinderPatternOuterStyle =
Expand Down
18 changes: 18 additions & 0 deletions packages/react-qr-code/src/utils/data-modules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,24 @@ describe('getScaleFactor', () => {
const scaleFactor = getScaleFactor('circle', true)
expect(scaleFactor).toEqual(0.9 * (1 - 0.75) + 0.75)
})

it('returns the provided size for fillable styles when randomSize is false', () => {
expect(getScaleFactor('circle', false, 0.8)).toBe(0.8)
expect(getScaleFactor('square', false, 0.9)).toBe(0.9)
})

it('ignores size for styles that cannot be scaled', () => {
expect(getScaleFactor('rounded', false, 0.8)).toBe(1)
expect(getScaleFactor('leaf', false, 0.8)).toBe(1)
expect(getScaleFactor('vertical-line', false, 0.8)).toBe(1)
expect(getScaleFactor('horizontal-line', false, 0.8)).toBe(1)
expect(getScaleFactor('circuit-board', false, 0.8)).toBe(1)
})

it('ignores size when randomSize is true', () => {
vi.spyOn(Math, 'random').mockReturnValue(0.5)
expect(getScaleFactor('circle', true, 0.8)).toEqual(0.5 * (1 - 0.75) + 0.75)
})
})

describe('dataModuleCanBeRandomSize', () => {
Expand Down
4 changes: 3 additions & 1 deletion packages/react-qr-code/src/utils/data-modules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ export const dataModuleCanBeRandomSize = (style: DataModulesStyle): boolean =>
style === 'diamond' ||
style === 'hashtag'

export const getScaleFactor = (style: string, isRandom: boolean) => {
export const getScaleFactor = (style: string, isRandom: boolean, size = 1) => {
if (style === 'square-sm') {
return 0.75
} else if (isRandom) {
return Math.random() * (1 - 0.75) + 0.75
} else if (dataModuleCanBeRandomSize(style as DataModulesStyle)) {
return size
}
return 1
}
Expand Down
1 change: 1 addition & 0 deletions packages/react-qr-code/src/utils/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export const sanitizeDataModulesSettings = (settings?: DataModulesSettings) => {
color: settings?.color || DEFAULT_DATA_MODULES_COLOR,
style: settings?.style || DEFAULT_DATA_MODULES_STYLE,
randomSize: settings?.randomSize || false,
size: settings?.size ?? 1,
}
}

Expand Down