react-id-card-generator 1.0.7 → 1.0.9
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.
- package/dist/cjs/components/IDCardDesigner.js +103 -22
- package/dist/cjs/components/IDCardDesigner.js.map +1 -1
- package/dist/cjs/components/IDCardPreview.js +36 -4
- package/dist/cjs/components/IDCardPreview.js.map +1 -1
- package/dist/cjs/utils/styleUtils.js +2 -18
- package/dist/cjs/utils/styleUtils.js.map +1 -1
- package/dist/esm/components/IDCardDesigner.js +104 -23
- package/dist/esm/components/IDCardDesigner.js.map +1 -1
- package/dist/esm/components/IDCardPreview.js +36 -4
- package/dist/esm/components/IDCardPreview.js.map +1 -1
- package/dist/esm/utils/styleUtils.js +2 -18
- package/dist/esm/utils/styleUtils.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/types/types/index.d.ts +6 -4
- package/package.json +1 -1
|
@@ -54,6 +54,17 @@ const IDCardPreview = ({ template, data, side, scale = 1, showGrid = false, onFi
|
|
|
54
54
|
const posStyle = styleUtils.positionToStyle(field.position);
|
|
55
55
|
const fieldStyle = styleUtils.fieldStyleToCSS(field.style);
|
|
56
56
|
const isSelected = field.id === selectedFieldId;
|
|
57
|
+
// Determine vertical alignment and label position for text fields
|
|
58
|
+
let verticalJustify = 'flex-start';
|
|
59
|
+
let flexDirection = 'column';
|
|
60
|
+
if (field.type === 'text') {
|
|
61
|
+
if (field.style?.verticalAlign === 'middle')
|
|
62
|
+
verticalJustify = 'center';
|
|
63
|
+
else if (field.style?.verticalAlign === 'bottom')
|
|
64
|
+
verticalJustify = 'flex-end';
|
|
65
|
+
if (field.style?.labelPosition === 'left')
|
|
66
|
+
flexDirection = 'row';
|
|
67
|
+
}
|
|
57
68
|
const combinedStyle = {
|
|
58
69
|
...posStyle,
|
|
59
70
|
...fieldStyle,
|
|
@@ -63,6 +74,9 @@ const IDCardPreview = ({ template, data, side, scale = 1, showGrid = false, onFi
|
|
|
63
74
|
cursor: editable ? 'move' : 'default',
|
|
64
75
|
outline: isSelected ? '2px solid #2196F3' : 'none',
|
|
65
76
|
outlineOffset: '1px',
|
|
77
|
+
display: field.type === 'text' ? 'flex' : undefined,
|
|
78
|
+
flexDirection: field.type === 'text' ? flexDirection : undefined,
|
|
79
|
+
justifyContent: field.type === 'text' ? verticalJustify : undefined,
|
|
66
80
|
};
|
|
67
81
|
const handleClick = (e) => {
|
|
68
82
|
e.stopPropagation();
|
|
@@ -71,8 +85,21 @@ const IDCardPreview = ({ template, data, side, scale = 1, showGrid = false, onFi
|
|
|
71
85
|
const value = data[field.fieldKey];
|
|
72
86
|
const content = (() => {
|
|
73
87
|
switch (field.type) {
|
|
74
|
-
case 'text':
|
|
75
|
-
|
|
88
|
+
case 'text': {
|
|
89
|
+
// Determine label vertical alignment
|
|
90
|
+
let labelAlignSelf = 'flex-start';
|
|
91
|
+
if (field.style?.labelVerticalAlign === 'middle')
|
|
92
|
+
labelAlignSelf = 'center';
|
|
93
|
+
else if (field.style?.labelVerticalAlign === 'bottom')
|
|
94
|
+
labelAlignSelf = 'flex-end';
|
|
95
|
+
const labelFontSize = field.style?.labelFontSize || '0.75em';
|
|
96
|
+
const labelFontWeight = field.style?.labelFontWeight || 'normal';
|
|
97
|
+
const labelColor = field.style?.labelColor || '#666666';
|
|
98
|
+
const labelStyle = field.style?.labelPosition === 'left'
|
|
99
|
+
? { fontSize: labelFontSize, fontWeight: labelFontWeight, color: labelColor, opacity: 0.7, marginRight: 8, alignSelf: labelAlignSelf, whiteSpace: 'nowrap' }
|
|
100
|
+
: { fontSize: labelFontSize, fontWeight: labelFontWeight, color: labelColor, display: 'block', opacity: 0.7, alignSelf: labelAlignSelf };
|
|
101
|
+
return (jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [field.label && (jsxRuntime.jsx("span", { style: labelStyle, children: field.label })), jsxRuntime.jsx("span", { children: String(value || field.placeholder || field.fieldKey) })] }));
|
|
102
|
+
}
|
|
76
103
|
case 'label':
|
|
77
104
|
return jsxRuntime.jsx("span", { children: field.staticText || field.label || 'Label' });
|
|
78
105
|
case 'image':
|
|
@@ -93,10 +120,15 @@ const IDCardPreview = ({ template, data, side, scale = 1, showGrid = false, onFi
|
|
|
93
120
|
flexDirection: 'column',
|
|
94
121
|
}, children: [jsxRuntime.jsx("span", { children: "\uD83D\uDCF7" }), jsxRuntime.jsx("span", { children: field.label || 'Photo' })] }));
|
|
95
122
|
case 'qrcode':
|
|
123
|
+
// Use the field's rendered width/height (in px) for QR code size
|
|
124
|
+
const qrWidth = Math.max(32, Math.round(field.position.width));
|
|
125
|
+
const qrHeight = Math.max(32, Math.round(field.position.height));
|
|
126
|
+
// Always generate QR code at the field's current size for smooth scaling
|
|
127
|
+
const qrSize = Math.max(qrWidth, qrHeight);
|
|
96
128
|
const qrDataUrl = field.qrFields
|
|
97
|
-
? qrUtils.generateQrCodeFromFields(data, field.qrFields,
|
|
129
|
+
? qrUtils.generateQrCodeFromFields(data, field.qrFields, qrSize)
|
|
98
130
|
: '';
|
|
99
|
-
return qrDataUrl ? (jsxRuntime.jsx("img", { src: qrDataUrl, alt: "QR Code", style: { width: '100%', height: '100%', objectFit: 'contain' } })) : (jsxRuntime.jsx("div", { style: {
|
|
131
|
+
return qrDataUrl ? (jsxRuntime.jsx("img", { src: qrDataUrl, alt: "QR Code", style: { width: '100%', height: '100%', objectFit: 'contain', display: 'block', pointerEvents: 'none' } })) : (jsxRuntime.jsx("div", { style: {
|
|
100
132
|
width: '100%',
|
|
101
133
|
height: '100%',
|
|
102
134
|
backgroundColor: '#f0f0f0',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IDCardPreview.js","sources":["../../../src/components/IDCardPreview.tsx"],"sourcesContent":["import React, { useMemo } from 'react';\r\nimport type { IDCardPreviewProps, FieldMapping } from '../types';\r\nimport { fieldStyleToCSS, positionToStyle } from '../utils/styleUtils';\r\nimport { generateQrCodeFromFields } from '../core/qrUtils';\r\n\r\n/**\r\n * IDCardPreview - Interactive card preview component\r\n * \r\n * Renders an ID card with:\r\n * - Field selection and highlighting\r\n * - Resize handles for selected fields\r\n * - Grid overlay for alignment\r\n * - Dynamic field rendering based on type\r\n * \r\n * Used inside the designer for visual editing\r\n */\r\nexport const IDCardPreview: React.FC<IDCardPreviewProps> = ({\r\n template,\r\n data,\r\n side,\r\n scale = 1,\r\n showGrid = false,\r\n onFieldSelect,\r\n selectedFieldId,\r\n editable = true,\r\n onFieldUpdate,\r\n}: IDCardPreviewProps) => {\r\n // Get only fields that belong to the current side\r\n const fieldsToRender = useMemo(() => {\r\n return template.fields.filter((field: FieldMapping) => field.side === side);\r\n }, [template.fields, side]);\r\n\r\n // Get background for current side\r\n const backgroundImage = side === 'front' ? template.backgrounds.front : template.backgrounds.back;\r\n\r\n // Render resize handles for selected field\r\n const renderResizeHandles = (field: FieldMapping) => {\r\n if (field.id !== selectedFieldId || !editable) return null;\r\n\r\n const handles = ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'];\r\n const handleStyle: React.CSSProperties = {\r\n position: 'absolute',\r\n width: '8px',\r\n height: '8px',\r\n backgroundColor: '#2196F3',\r\n border: '1px solid #fff',\r\n borderRadius: '2px',\r\n zIndex: 1000,\r\n };\r\n\r\n const positions: Record<string, React.CSSProperties> = {\r\n n: { top: '-4px', left: '50%', transform: 'translateX(-50%)', cursor: 'n-resize' },\r\n s: { bottom: '-4px', left: '50%', transform: 'translateX(-50%)', cursor: 's-resize' },\r\n e: { right: '-4px', top: '50%', transform: 'translateY(-50%)', cursor: 'e-resize' },\r\n w: { left: '-4px', top: '50%', transform: 'translateY(-50%)', cursor: 'w-resize' },\r\n ne: { top: '-4px', right: '-4px', cursor: 'ne-resize' },\r\n nw: { top: '-4px', left: '-4px', cursor: 'nw-resize' },\r\n se: { bottom: '-4px', right: '-4px', cursor: 'se-resize' },\r\n sw: { bottom: '-4px', left: '-4px', cursor: 'sw-resize' },\r\n };\r\n\r\n return handles.map((handle) => (\r\n <div\r\n key={handle}\r\n className={`resize-handle resize-handle-${handle}`}\r\n style={{ ...handleStyle, ...positions[handle] }}\r\n data-handle={handle}\r\n />\r\n ));\r\n };\r\n\r\n // Render a single field\r\n const renderField = (field: FieldMapping) => {\r\n const posStyle = positionToStyle(field.position);\r\n const fieldStyle = fieldStyleToCSS(field.style);\r\n const isSelected = field.id === selectedFieldId;\r\n\r\n const combinedStyle: React.CSSProperties = {\r\n ...posStyle,\r\n ...fieldStyle,\r\n zIndex: field.zIndex || 1,\r\n boxSizing: 'border-box',\r\n overflow: 'hidden',\r\n cursor: editable ? 'move' : 'default',\r\n outline: isSelected ? '2px solid #2196F3' : 'none',\r\n outlineOffset: '1px',\r\n };\r\n\r\n const handleClick = (e: React.MouseEvent) => {\r\n e.stopPropagation();\r\n onFieldSelect?.(field);\r\n };\r\n\r\n const value = data[field.fieldKey];\r\n\r\n const content = (() => {\r\n switch (field.type) {\r\n case 'text':\r\n return (\r\n <>\r\n {field.label && (\r\n <span style={{ fontSize: '0.75em', display: 'block', opacity: 0.7 }}>\r\n {field.label}\r\n </span>\r\n )}\r\n <span>{String(value || field.placeholder || field.fieldKey)}</span>\r\n </>\r\n );\r\n\r\n case 'label':\r\n return <span>{field.staticText || field.label || 'Label'}</span>;\r\n\r\n case 'image':\r\n return value ? (\r\n <img\r\n src={String(value)}\r\n alt={field.label || 'Image'}\r\n style={{\r\n width: '100%',\r\n height: '100%',\r\n objectFit: 'cover',\r\n objectPosition: 'center top',\r\n }}\r\n />\r\n ) : (\r\n <div\r\n style={{\r\n width: '100%',\r\n height: '100%',\r\n backgroundColor: '#e0e0e0',\r\n display: 'flex',\r\n alignItems: 'center',\r\n justifyContent: 'center',\r\n fontSize: '10px',\r\n color: '#666',\r\n flexDirection: 'column',\r\n }}\r\n >\r\n <span>📷</span>\r\n <span>{field.label || 'Photo'}</span>\r\n </div>\r\n );\r\n\r\n case 'qrcode':\r\n const qrDataUrl = field.qrFields\r\n ? generateQrCodeFromFields(data, field.qrFields, 200)\r\n : '';\r\n\r\n return qrDataUrl ? (\r\n <img\r\n src={qrDataUrl}\r\n alt=\"QR Code\"\r\n style={{ width: '100%', height: '100%', objectFit: 'contain' }}\r\n />\r\n ) : (\r\n <div\r\n style={{\r\n width: '100%',\r\n height: '100%',\r\n backgroundColor: '#f0f0f0',\r\n display: 'flex',\r\n alignItems: 'center',\r\n justifyContent: 'center',\r\n fontSize: '10px',\r\n color: '#666',\r\n border: '1px dashed #ccc',\r\n }}\r\n >\r\n QR Code\r\n </div>\r\n );\r\n\r\n default:\r\n return null;\r\n }\r\n })();\r\n\r\n return (\r\n <div\r\n key={field.id}\r\n id={`preview-${field.id}`}\r\n style={combinedStyle}\r\n onClick={handleClick}\r\n className={`preview-field preview-field-${field.type} ${isSelected ? 'selected' : ''}`}\r\n >\r\n {content}\r\n {renderResizeHandles(field)}\r\n </div>\r\n );\r\n };\r\n\r\n // Card container style\r\n const cardStyle: React.CSSProperties = {\r\n width: template.cardSize.width,\r\n height: template.cardSize.height,\r\n position: 'relative',\r\n overflow: 'hidden',\r\n backgroundColor: backgroundImage ? 'transparent' : '#f5f5f5',\r\n border: '1px solid #ddd',\r\n transform: `scale(${scale})`,\r\n transformOrigin: 'top left',\r\n };\r\n\r\n // Grid overlay style\r\n const gridStyle: React.CSSProperties = {\r\n position: 'absolute',\r\n top: 0,\r\n left: 0,\r\n width: '100%',\r\n height: '100%',\r\n backgroundImage: showGrid\r\n ? 'linear-gradient(to right, rgba(0,0,0,0.05) 1px, transparent 1px), linear-gradient(to bottom, rgba(0,0,0,0.05) 1px, transparent 1px)'\r\n : 'none',\r\n backgroundSize: '10px 10px',\r\n pointerEvents: 'none',\r\n zIndex: 999,\r\n };\r\n\r\n const handleContainerClick = () => {\r\n onFieldSelect?.(null);\r\n };\r\n\r\n return (\r\n <div\r\n className=\"idcard-preview-container\"\r\n style={cardStyle}\r\n onClick={handleContainerClick}\r\n >\r\n {/* Background */}\r\n {backgroundImage && (\r\n <img\r\n src={backgroundImage}\r\n alt={`${side} background`}\r\n style={{\r\n position: 'absolute',\r\n top: 0,\r\n left: 0,\r\n width: '100%',\r\n height: '100%',\r\n objectFit: 'cover',\r\n zIndex: 0,\r\n pointerEvents: 'none',\r\n }}\r\n />\r\n )}\r\n\r\n {/* Fields */}\r\n {fieldsToRender.map(renderField)}\r\n\r\n {/* Grid overlay */}\r\n {showGrid && <div style={gridStyle} />}\r\n\r\n {/* Empty state */}\r\n {!backgroundImage && fieldsToRender.length === 0 && (\r\n <div\r\n style={{\r\n position: 'absolute',\r\n top: '50%',\r\n left: '50%',\r\n transform: 'translate(-50%, -50%)',\r\n textAlign: 'center',\r\n color: '#999',\r\n fontSize: '14px',\r\n }}\r\n >\r\n <p>Upload a background image</p>\r\n <p>and add fields to get started</p>\r\n </div>\r\n )}\r\n </div>\r\n );\r\n};\r\n\r\n"],"names":["useMemo","_jsx","positionToStyle","fieldStyleToCSS","_jsxs","generateQrCodeFromFields"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"IDCardPreview.js","sources":["../../../src/components/IDCardPreview.tsx"],"sourcesContent":["import React, { useMemo } from 'react';\r\nimport type { IDCardPreviewProps, FieldMapping } from '../types';\r\nimport { fieldStyleToCSS, positionToStyle } from '../utils/styleUtils';\r\nimport { generateQrCodeFromFields } from '../core/qrUtils';\r\n\r\n/**\r\n * IDCardPreview - Interactive card preview component\r\n * \r\n * Renders an ID card with:\r\n * - Field selection and highlighting\r\n * - Resize handles for selected fields\r\n * - Grid overlay for alignment\r\n * - Dynamic field rendering based on type\r\n * \r\n * Used inside the designer for visual editing\r\n */\r\nexport const IDCardPreview: React.FC<IDCardPreviewProps> = ({\r\n template,\r\n data,\r\n side,\r\n scale = 1,\r\n showGrid = false,\r\n onFieldSelect,\r\n selectedFieldId,\r\n editable = true,\r\n onFieldUpdate,\r\n}: IDCardPreviewProps) => {\r\n // Get only fields that belong to the current side\r\n const fieldsToRender = useMemo(() => {\r\n return template.fields.filter((field: FieldMapping) => field.side === side);\r\n }, [template.fields, side]);\r\n\r\n // Get background for current side\r\n const backgroundImage = side === 'front' ? template.backgrounds.front : template.backgrounds.back;\r\n\r\n // Render resize handles for selected field\r\n const renderResizeHandles = (field: FieldMapping) => {\r\n if (field.id !== selectedFieldId || !editable) return null;\r\n\r\n const handles = ['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw'];\r\n const handleStyle: React.CSSProperties = {\r\n position: 'absolute',\r\n width: '8px',\r\n height: '8px',\r\n backgroundColor: '#2196F3',\r\n border: '1px solid #fff',\r\n borderRadius: '2px',\r\n zIndex: 1000,\r\n };\r\n\r\n const positions: Record<string, React.CSSProperties> = {\r\n n: { top: '-4px', left: '50%', transform: 'translateX(-50%)', cursor: 'n-resize' },\r\n s: { bottom: '-4px', left: '50%', transform: 'translateX(-50%)', cursor: 's-resize' },\r\n e: { right: '-4px', top: '50%', transform: 'translateY(-50%)', cursor: 'e-resize' },\r\n w: { left: '-4px', top: '50%', transform: 'translateY(-50%)', cursor: 'w-resize' },\r\n ne: { top: '-4px', right: '-4px', cursor: 'ne-resize' },\r\n nw: { top: '-4px', left: '-4px', cursor: 'nw-resize' },\r\n se: { bottom: '-4px', right: '-4px', cursor: 'se-resize' },\r\n sw: { bottom: '-4px', left: '-4px', cursor: 'sw-resize' },\r\n };\r\n\r\n return handles.map((handle) => (\r\n <div\r\n key={handle}\r\n className={`resize-handle resize-handle-${handle}`}\r\n style={{ ...handleStyle, ...positions[handle] }}\r\n data-handle={handle}\r\n />\r\n ));\r\n };\r\n\r\n // Render a single field\r\n const renderField = (field: FieldMapping) => {\r\n const posStyle = positionToStyle(field.position);\r\n const fieldStyle = fieldStyleToCSS(field.style);\r\n const isSelected = field.id === selectedFieldId;\r\n\r\n // Determine vertical alignment and label position for text fields\r\n let verticalJustify: 'flex-start' | 'center' | 'flex-end' = 'flex-start';\r\n let flexDirection: 'column' | 'row' = 'column';\r\n if (field.type === 'text') {\r\n if (field.style?.verticalAlign === 'middle') verticalJustify = 'center';\r\n else if (field.style?.verticalAlign === 'bottom') verticalJustify = 'flex-end';\r\n if (field.style?.labelPosition === 'left') flexDirection = 'row';\r\n }\r\n\r\n const combinedStyle: React.CSSProperties = {\r\n ...posStyle,\r\n ...fieldStyle,\r\n zIndex: field.zIndex || 1,\r\n boxSizing: 'border-box',\r\n overflow: 'hidden',\r\n cursor: editable ? 'move' : 'default',\r\n outline: isSelected ? '2px solid #2196F3' : 'none',\r\n outlineOffset: '1px',\r\n display: field.type === 'text' ? 'flex' : undefined,\r\n flexDirection: field.type === 'text' ? flexDirection : undefined,\r\n justifyContent: field.type === 'text' ? verticalJustify : undefined,\r\n };\r\n\r\n const handleClick = (e: React.MouseEvent) => {\r\n e.stopPropagation();\r\n onFieldSelect?.(field);\r\n };\r\n\r\n const value = data[field.fieldKey];\r\n\r\n const content = (() => {\r\n switch (field.type) {\r\n case 'text': {\r\n // Determine label vertical alignment\r\n let labelAlignSelf: React.CSSProperties['alignSelf'] = 'flex-start';\r\n if (field.style?.labelVerticalAlign === 'middle') labelAlignSelf = 'center';\r\n else if (field.style?.labelVerticalAlign === 'bottom') labelAlignSelf = 'flex-end';\r\n const labelFontSize = field.style?.labelFontSize || '0.75em';\r\n const labelFontWeight = field.style?.labelFontWeight || 'normal';\r\n const labelColor = field.style?.labelColor || '#666666';\r\n const labelStyle: React.CSSProperties = field.style?.labelPosition === 'left'\r\n ? { fontSize: labelFontSize, fontWeight: labelFontWeight, color: labelColor, opacity: 0.7, marginRight: 8, alignSelf: labelAlignSelf, whiteSpace: 'nowrap' }\r\n : { fontSize: labelFontSize, fontWeight: labelFontWeight, color: labelColor, display: 'block', opacity: 0.7, alignSelf: labelAlignSelf };\r\n return (\r\n <>\r\n {field.label && (\r\n <span style={labelStyle}>\r\n {field.label}\r\n </span>\r\n )}\r\n <span>{String(value || field.placeholder || field.fieldKey)}</span>\r\n </>\r\n );\r\n }\r\n\r\n case 'label':\r\n return <span>{field.staticText || field.label || 'Label'}</span>;\r\n\r\n case 'image':\r\n return value ? (\r\n <img\r\n src={String(value)}\r\n alt={field.label || 'Image'}\r\n style={{\r\n width: '100%',\r\n height: '100%',\r\n objectFit: 'cover',\r\n objectPosition: 'center top',\r\n }}\r\n />\r\n ) : (\r\n <div\r\n style={{\r\n width: '100%',\r\n height: '100%',\r\n backgroundColor: '#e0e0e0',\r\n display: 'flex',\r\n alignItems: 'center',\r\n justifyContent: 'center',\r\n fontSize: '10px',\r\n color: '#666',\r\n flexDirection: 'column',\r\n }}\r\n >\r\n <span>📷</span>\r\n <span>{field.label || 'Photo'}</span>\r\n </div>\r\n );\r\n\r\n case 'qrcode':\r\n // Use the field's rendered width/height (in px) for QR code size\r\n const qrWidth = Math.max(32, Math.round(field.position.width));\r\n const qrHeight = Math.max(32, Math.round(field.position.height));\r\n // Always generate QR code at the field's current size for smooth scaling\r\n const qrSize = Math.max(qrWidth, qrHeight);\r\n const qrDataUrl = field.qrFields\r\n ? generateQrCodeFromFields(data, field.qrFields, qrSize)\r\n : '';\r\n\r\n return qrDataUrl ? (\r\n <img\r\n src={qrDataUrl}\r\n alt=\"QR Code\"\r\n style={{ width: '100%', height: '100%', objectFit: 'contain', display: 'block', pointerEvents: 'none' }}\r\n />\r\n ) : (\r\n <div\r\n style={{\r\n width: '100%',\r\n height: '100%',\r\n backgroundColor: '#f0f0f0',\r\n display: 'flex',\r\n alignItems: 'center',\r\n justifyContent: 'center',\r\n fontSize: '10px',\r\n color: '#666',\r\n border: '1px dashed #ccc',\r\n }}\r\n >\r\n QR Code\r\n </div>\r\n );\r\n\r\n default:\r\n return null;\r\n }\r\n })();\r\n\r\n return (\r\n <div\r\n key={field.id}\r\n id={`preview-${field.id}`}\r\n style={combinedStyle}\r\n onClick={handleClick}\r\n className={`preview-field preview-field-${field.type} ${isSelected ? 'selected' : ''}`}\r\n >\r\n {content}\r\n {renderResizeHandles(field)}\r\n </div>\r\n );\r\n };\r\n\r\n // Card container style\r\n const cardStyle: React.CSSProperties = {\r\n width: template.cardSize.width,\r\n height: template.cardSize.height,\r\n position: 'relative',\r\n overflow: 'hidden',\r\n backgroundColor: backgroundImage ? 'transparent' : '#f5f5f5',\r\n border: '1px solid #ddd',\r\n transform: `scale(${scale})`,\r\n transformOrigin: 'top left',\r\n };\r\n\r\n // Grid overlay style\r\n const gridStyle: React.CSSProperties = {\r\n position: 'absolute',\r\n top: 0,\r\n left: 0,\r\n width: '100%',\r\n height: '100%',\r\n backgroundImage: showGrid\r\n ? 'linear-gradient(to right, rgba(0,0,0,0.05) 1px, transparent 1px), linear-gradient(to bottom, rgba(0,0,0,0.05) 1px, transparent 1px)'\r\n : 'none',\r\n backgroundSize: '10px 10px',\r\n pointerEvents: 'none',\r\n zIndex: 999,\r\n };\r\n\r\n const handleContainerClick = () => {\r\n onFieldSelect?.(null);\r\n };\r\n\r\n return (\r\n <div\r\n className=\"idcard-preview-container\"\r\n style={cardStyle}\r\n onClick={handleContainerClick}\r\n >\r\n {/* Background */}\r\n {backgroundImage && (\r\n <img\r\n src={backgroundImage}\r\n alt={`${side} background`}\r\n style={{\r\n position: 'absolute',\r\n top: 0,\r\n left: 0,\r\n width: '100%',\r\n height: '100%',\r\n objectFit: 'cover',\r\n zIndex: 0,\r\n pointerEvents: 'none',\r\n }}\r\n />\r\n )}\r\n\r\n {/* Fields */}\r\n {fieldsToRender.map(renderField)}\r\n\r\n {/* Grid overlay */}\r\n {showGrid && <div style={gridStyle} />}\r\n\r\n {/* Empty state */}\r\n {!backgroundImage && fieldsToRender.length === 0 && (\r\n <div\r\n style={{\r\n position: 'absolute',\r\n top: '50%',\r\n left: '50%',\r\n transform: 'translate(-50%, -50%)',\r\n textAlign: 'center',\r\n color: '#999',\r\n fontSize: '14px',\r\n }}\r\n >\r\n <p>Upload a background image</p>\r\n <p>and add fields to get started</p>\r\n </div>\r\n )}\r\n </div>\r\n );\r\n};\r\n\r\n"],"names":["useMemo","_jsx","positionToStyle","fieldStyleToCSS","_jsxs","_Fragment","generateQrCodeFromFields"],"mappings":"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"}
|
|
@@ -27,7 +27,7 @@ function toReactStyle(styleObj) {
|
|
|
27
27
|
function fieldStyleToCSS(style) {
|
|
28
28
|
if (!style)
|
|
29
29
|
return {};
|
|
30
|
-
|
|
30
|
+
return {
|
|
31
31
|
fontSize: style.fontSize,
|
|
32
32
|
fontWeight: style.fontWeight,
|
|
33
33
|
fontFamily: style.fontFamily,
|
|
@@ -40,24 +40,8 @@ function fieldStyleToCSS(style) {
|
|
|
40
40
|
border: style.border,
|
|
41
41
|
lineHeight: style.lineHeight,
|
|
42
42
|
letterSpacing: style.letterSpacing,
|
|
43
|
+
// verticalAlign is handled in the preview component for flex
|
|
43
44
|
};
|
|
44
|
-
// Add vertical alignment for text fields
|
|
45
|
-
if (style.verticalAlign) {
|
|
46
|
-
if (style.verticalAlign === 'top') {
|
|
47
|
-
css.display = 'flex';
|
|
48
|
-
css.alignItems = 'flex-start';
|
|
49
|
-
}
|
|
50
|
-
else if (style.verticalAlign === 'middle') {
|
|
51
|
-
css.display = 'flex';
|
|
52
|
-
css.alignItems = 'center';
|
|
53
|
-
}
|
|
54
|
-
else if (style.verticalAlign === 'bottom') {
|
|
55
|
-
css.display = 'flex';
|
|
56
|
-
css.alignItems = 'flex-end';
|
|
57
|
-
}
|
|
58
|
-
css.height = '100%';
|
|
59
|
-
}
|
|
60
|
-
return css;
|
|
61
45
|
}
|
|
62
46
|
/**
|
|
63
47
|
* Merge multiple style objects
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"styleUtils.js","sources":["../../../src/utils/styleUtils.ts"],"sourcesContent":["import type React from 'react';\r\nimport type { FieldStyle } from '../types';\r\n\r\n/**\r\n * Convert a style object with kebab-case keys to React camelCase\r\n * Useful when importing styles from external sources\r\n */\r\nexport function toReactStyle(\r\n styleObj: Record<string, unknown> | undefined\r\n): React.CSSProperties {\r\n if (!styleObj) return {};\r\n\r\n const out: Record<string, unknown> = {};\r\n for (const key in styleObj) {\r\n // Skip non-style properties\r\n if (key === 'frontOrBack' || key === 'category' || key === 'element') {\r\n continue;\r\n }\r\n // Convert kebab-case to camelCase (e.g., 'font-size' -> 'fontSize')\r\n const reactKey = key.replace(/-([a-z])/g, (g) => g[1].toUpperCase());\r\n out[reactKey] = styleObj[key];\r\n }\r\n return out as React.CSSProperties;\r\n}\r\n\r\n/**\r\n * Convert FieldStyle to React CSS properties\r\n * @param style - The field style object\r\n * @returns React-compatible CSS properties object\r\n */\r\nexport function fieldStyleToCSS(style: FieldStyle | undefined): React.CSSProperties {\r\n if (!style) return {};\r\n
|
|
1
|
+
{"version":3,"file":"styleUtils.js","sources":["../../../src/utils/styleUtils.ts"],"sourcesContent":["import type React from 'react';\r\nimport type { FieldStyle } from '../types';\r\n\r\n/**\r\n * Convert a style object with kebab-case keys to React camelCase\r\n * Useful when importing styles from external sources\r\n */\r\nexport function toReactStyle(\r\n styleObj: Record<string, unknown> | undefined\r\n): React.CSSProperties {\r\n if (!styleObj) return {};\r\n\r\n const out: Record<string, unknown> = {};\r\n for (const key in styleObj) {\r\n // Skip non-style properties\r\n if (key === 'frontOrBack' || key === 'category' || key === 'element') {\r\n continue;\r\n }\r\n // Convert kebab-case to camelCase (e.g., 'font-size' -> 'fontSize')\r\n const reactKey = key.replace(/-([a-z])/g, (g) => g[1].toUpperCase());\r\n out[reactKey] = styleObj[key];\r\n }\r\n return out as React.CSSProperties;\r\n}\r\n\r\n/**\r\n * Convert FieldStyle to React CSS properties\r\n * @param style - The field style object\r\n * @returns React-compatible CSS properties object\r\n */\r\nexport function fieldStyleToCSS(style: FieldStyle | undefined): React.CSSProperties {\r\n if (!style) return {};\r\n return {\r\n fontSize: style.fontSize,\r\n fontWeight: style.fontWeight,\r\n fontFamily: style.fontFamily,\r\n color: style.color,\r\n textAlign: style.textAlign,\r\n textTransform: style.textTransform,\r\n backgroundColor: style.backgroundColor,\r\n borderRadius: style.borderRadius,\r\n padding: style.padding,\r\n border: style.border,\r\n lineHeight: style.lineHeight,\r\n letterSpacing: style.letterSpacing,\r\n // verticalAlign is handled in the preview component for flex\r\n };\r\n}\r\n\r\n/**\r\n * Merge multiple style objects\r\n */\r\nexport function mergeStyles(\r\n ...styles: (React.CSSProperties | undefined)[]\r\n): React.CSSProperties {\r\n return Object.assign({}, ...styles.filter(Boolean));\r\n}\r\n\r\n/**\r\n * Generate position styles from field position\r\n */\r\nexport function positionToStyle(position: {\r\n x: number;\r\n y: number;\r\n width: number;\r\n height: number;\r\n}): React.CSSProperties {\r\n return {\r\n position: 'absolute',\r\n left: `${position.x}px`,\r\n top: `${position.y}px`,\r\n width: `${position.width}px`,\r\n height: `${position.height}px`,\r\n };\r\n}\r\n\r\n/**\r\n * Default styles for different field types\r\n */\r\nexport const defaultFieldStyles: Record<string, FieldStyle> = {\r\n text: {\r\n fontSize: '12px',\r\n fontWeight: 'normal',\r\n color: '#000000',\r\n textAlign: 'left',\r\n },\r\n label: {\r\n fontSize: '10px',\r\n fontWeight: 'bold',\r\n color: '#333333',\r\n textAlign: 'left',\r\n textTransform: 'uppercase',\r\n },\r\n image: {},\r\n qrcode: {\r\n backgroundColor: '#ffffff',\r\n },\r\n};\r\n\r\n/**\r\n * CSS class name generator\r\n */\r\nexport function cn(...classes: (string | undefined | null | false)[]): string {\r\n return classes.filter(Boolean).join(' ');\r\n}\r\n"],"names":[],"mappings":";;AAGA;;;AAGG;AACG,SAAU,YAAY,CAC1B,QAA6C,EAAA;AAE7C,IAAA,IAAI,CAAC,QAAQ;AAAE,QAAA,OAAO,EAAE;IAExB,MAAM,GAAG,GAA4B,EAAE;AACvC,IAAA,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE;;AAE1B,QAAA,IAAI,GAAG,KAAK,aAAa,IAAI,GAAG,KAAK,UAAU,IAAI,GAAG,KAAK,SAAS,EAAE;YACpE;QACF;;QAEA,MAAM,QAAQ,GAAG,GAAG,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QACpE,GAAG,CAAC,QAAQ,CAAC,GAAG,QAAQ,CAAC,GAAG,CAAC;IAC/B;AACA,IAAA,OAAO,GAA0B;AACnC;AAEA;;;;AAIG;AACG,SAAU,eAAe,CAAC,KAA6B,EAAA;AAC3D,IAAA,IAAI,CAAC,KAAK;AAAE,QAAA,OAAO,EAAE;IACrB,OAAO;QACL,QAAQ,EAAE,KAAK,CAAC,QAAQ;QACxB,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,KAAK,EAAE,KAAK,CAAC,KAAK;QAClB,SAAS,EAAE,KAAK,CAAC,SAAS;QAC1B,aAAa,EAAE,KAAK,CAAC,aAAa;QAClC,eAAe,EAAE,KAAK,CAAC,eAAe;QACtC,YAAY,EAAE,KAAK,CAAC,YAAY;QAChC,OAAO,EAAE,KAAK,CAAC,OAAO;QACtB,MAAM,EAAE,KAAK,CAAC,MAAM;QACpB,UAAU,EAAE,KAAK,CAAC,UAAU;QAC5B,aAAa,EAAE,KAAK,CAAC,aAAa;;KAEnC;AACH;AAEA;;AAEG;AACG,SAAU,WAAW,CACzB,GAAG,MAA2C,EAAA;AAE9C,IAAA,OAAO,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AACrD;AAEA;;AAEG;AACG,SAAU,eAAe,CAAC,QAK/B,EAAA;IACC,OAAO;AACL,QAAA,QAAQ,EAAE,UAAU;AACpB,QAAA,IAAI,EAAE,CAAA,EAAG,QAAQ,CAAC,CAAC,CAAA,EAAA,CAAI;AACvB,QAAA,GAAG,EAAE,CAAA,EAAG,QAAQ,CAAC,CAAC,CAAA,EAAA,CAAI;AACtB,QAAA,KAAK,EAAE,CAAA,EAAG,QAAQ,CAAC,KAAK,CAAA,EAAA,CAAI;AAC5B,QAAA,MAAM,EAAE,CAAA,EAAG,QAAQ,CAAC,MAAM,CAAA,EAAA,CAAI;KAC/B;AACH;AAEA;;AAEG;AACI,MAAM,kBAAkB,GAA+B;AAC5D,IAAA,IAAI,EAAE;AACJ,QAAA,QAAQ,EAAE,MAAM;AAChB,QAAA,UAAU,EAAE,QAAQ;AACpB,QAAA,KAAK,EAAE,SAAS;AAChB,QAAA,SAAS,EAAE,MAAM;AAClB,KAAA;AACD,IAAA,KAAK,EAAE;AACL,QAAA,QAAQ,EAAE,MAAM;AAChB,QAAA,UAAU,EAAE,MAAM;AAClB,QAAA,KAAK,EAAE,SAAS;AAChB,QAAA,SAAS,EAAE,MAAM;AACjB,QAAA,aAAa,EAAE,WAAW;AAC3B,KAAA;AACD,IAAA,KAAK,EAAE,EAAE;AACT,IAAA,MAAM,EAAE;AACN,QAAA,eAAe,EAAE,SAAS;AAC3B,KAAA;;AAGH;;AAEG;AACG,SAAU,EAAE,CAAC,GAAG,OAA8C,EAAA;IAClE,OAAO,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;AAC1C;;;;;;;;;"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
1
|
+
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
2
2
|
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
3
3
|
import { useIDCardTemplate } from '../hooks/useIDCardTemplate.js';
|
|
4
4
|
import { deepCleanUndefined } from '../utils/deepCleanUndefined.js';
|
|
@@ -61,6 +61,24 @@ const IDCardDesigner = ({ initialTemplate, onSave, onCancel, className = '', sty
|
|
|
61
61
|
const handleFieldSelect = useCallback((field) => {
|
|
62
62
|
setSelectedFieldId(field?.id || null);
|
|
63
63
|
}, []);
|
|
64
|
+
// Duplicate selected text field
|
|
65
|
+
const handleDuplicateField = useCallback(() => {
|
|
66
|
+
if (!selectedField || selectedField.type !== 'text')
|
|
67
|
+
return;
|
|
68
|
+
const { id, fieldKey, position, ...rest } = selectedField;
|
|
69
|
+
const newField = {
|
|
70
|
+
...rest,
|
|
71
|
+
id: generateId('field'),
|
|
72
|
+
fieldKey: `field_${Date.now()}`,
|
|
73
|
+
position: {
|
|
74
|
+
...position,
|
|
75
|
+
x: Math.min(position.x + 20, template.cardSize.width - position.width),
|
|
76
|
+
y: Math.min(position.y + 20, template.cardSize.height - position.height),
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
addField(newField);
|
|
80
|
+
setSelectedFieldId(newField.id);
|
|
81
|
+
}, [selectedField, addField, setSelectedFieldId, template.cardSize.width, template.cardSize.height]);
|
|
64
82
|
// Handle adding new field
|
|
65
83
|
const handleAddField = useCallback((type) => {
|
|
66
84
|
const newField = {
|
|
@@ -75,6 +93,7 @@ const IDCardDesigner = ({ initialTemplate, onSave, onCancel, className = '', sty
|
|
|
75
93
|
fontWeight: type === 'label' ? 'bold' : 'normal',
|
|
76
94
|
color: '#000000',
|
|
77
95
|
textAlign: 'left',
|
|
96
|
+
backgroundColor: 'transparent',
|
|
78
97
|
},
|
|
79
98
|
zIndex: template.fields.length + 1,
|
|
80
99
|
staticText: type === 'label' ? 'Static Label' : '',
|
|
@@ -162,6 +181,7 @@ const IDCardDesigner = ({ initialTemplate, onSave, onCancel, className = '', sty
|
|
|
162
181
|
if (isDragging) {
|
|
163
182
|
const deltaX = e.clientX - dragStartRef.current.x;
|
|
164
183
|
const deltaY = e.clientY - dragStartRef.current.y;
|
|
184
|
+
// Unified drag logic for all field types
|
|
165
185
|
const newX = Math.max(0, Math.min(template.cardSize.width - selectedField.position.width, dragStartRef.current.fieldX + deltaX));
|
|
166
186
|
const newY = Math.max(0, Math.min(template.cardSize.height - selectedField.position.height, dragStartRef.current.fieldY + deltaY));
|
|
167
187
|
updateField(selectedField.id, {
|
|
@@ -176,19 +196,50 @@ const IDCardDesigner = ({ initialTemplate, onSave, onCancel, className = '', sty
|
|
|
176
196
|
let newHeight = height;
|
|
177
197
|
let newX = fieldX;
|
|
178
198
|
let newY = fieldY;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
199
|
+
// Enforce fixed aspect ratio for QR code fields
|
|
200
|
+
if (selectedField.type === 'qrcode') {
|
|
201
|
+
// Use the larger delta for both width and height
|
|
202
|
+
let delta = 0;
|
|
203
|
+
if (handle.includes('e') || handle.includes('w')) {
|
|
204
|
+
delta = deltaX;
|
|
205
|
+
}
|
|
206
|
+
else if (handle.includes('s') || handle.includes('n')) {
|
|
207
|
+
delta = deltaY;
|
|
208
|
+
}
|
|
209
|
+
if (handle.includes('e')) {
|
|
210
|
+
newWidth = Math.max(20, width + delta);
|
|
211
|
+
newHeight = newWidth;
|
|
212
|
+
}
|
|
213
|
+
if (handle.includes('w')) {
|
|
214
|
+
newWidth = Math.max(20, width - delta);
|
|
215
|
+
newHeight = newWidth;
|
|
216
|
+
newX = Math.max(0, fieldX + delta);
|
|
217
|
+
}
|
|
218
|
+
if (handle.includes('s')) {
|
|
219
|
+
newHeight = Math.max(20, height + delta);
|
|
220
|
+
newWidth = newHeight;
|
|
221
|
+
}
|
|
222
|
+
if (handle.includes('n')) {
|
|
223
|
+
newHeight = Math.max(20, height - delta);
|
|
224
|
+
newWidth = newHeight;
|
|
225
|
+
newY = Math.max(0, fieldY + delta);
|
|
226
|
+
}
|
|
188
227
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
228
|
+
else {
|
|
229
|
+
if (handle.includes('e')) {
|
|
230
|
+
newWidth = Math.max(20, width + deltaX);
|
|
231
|
+
}
|
|
232
|
+
if (handle.includes('w')) {
|
|
233
|
+
newWidth = Math.max(20, width - deltaX);
|
|
234
|
+
newX = Math.max(0, fieldX + deltaX);
|
|
235
|
+
}
|
|
236
|
+
if (handle.includes('s')) {
|
|
237
|
+
newHeight = Math.max(20, height + deltaY);
|
|
238
|
+
}
|
|
239
|
+
if (handle.includes('n')) {
|
|
240
|
+
newHeight = Math.max(20, height - deltaY);
|
|
241
|
+
newY = Math.max(0, fieldY + deltaY);
|
|
242
|
+
}
|
|
192
243
|
}
|
|
193
244
|
updateField(selectedField.id, {
|
|
194
245
|
position: { x: newX, y: newY, width: newWidth, height: newHeight },
|
|
@@ -198,14 +249,19 @@ const IDCardDesigner = ({ initialTemplate, onSave, onCancel, className = '', sty
|
|
|
198
249
|
const handleMouseUp = () => {
|
|
199
250
|
setIsDragging(false);
|
|
200
251
|
setIsResizing(false);
|
|
252
|
+
// Remove any accidental selection or pointer capture
|
|
253
|
+
window.getSelection?.()?.removeAllRanges?.();
|
|
254
|
+
if (previewContainerRef.current) {
|
|
255
|
+
previewContainerRef.current.releasePointerCapture?.(0);
|
|
256
|
+
}
|
|
201
257
|
};
|
|
202
258
|
if (isDragging || isResizing) {
|
|
203
259
|
document.addEventListener('mousemove', handleMouseMove);
|
|
204
|
-
|
|
260
|
+
window.addEventListener('mouseup', handleMouseUp);
|
|
205
261
|
}
|
|
206
262
|
return () => {
|
|
207
263
|
document.removeEventListener('mousemove', handleMouseMove);
|
|
208
|
-
|
|
264
|
+
window.removeEventListener('mouseup', handleMouseUp);
|
|
209
265
|
};
|
|
210
266
|
}, [isDragging, isResizing, selectedField, updateField, template.cardSize]);
|
|
211
267
|
// Handle field property update
|
|
@@ -256,16 +312,41 @@ const IDCardDesigner = ({ initialTemplate, onSave, onCancel, className = '', sty
|
|
|
256
312
|
// Keyboard shortcuts
|
|
257
313
|
useEffect(() => {
|
|
258
314
|
const handleKeyDown = (e) => {
|
|
259
|
-
if (
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
315
|
+
if (selectedFieldId) {
|
|
316
|
+
// Arrow key movement
|
|
317
|
+
const step = e.shiftKey ? 5 : 1;
|
|
318
|
+
let dx = 0, dy = 0;
|
|
319
|
+
if (e.key === 'ArrowLeft')
|
|
320
|
+
dx = -step;
|
|
321
|
+
if (e.key === 'ArrowRight')
|
|
322
|
+
dx = step;
|
|
323
|
+
if (e.key === 'ArrowUp')
|
|
324
|
+
dy = -step;
|
|
325
|
+
if (e.key === 'ArrowDown')
|
|
326
|
+
dy = step;
|
|
327
|
+
if (dx !== 0 || dy !== 0) {
|
|
328
|
+
const field = template.fields.find(f => f.id === selectedFieldId);
|
|
329
|
+
if (field) {
|
|
330
|
+
let newX = Math.max(0, Math.min(template.cardSize.width - field.position.width, field.position.x + dx));
|
|
331
|
+
let newY = Math.max(0, Math.min(template.cardSize.height - field.position.height, field.position.y + dy));
|
|
332
|
+
updateField(field.id, {
|
|
333
|
+
position: { ...field.position, x: newX, y: newY },
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
e.preventDefault();
|
|
337
|
+
}
|
|
338
|
+
// Delete and Escape
|
|
339
|
+
if (e.key === 'Delete') {
|
|
340
|
+
handleDeleteField();
|
|
341
|
+
}
|
|
342
|
+
if (e.key === 'Escape') {
|
|
343
|
+
setSelectedFieldId(null);
|
|
344
|
+
}
|
|
264
345
|
}
|
|
265
346
|
};
|
|
266
347
|
document.addEventListener('keydown', handleKeyDown);
|
|
267
348
|
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
268
|
-
}, [selectedFieldId, handleDeleteField]);
|
|
349
|
+
}, [selectedFieldId, handleDeleteField, template.fields, template.cardSize, updateField]);
|
|
269
350
|
return (jsxs("div", { className: `idcard-designer ${className}`, style: style, children: [jsxs("div", { className: "designer-header", children: [jsx("input", { type: "text", value: templateName, onChange: (e) => setTemplateName(e.target.value), className: "template-name-input", placeholder: "Template Name" }), jsxs("div", { className: "header-actions", children: [onCancel && (jsx("button", { onClick: onCancel, className: "btn btn-secondary", children: "Cancel" })), jsx("button", { onClick: handleSave, className: "btn btn-primary", disabled: !isValid, children: "Save Template" })] })] }), jsxs("div", { className: "designer-body", children: [jsxs("div", { className: "designer-sidebar left-sidebar", children: [jsx("h3", { children: "Add Fields" }), jsx("div", { className: "field-types", children: defaultFieldTypes.map((fieldType) => (jsxs("button", { onClick: () => handleAddField(fieldType.type), className: "field-type-btn", children: [jsx("span", { className: "field-icon", children: fieldType.icon }), jsx("span", { className: "field-label", children: fieldType.label })] }, fieldType.type))) }), jsx("h3", { children: "Card Settings" }), jsxs("div", { className: "card-settings", children: [jsxs("div", { className: "setting-group", children: [jsx("label", { children: "Orientation" }), jsxs("div", { className: "btn-group", children: [jsx("button", { className: `btn ${template.orientation === 'landscape' ? 'active' : ''}`, onClick: () => handleOrientationChange('landscape'), children: "Landscape" }), jsx("button", { className: `btn ${template.orientation === 'portrait' ? 'active' : ''}`, onClick: () => handleOrientationChange('portrait'), children: "Portrait" })] })] }), jsxs("div", { className: "setting-group", children: [jsx("label", { children: "Card Sides" }), jsxs("div", { className: "btn-group", children: [jsx("button", { className: `btn ${template.sides === 'single' ? 'active' : ''}`, onClick: () => handleSidesChange('single'), children: "Single" }), jsx("button", { className: `btn ${template.sides === 'double' ? 'active' : ''}`, onClick: () => handleSidesChange('double'), children: "Double" })] })] }), jsxs("div", { className: "setting-group", children: [jsx("label", { children: "Card Size (px)" }), jsxs("div", { className: "size-inputs", children: [jsx("input", { type: "number", value: template.cardSize.width, onChange: (e) => setCardSize({ ...template.cardSize, width: Number(e.target.value) }), placeholder: "Width" }), jsx("span", { children: "\u00D7" }), jsx("input", { type: "number", value: template.cardSize.height, onChange: (e) => setCardSize({ ...template.cardSize, height: Number(e.target.value) }), placeholder: "Height" })] })] })] }), jsx("h3", { children: "Background" }), jsxs("div", { className: "background-upload", children: [jsx("input", { type: "file", accept: "image/*", onChange: handleBackgroundUpload, id: "bg-upload", className: "file-input" }), jsxs("label", { htmlFor: "bg-upload", className: "btn btn-outline", children: ["Upload ", activeSide, " Background"] }), template.backgrounds[activeSide] && (jsx("button", { className: "btn btn-danger btn-sm", onClick: () => setBackground(activeSide, ''), children: "Remove" }))] })] }), jsxs("div", { className: "designer-preview", children: [jsxs("div", { className: "side-tabs", children: [jsx("button", { className: `tab ${activeSide === 'front' ? 'active' : ''}`, onClick: () => setActiveSide('front'), children: "Front" }), template.sides === 'double' && (jsx("button", { className: `tab ${activeSide === 'back' ? 'active' : ''}`, onClick: () => setActiveSide('back'), children: "Back" })), jsxs("label", { className: "grid-toggle", children: [jsx("input", { type: "checkbox", checked: showGrid, onChange: (e) => setShowGrid(e.target.checked) }), "Show Grid"] })] }), jsx("div", { ref: previewContainerRef, className: "preview-wrapper", onMouseDown: handleMouseDown, children: jsx(IDCardPreview, { template: template, data: sampleData, side: activeSide, showGrid: showGrid, onFieldSelect: handleFieldSelect, selectedFieldId: selectedFieldId, editable: true }) }), jsxs("div", { className: "fields-list", children: [jsxs("h4", { children: ["Fields on ", activeSide, " side:"] }), template.fields
|
|
270
351
|
.filter((f) => f.side === activeSide)
|
|
271
352
|
.map((field) => (jsxs("div", { className: `field-item ${field.id === selectedFieldId ? 'selected' : ''}`, onClick: () => setSelectedFieldId(field.id), children: [jsx("span", { className: "field-type-icon", children: defaultFieldTypes.find((t) => t.type === field.type)?.icon }), jsx("span", { className: "field-key", children: field.fieldKey }), jsx("button", { className: "delete-btn", onClick: (e) => {
|
|
@@ -273,7 +354,7 @@ const IDCardDesigner = ({ initialTemplate, onSave, onCancel, className = '', sty
|
|
|
273
354
|
removeField(field.id);
|
|
274
355
|
if (field.id === selectedFieldId)
|
|
275
356
|
setSelectedFieldId(null);
|
|
276
|
-
}, children: "\u00D7" })] }, field.id)))] })] }), jsxs("div", { className: "designer-sidebar right-sidebar", children: [jsx("h3", { children: "Field Properties" }), selectedField ? (jsxs("div", { className: "field-properties", children: [jsxs("div", { className: "property-group", children: [jsx("label", { children: "Field Key" }), jsx("input", { type: "text", value: selectedField.fieldKey, onChange: (e) => handleFieldPropertyUpdate('fieldKey', e.target.value) })] }), selectedField.type !== 'label' && (jsxs("div", { className: "property-group", children: [jsx("label", { children: "Label" }), jsx("input", { type: "text", value: selectedField.label || '', onChange: (e) => handleFieldPropertyUpdate('label', e.target.value) })] })), selectedField.type === 'label' && (jsxs("div", { className: "property-group", children: [jsx("label", { children: "Static Text" }), jsx("input", { type: "text", value: selectedField.staticText || '', onChange: (e) => handleFieldPropertyUpdate('staticText', e.target.value) })] })), selectedField.type === 'qrcode' && (jsxs("div", { className: "property-group", children: [jsx("label", { children: "QR Code Data Fields" }), jsx("small", { className: "hint", children: "Select which fields to encode in the QR code:" }), jsx("div", { style: { marginTop: '8px', maxHeight: '150px', overflowY: 'auto', border: '1px solid #ddd', borderRadius: '4px', padding: '8px' }, children: template.fields
|
|
357
|
+
}, children: "\u00D7" })] }, field.id)))] })] }), jsxs("div", { className: "designer-sidebar right-sidebar", children: [jsx("h3", { children: "Field Properties" }), selectedField ? (jsxs("div", { className: "field-properties", children: [jsxs("div", { className: "property-group", children: [jsx("label", { children: "Field Key" }), jsx("input", { type: "text", value: selectedField.fieldKey, onChange: (e) => handleFieldPropertyUpdate('fieldKey', e.target.value) })] }), selectedField.type !== 'label' && (jsxs("div", { className: "property-group", children: [jsx("label", { children: "Label" }), jsx("input", { type: "text", value: selectedField.label || '', onChange: (e) => handleFieldPropertyUpdate('label', e.target.value) })] })), selectedField.type === 'text' && (jsxs(Fragment, { children: [jsx("button", { className: "btn btn-outline btn-sm", style: { marginBottom: 12 }, onClick: handleDuplicateField, children: "Duplicate Field" }), jsxs("div", { style: { border: '1px solid #eee', borderRadius: 4, marginBottom: 12, padding: 8 }, children: [jsx("strong", { style: { fontSize: '1em', display: 'block', marginBottom: 6 }, children: "Label Style" }), jsxs("div", { className: "property-group", children: [jsx("label", { children: "Label Position" }), jsxs("select", { value: selectedField.style?.labelPosition || 'top', onChange: (e) => handleFieldPropertyUpdate('style.labelPosition', e.target.value), children: [jsx("option", { value: "top", children: "Top" }), jsx("option", { value: "left", children: "Left (Side)" })] })] }), jsxs("div", { className: "property-group", children: [jsx("label", { children: "Label Vertical Align" }), jsxs("select", { value: selectedField.style?.labelVerticalAlign || 'top', onChange: (e) => handleFieldPropertyUpdate('style.labelVerticalAlign', e.target.value), children: [jsx("option", { value: "top", children: "Top" }), jsx("option", { value: "middle", children: "Middle" }), jsx("option", { value: "bottom", children: "Bottom" })] })] }), jsxs("div", { className: "property-group", children: [jsx("label", { children: "Label Font Size" }), jsx("input", { type: "text", value: selectedField.style?.labelFontSize || '0.75em', onChange: (e) => handleFieldPropertyUpdate('style.labelFontSize', e.target.value) })] }), jsxs("div", { className: "property-group", children: [jsx("label", { children: "Label Font Weight" }), jsxs("select", { value: selectedField.style?.labelFontWeight || 'normal', onChange: (e) => handleFieldPropertyUpdate('style.labelFontWeight', e.target.value), children: [jsx("option", { value: "normal", children: "Normal" }), jsx("option", { value: "bold", children: "Bold" }), jsx("option", { value: "100", children: "100" }), jsx("option", { value: "200", children: "200" }), jsx("option", { value: "300", children: "300" }), jsx("option", { value: "400", children: "400" }), jsx("option", { value: "500", children: "500" }), jsx("option", { value: "600", children: "600" }), jsx("option", { value: "700", children: "700" }), jsx("option", { value: "800", children: "800" }), jsx("option", { value: "900", children: "900" })] })] }), jsxs("div", { className: "property-group", children: [jsx("label", { children: "Label Color" }), jsx("input", { type: "color", value: selectedField.style?.labelColor || '#666666', onChange: (e) => handleFieldPropertyUpdate('style.labelColor', e.target.value) })] })] }), jsxs("div", { style: { border: '1px solid #eee', borderRadius: 4, marginBottom: 12, padding: 8 }, children: [jsx("strong", { style: { fontSize: '1em', display: 'block', marginBottom: 6 }, children: "Text Style" }), jsxs("div", { className: "property-group", children: [jsx("label", { children: "Font Size" }), jsx("input", { type: "text", value: selectedField.style?.fontSize || '12px', onChange: (e) => handleFieldPropertyUpdate('style.fontSize', e.target.value) })] }), jsxs("div", { className: "property-group", children: [jsx("label", { children: "Font Weight" }), jsxs("select", { value: selectedField.style?.fontWeight || 'normal', onChange: (e) => handleFieldPropertyUpdate('style.fontWeight', e.target.value), children: [jsx("option", { value: "normal", children: "Normal" }), jsx("option", { value: "bold", children: "Bold" }), jsx("option", { value: "100", children: "100" }), jsx("option", { value: "200", children: "200" }), jsx("option", { value: "300", children: "300" }), jsx("option", { value: "400", children: "400" }), jsx("option", { value: "500", children: "500" }), jsx("option", { value: "600", children: "600" }), jsx("option", { value: "700", children: "700" }), jsx("option", { value: "800", children: "800" }), jsx("option", { value: "900", children: "900" })] })] }), jsxs("div", { className: "property-group", children: [jsx("label", { children: "Text Color" }), jsx("input", { type: "color", value: selectedField.style?.color || '#000000', onChange: (e) => handleFieldPropertyUpdate('style.color', e.target.value) })] }), jsxs("div", { className: "property-group", children: [jsx("label", { children: "Text Align" }), jsxs("select", { value: selectedField.style?.textAlign || 'left', onChange: (e) => handleFieldPropertyUpdate('style.textAlign', e.target.value), children: [jsx("option", { value: "left", children: "Left" }), jsx("option", { value: "center", children: "Center" }), jsx("option", { value: "right", children: "Right" })] })] }), jsxs("div", { className: "property-group", children: [jsx("label", { children: "Text Transform" }), jsxs("select", { value: selectedField.style?.textTransform || 'none', onChange: (e) => handleFieldPropertyUpdate('style.textTransform', e.target.value), children: [jsx("option", { value: "none", children: "None" }), jsx("option", { value: "uppercase", children: "UPPERCASE" }), jsx("option", { value: "lowercase", children: "lowercase" }), jsx("option", { value: "capitalize", children: "Capitalize" })] })] }), jsxs("div", { className: "property-group", children: [jsx("label", { children: "Vertical Align" }), jsxs("select", { value: selectedField.style?.verticalAlign || 'top', onChange: (e) => handleFieldPropertyUpdate('style.verticalAlign', e.target.value), children: [jsx("option", { value: "top", children: "Top" }), jsx("option", { value: "middle", children: "Middle" }), jsx("option", { value: "bottom", children: "Bottom" })] })] })] })] })), selectedField.type === 'label' && (jsxs("div", { className: "property-group", children: [jsx("label", { children: "Static Text" }), jsx("input", { type: "text", value: selectedField.staticText || '', onChange: (e) => handleFieldPropertyUpdate('staticText', e.target.value) })] })), selectedField.type === 'qrcode' && (jsxs("div", { className: "property-group", children: [jsx("label", { children: "QR Code Data Fields" }), jsx("small", { className: "hint", children: "Select which fields to encode in the QR code:" }), jsx("div", { style: { marginTop: '8px', maxHeight: '150px', overflowY: 'auto', border: '1px solid #ddd', borderRadius: '4px', padding: '8px' }, children: template.fields
|
|
277
358
|
.filter(f => f.type !== 'qrcode' && f.type !== 'label')
|
|
278
359
|
.map((field) => (jsxs("label", { style: { display: 'block', marginBottom: '6px', cursor: 'pointer' }, children: [jsx("input", { type: "checkbox", checked: selectedField.qrFields?.includes(field.fieldKey) || false, onChange: (e) => {
|
|
279
360
|
const currentFields = selectedField.qrFields || [];
|
|
@@ -281,7 +362,7 @@ const IDCardDesigner = ({ initialTemplate, onSave, onCancel, className = '', sty
|
|
|
281
362
|
? [...currentFields, field.fieldKey]
|
|
282
363
|
: currentFields.filter(f => f !== field.fieldKey);
|
|
283
364
|
handleFieldPropertyUpdate('qrFields', newFields);
|
|
284
|
-
}, style: { marginRight: '6px' } }), field.label || field.fieldKey] }, field.fieldKey))) })] })),
|
|
365
|
+
}, style: { marginRight: '6px' } }), field.label || field.fieldKey] }, field.fieldKey))) })] })), selectedField.type === 'image' && (jsxs("div", { className: "property-group", children: [jsx("label", { children: "Border Radius" }), jsx("input", { type: "text", placeholder: "e.g. 50% or 8px", value: selectedField.style?.borderRadius || '', onChange: (e) => handleFieldPropertyUpdate('style.borderRadius', e.target.value) }), jsxs("small", { className: "hint", children: ["Use ", jsx("b", { children: "50%" }), " for a circle, ", jsx("b", { children: "8px" }), " for rounded corners, etc."] })] })), selectedField.type !== 'text' && jsxs(Fragment, { children: [jsx("h4", { children: "Style" }), jsxs("div", { className: "property-group", children: [jsx("label", { children: "Font Size" }), jsx("input", { type: "text", value: selectedField.style?.fontSize || '12px', onChange: (e) => handleFieldPropertyUpdate('style.fontSize', e.target.value) })] }), jsxs("div", { className: "property-group", children: [jsx("label", { children: "Font Weight" }), jsxs("select", { value: selectedField.style?.fontWeight || 'normal', onChange: (e) => handleFieldPropertyUpdate('style.fontWeight', e.target.value), children: [jsx("option", { value: "normal", children: "Normal" }), jsx("option", { value: "bold", children: "Bold" }), jsx("option", { value: "100", children: "100" }), jsx("option", { value: "200", children: "200" }), jsx("option", { value: "300", children: "300" }), jsx("option", { value: "400", children: "400" }), jsx("option", { value: "500", children: "500" }), jsx("option", { value: "600", children: "600" }), jsx("option", { value: "700", children: "700" }), jsx("option", { value: "800", children: "800" }), jsx("option", { value: "900", children: "900" })] })] }), jsxs("div", { className: "property-group", children: [jsx("label", { children: "Text Color" }), jsx("input", { type: "color", value: selectedField.style?.color || '#000000', onChange: (e) => handleFieldPropertyUpdate('style.color', e.target.value) })] }), jsxs("div", { className: "property-group", children: [jsx("label", { children: "Text Align" }), jsxs("select", { value: selectedField.style?.textAlign || 'left', onChange: (e) => handleFieldPropertyUpdate('style.textAlign', e.target.value), children: [jsx("option", { value: "left", children: "Left" }), jsx("option", { value: "center", children: "Center" }), jsx("option", { value: "right", children: "Right" })] })] }), jsxs("div", { className: "property-group", children: [jsx("label", { children: "Text Transform" }), jsxs("select", { value: selectedField.style?.textTransform || 'none', onChange: (e) => handleFieldPropertyUpdate('style.textTransform', e.target.value), children: [jsx("option", { value: "none", children: "None" }), jsx("option", { value: "uppercase", children: "UPPERCASE" }), jsx("option", { value: "lowercase", children: "lowercase" }), jsx("option", { value: "capitalize", children: "Capitalize" })] })] })] }), jsxs("div", { className: "property-group", children: [jsx("label", { children: "Background Color" }), jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8 }, children: [jsx("input", { type: "checkbox", id: "transparent-bg-checkbox", checked: selectedField.style?.backgroundColor === 'transparent', onChange: e => handleFieldPropertyUpdate('style.backgroundColor', e.target.checked ? 'transparent' : '#ffffff') }), jsx("label", { htmlFor: "transparent-bg-checkbox", style: { margin: 0, fontWeight: 400, fontSize: '0.95em' }, children: "Transparent" }), selectedField.style?.backgroundColor !== 'transparent' && (jsx("input", { type: "color", value: selectedField.style?.backgroundColor || '#ffffff', onChange: e => handleFieldPropertyUpdate('style.backgroundColor', e.target.value) }))] })] }), jsxs("div", { className: "property-group", children: [jsx("label", { children: "Side" }), jsxs("select", { value: selectedField.side, onChange: (e) => handleFieldPropertyUpdate('side', e.target.value), children: [jsx("option", { value: "front", children: "Front" }), template.sides === 'double' && jsx("option", { value: "back", children: "Back" })] })] }), jsx("button", { className: "btn btn-danger", onClick: handleDeleteField, children: "Delete Field" })] })) : (jsx("p", { className: "no-selection", children: "Select a field to edit its properties" })), jsx("h3", { children: "Sample Data" }), jsxs("div", { className: "sample-data", children: [jsx("p", { className: "hint", children: "Edit sample data to preview:" }), Object.entries(sampleData).map(([key, value]) => (jsxs("div", { className: "property-group", children: [jsx("label", { children: key }), jsx("input", { type: "text", value: String(value), onChange: (e) => setSampleData((prev) => ({ ...prev, [key]: e.target.value })) })] }, key))), jsx("button", { className: "btn btn-outline btn-sm", onClick: () => setSampleData((prev) => ({ ...prev, [`custom_${Date.now()}`]: '' })), children: "+ Add Field" })] }), validationErrors.length > 0 && (jsxs("div", { className: "validation-errors", children: [jsx("h4", { children: "\u26A0\uFE0F Validation Issues" }), jsx("ul", { children: validationErrors.map((error, index) => (jsx("li", { children: error }, index))) })] }))] })] })] }));
|
|
285
366
|
};
|
|
286
367
|
|
|
287
368
|
export { IDCardDesigner };
|