diff --git a/.changeset/feat-data-modules-size.md b/.changeset/feat-data-modules-size.md new file mode 100644 index 00000000..8b85e79e --- /dev/null +++ b/.changeset/feat-data-modules-size.md @@ -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. diff --git a/apps/docs/public/llms-full.txt b/apps/docs/public/llms-full.txt index 666cead6..7f2fbd94 100644 --- a/apps/docs/public/llms-full.txt +++ b/apps/docs/public/llms-full.txt @@ -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'` diff --git a/apps/docs/src/app/data-modules-settings/page.tsx b/apps/docs/src/app/data-modules-settings/page.tsx index 66cb122a..cee198e3 100644 --- a/apps/docs/src/app/data-modules-settings/page.tsx +++ b/apps/docs/src/app/data-modules-settings/page.tsx @@ -45,13 +45,29 @@ const props: Prop[] = [ description: ( <> If true, the modules will have random sizes. Can only be used with styles{' '} - square, circle, diamond, star,{' '} - heart and hashtag. + square, pinched-square, circle,{' '} + diamond, star, heart and{' '} + hashtag. ), defaultValue: 'false', possibleValues: ['true', 'false'], }, + { + name: 'size', + type: 'number', + description: ( + <> + 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 styles square,{' '} + pinched-square, circle, diamond,{' '} + star, heart and hashtag. Ignored when{' '} + randomSize is true. + + ), + defaultValue: '1', + }, ] export default function Page() { diff --git a/apps/docs/src/components/demo/data-modules.tsx b/apps/docs/src/components/demo/data-modules.tsx index 0c66a0cf..5281879d 100644 --- a/apps/docs/src/components/demo/data-modules.tsx +++ b/apps/docs/src/components/demo/data-modules.tsx @@ -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' @@ -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 ( <> @@ -76,13 +80,34 @@ export const DataModules = ({ qrProps, setQrProps }: DataModulesProps) => { {canBeRandomSize && ( - - onCheckboxChange(checked as boolean, 'randomSize') - } - /> + <> + + onCheckboxChange(checked as boolean, 'randomSize') + } + /> + + + setQrProps((prevProps) => ({ + ...prevProps, + dataModulesSettings: { + ...prevProps.dataModulesSettings, + size: value, + }, + })) + } + min={0.75} + max={1} + step={0.01} + disabled={randomSize} + className={randomSize ? 'opacity-50' : undefined} + /> + + )} ) diff --git a/packages/react-qr-code/src/components/data-modules.tsx b/packages/react-qr-code/src/components/data-modules.tsx index 10b715db..56d9c23b 100644 --- a/packages/react-qr-code/src/components/data-modules.tsx +++ b/packages/react-qr-code/src/components/data-modules.tsx @@ -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], ) @@ -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) => { diff --git a/packages/react-qr-code/src/types/lib.ts b/packages/react-qr-code/src/types/lib.ts index 389e4660..d2291290 100644 --- a/packages/react-qr-code/src/types/lib.ts +++ b/packages/react-qr-code/src/types/lib.ts @@ -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 = diff --git a/packages/react-qr-code/src/utils/data-modules.test.ts b/packages/react-qr-code/src/utils/data-modules.test.ts index facb457e..72d9cef5 100644 --- a/packages/react-qr-code/src/utils/data-modules.test.ts +++ b/packages/react-qr-code/src/utils/data-modules.test.ts @@ -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', () => { diff --git a/packages/react-qr-code/src/utils/data-modules.ts b/packages/react-qr-code/src/utils/data-modules.ts index 1c185608..c3fb1877 100644 --- a/packages/react-qr-code/src/utils/data-modules.ts +++ b/packages/react-qr-code/src/utils/data-modules.ts @@ -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 } diff --git a/packages/react-qr-code/src/utils/settings.ts b/packages/react-qr-code/src/utils/settings.ts index d52de2bb..84753a03 100644 --- a/packages/react-qr-code/src/utils/settings.ts +++ b/packages/react-qr-code/src/utils/settings.ts @@ -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, } }