qrlayout-ui 1.0.0
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/README.md +118 -0
- package/dist/index.d.ts +51 -0
- package/dist/qrlayout-ui.css +1 -0
- package/dist/qrlayout-ui.js +321 -0
- package/dist/qrlayout-ui.umd.js +147 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# qrlayout-ui
|
|
2
|
+
|
|
3
|
+
A framework-agnostic, embeddable UI for designing sticker layouts with QR codes. Part of the [QR Layout Tool](https://github.com/shashi089/qr-code-layout-generate-tool).
|
|
4
|
+
|
|
5
|
+
[**🚀 Live Demo**](https://qr-layout-designer.netlify.app/)
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Framework Independent**: Built with vanilla TypeScript, works with React, Vue, Angular, Svelte, or plain HTML/JS.
|
|
10
|
+
- **Drag & Drop Designer**: Visual placement of text and QR code elements.
|
|
11
|
+
- **Data Binding**: Bind fields like `{{name}}` or `{{id}}` from your entity schemas.
|
|
12
|
+
- **Rich Text Styling**: Customize font size, weight, and alignment (horizontal/vertical).
|
|
13
|
+
- **Auto-Join Fields**: Set a "Field Separator" (e.g., `|`) on QR elements to automatically join variables (e.g. `{{id}}{{name}}` becomes `ID|NAME`).
|
|
14
|
+
- **Dark Mode**: Built-in support for light and dark themes.
|
|
15
|
+
- **Flexible Units**: Design in millimeters (mm), centimeters (cm), inches (in), or pixels (px).
|
|
16
|
+
- **Export**: Get the final layout JSON for storage.
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install qrlayout-ui qrlayout-core
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
This library is exposed as a class `QRLayoutDesigner` that can be mounted into any HTML element. It also re-exports `StickerPrinter` for rendering layouts without the UI.
|
|
27
|
+
|
|
28
|
+
### 1. Import Styles
|
|
29
|
+
|
|
30
|
+
Make sure to import the CSS file in your project entry point:
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
import "qrlayout-ui/style.css";
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### 2. Basic Setup
|
|
37
|
+
|
|
38
|
+
```typescript
|
|
39
|
+
import { QRLayoutDesigner } from "qrlayout-ui";
|
|
40
|
+
|
|
41
|
+
const container = document.getElementById("my-designer-container");
|
|
42
|
+
|
|
43
|
+
const designer = new QRLayoutDesigner({
|
|
44
|
+
element: container,
|
|
45
|
+
|
|
46
|
+
// Optional: Provide Schemas for data binding
|
|
47
|
+
entitySchemas: {
|
|
48
|
+
employee: {
|
|
49
|
+
label: "Employee",
|
|
50
|
+
fields: [
|
|
51
|
+
{ name: "name", label: "Full Name" },
|
|
52
|
+
{ name: "id", label: "Employee ID" }
|
|
53
|
+
],
|
|
54
|
+
sampleData: { name: "Vishal Naik", id: "12345" } // Used for preview
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
// Optional: Load an existing layout
|
|
59
|
+
initialLayout: {
|
|
60
|
+
id: "1",
|
|
61
|
+
name: "My Layout",
|
|
62
|
+
targetEntity: "employee",
|
|
63
|
+
width: 100,
|
|
64
|
+
height: 60,
|
|
65
|
+
unit: "mm",
|
|
66
|
+
backgroundColor: "#ffffff",
|
|
67
|
+
elements: []
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
onSave: (layout) => {
|
|
71
|
+
console.log("Layout saved:", layout);
|
|
72
|
+
// Save to your backend here
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 3. Cleanup
|
|
78
|
+
|
|
79
|
+
When unmounting your component (e.g., in React's `useEffect` return or Vue's `onUnmounted`):
|
|
80
|
+
|
|
81
|
+
```javascript
|
|
82
|
+
designer.destroy();
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Props / Options
|
|
86
|
+
|
|
87
|
+
| Option | Type | Description |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `element` | `HTMLElement` | **Required**. The DOM element to mount the designer into. |
|
|
90
|
+
| `entitySchemas` | `Record<string, Schema>` | Definitions for data entities. Allows users to pick fields (like `{{name}}`) to bind to text/QR elements. |
|
|
91
|
+
| `initialLayout` | `StickerLayout` | The initial layout state to load. |
|
|
92
|
+
| `onSave` | `(layout) => void` | Callback triggered when the "Save Layout" button is clicked. |
|
|
93
|
+
|
|
94
|
+
## React Integration Example
|
|
95
|
+
|
|
96
|
+
```tsx
|
|
97
|
+
import { useEffect, useRef } from 'react';
|
|
98
|
+
import { QRLayoutDesigner } from 'qrlayout-ui';
|
|
99
|
+
import 'qrlayout-ui/style.css';
|
|
100
|
+
|
|
101
|
+
const MyDesigner = () => {
|
|
102
|
+
const containerRef = useRef(null);
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
if (!containerRef.current) return;
|
|
106
|
+
|
|
107
|
+
const designer = new QRLayoutDesigner({
|
|
108
|
+
element: containerRef.current,
|
|
109
|
+
onSave: (data) => console.log(data)
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return () => designer.destroy();
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
return <div ref={containerRef} style={{ width: '100%', height: '800px' }} />;
|
|
116
|
+
};
|
|
117
|
+
export default MyDesigner;
|
|
118
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { StickerPrinter, StickerLayout, StickerElement } from 'qrlayout-core';
|
|
2
|
+
export { StickerPrinter };
|
|
3
|
+
export type { StickerLayout, StickerElement };
|
|
4
|
+
export interface EntityField {
|
|
5
|
+
name: string;
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
export interface EntitySchema {
|
|
9
|
+
label: string;
|
|
10
|
+
fields: EntityField[];
|
|
11
|
+
sampleData: any;
|
|
12
|
+
}
|
|
13
|
+
export interface DesignerOptions {
|
|
14
|
+
element: HTMLElement;
|
|
15
|
+
initialLayout?: StickerLayout;
|
|
16
|
+
entitySchemas?: Record<string, EntitySchema>;
|
|
17
|
+
onSave?: (layout: StickerLayout) => void;
|
|
18
|
+
}
|
|
19
|
+
export declare class QRLayoutDesigner {
|
|
20
|
+
private container;
|
|
21
|
+
private currentLayout;
|
|
22
|
+
private entitySchemas;
|
|
23
|
+
private selectedElementId;
|
|
24
|
+
private isDarkMode;
|
|
25
|
+
private pxPerUnit;
|
|
26
|
+
private printer;
|
|
27
|
+
private onSaveCallback?;
|
|
28
|
+
private canvas;
|
|
29
|
+
private editorOverlay;
|
|
30
|
+
private elementsContainer;
|
|
31
|
+
private propertyPanel;
|
|
32
|
+
private propContent;
|
|
33
|
+
private leftSidebar;
|
|
34
|
+
private rightSidebar;
|
|
35
|
+
private inputs;
|
|
36
|
+
constructor(options: DesignerOptions);
|
|
37
|
+
private init;
|
|
38
|
+
private renderTemplate;
|
|
39
|
+
private cacheDOM;
|
|
40
|
+
private renderEntityOptions;
|
|
41
|
+
private syncInputsFromLayout;
|
|
42
|
+
private bindEvents;
|
|
43
|
+
updatePreview(): Promise<void>;
|
|
44
|
+
private renderElementsList;
|
|
45
|
+
private selectElement;
|
|
46
|
+
private renderPropertyPanel;
|
|
47
|
+
private updateEditorOverlay;
|
|
48
|
+
private startElementResize;
|
|
49
|
+
private startElementDrag;
|
|
50
|
+
destroy(): void;
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.qrlayout-designer{--primary-color: #6366f1;--primary-hover: #4f46e5;--bg-color: #f1f5f9;--panel-bg: #ffffff;--panel-bg-alt: #f8fafc;--text-primary: #0f172a;--text-secondary: #64748b;--border-color: #e2e8f0;--input-bg: #ffffff;--danger-color: #ef4444;--success-color: #10b981;--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / .05);--shadow-md: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif;background-color:var(--bg-color);color:var(--text-primary);display:flex;flex-direction:column;overflow:hidden;width:100%;height:100%;box-sizing:border-box}.qrlayout-designer.dark-mode{--bg-color: #0f172a;--panel-bg: #1e293b;--panel-bg-alt: #334155;--text-primary: #f8fafc;--text-secondary: #94a3b8;--border-color: #334155;--input-bg: #1e293b}.qrlayout-designer ::-webkit-scrollbar{width:8px;height:8px}.qrlayout-designer ::-webkit-scrollbar-track{background:transparent}.qrlayout-designer ::-webkit-scrollbar-thumb{background:var(--border-color);border-radius:4px}.qrlayout-designer ::-webkit-scrollbar-thumb:hover{background:var(--text-secondary)}.qrlayout-designer *{box-sizing:border-box}.qrlayout-designer header{height:64px;background-color:var(--panel-bg);border-bottom:1px solid var(--border-color);display:flex;align-items:center;padding:0 24px;justify-content:space-between;box-shadow:var(--shadow-sm);z-index:10;flex-shrink:0}.qrlayout-designer h1{font-size:1.25rem;font-weight:700;margin:0;display:flex;align-items:center;gap:12px}.qrlayout-designer .logo-icon{width:32px;height:32px;background:linear-gradient(135deg,var(--primary-color),var(--primary-hover));border-radius:8px;display:flex;align-items:center;justify-content:center;color:#fff}.qrlayout-designer .main-container{flex:1;display:flex;flex-direction:column;overflow:hidden;position:relative}.qrlayout-designer .list-view{padding:32px;flex-direction:column;max-width:1200px;margin:0 auto;width:100%}.qrlayout-designer .view-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px}.qrlayout-designer .table-container{background-color:var(--panel-bg);border:1px solid var(--border-color);border-radius:12px;overflow:hidden;box-shadow:var(--shadow-md)}.qrlayout-designer table{width:100%;border-collapse:collapse;text-align:left}.qrlayout-designer th{background-color:var(--panel-bg-alt);padding:12px 24px;font-size:.75rem;text-transform:uppercase;font-weight:600;color:var(--text-secondary);border-bottom:1px solid var(--border-color)}.qrlayout-designer td{padding:16px 24px;font-size:.875rem;border-bottom:1px solid var(--border-color)}.qrlayout-designer tr:last-child td{border-bottom:none}.qrlayout-designer tr:hover td{background-color:var(--panel-bg-alt)}.qrlayout-designer .edit-view{display:flex;height:100%}.qrlayout-designer .sidebar{width:320px;background-color:var(--panel-bg);border-right:1px solid var(--border-color);display:flex;flex-direction:column;overflow-y:auto}.qrlayout-designer .sidebar-right{width:320px;background-color:var(--panel-bg);border-left:1px solid var(--border-color);display:flex;flex-direction:column;overflow-y:auto}.qrlayout-designer .sidebar-section{padding:16px;border-bottom:1px solid var(--border-color)}.qrlayout-designer .sidebar-section:last-child{border-bottom:none}.qrlayout-designer .sidebar-title{font-size:.75rem;text-transform:uppercase;color:var(--text-secondary);font-weight:700;letter-spacing:.05em;margin-bottom:16px;display:flex;justify-content:space-between;align-items:center}.qrlayout-designer .preview-area{flex:1;background-color:var(--bg-color);display:flex;flex-direction:column;align-items:center;justify-content:center;position:relative;padding:40px;background-image:radial-gradient(var(--border-color) 1px,transparent 1px);background-size:24px 24px;overflow:auto}.qrlayout-designer .canvas-wrapper{background:#fff;box-shadow:var(--shadow-lg);position:relative}.qrlayout-designer .form-group{margin-bottom:16px}.qrlayout-designer .form-group label{display:block;font-size:.75rem;color:var(--text-secondary);margin-bottom:6px;font-weight:500}.qrlayout-designer .form-row{display:flex;gap:12px}.qrlayout-designer input,.qrlayout-designer select,.qrlayout-designer textarea{width:100%;background-color:var(--input-bg);border:1px solid var(--border-color);color:var(--text-primary);padding:8px 12px;border-radius:6px;font-size:.875rem;transition:all .2s}.qrlayout-designer input:focus,.qrlayout-designer select:focus,.qrlayout-designer textarea:focus{outline:none;border-color:var(--primary-color);box-shadow:0 0 0 2px #6366f133}.qrlayout-designer .color-picker-wrapper{display:flex;align-items:center;gap:8px}.qrlayout-designer .color-preview{width:24px;height:24px;border-radius:4px;border:1px solid var(--border-color)}.qrlayout-designer .btn{display:inline-flex;align-items:center;justify-content:center;padding:8px 16px;border-radius:6px;font-size:.875rem;font-weight:600;cursor:pointer;transition:all .2s;border:1px solid transparent;gap:8px;height:36px}.qrlayout-designer .btn-primary{background-color:var(--primary-color);color:#fff}.qrlayout-designer .btn-primary:hover{background-color:var(--primary-hover)}.qrlayout-designer .btn-secondary{background-color:var(--panel-bg);border-color:var(--border-color);color:var(--text-primary)}.qrlayout-designer .btn-secondary:hover{background-color:var(--panel-bg-alt)}.qrlayout-designer .btn-outline{background-color:transparent;border-color:var(--border-color);color:var(--text-primary)}.qrlayout-designer .btn-outline:hover{background-color:#80808014}.qrlayout-designer .btn-danger{background-color:#ef44441a;color:var(--danger-color)}.qrlayout-designer .btn-danger:hover{background-color:#ef444433}.qrlayout-designer .btn-icon{width:32px;padding:0;height:32px}.qrlayout-designer .btn-block{width:100%}.qrlayout-designer .btn-sm{padding:4px 8px;font-size:.75rem;height:28px}.qrlayout-designer .element-list{display:flex;flex-direction:column;gap:4px}.qrlayout-designer .element-item{padding:8px 12px;border-radius:6px;display:flex;justify-content:space-between;align-items:center;cursor:pointer;transition:all .2s}.qrlayout-designer .element-item:hover{background-color:var(--panel-bg-alt)}.qrlayout-designer .element-item.active{background-color:#6366f11a;color:var(--primary-color)}.qrlayout-designer .element-info{display:flex;flex-direction:column}.qrlayout-designer .element-name{font-size:.8125rem;font-weight:600}.qrlayout-designer .element-sub{font-size:.6875rem;color:var(--text-secondary)}.qrlayout-designer .toggle-group{display:flex;border:1px solid var(--border-color);border-radius:6px;overflow:hidden;width:fit-content}.qrlayout-designer .toggle-btn{padding:6px 10px;background:var(--panel-bg);border:none;cursor:pointer;color:var(--text-secondary);border-right:1px solid var(--border-color);display:flex;align-items:center;justify-content:center}.qrlayout-designer .toggle-btn:last-child{border-right:none}.qrlayout-designer .toggle-btn.active{background:var(--panel-bg-alt);color:var(--primary-color)}.qrlayout-designer .field-buttons{display:flex;flex-wrap:wrap;gap:4px;margin-top:8px}.qrlayout-designer .field-pill{font-size:.625rem;padding:2px 6px;border-radius:4px;background:var(--panel-bg-alt);border:1px solid var(--border-color);cursor:pointer;color:var(--text-secondary)}.qrlayout-designer .field-pill:hover{border-color:var(--primary-color);color:var(--primary-color)}.qrlayout-designer .editor-item{position:absolute;border:2px solid transparent;box-sizing:border-box;cursor:move}.qrlayout-designer .editor-item.selected{border:2px solid var(--primary-color);background:#6366f10d}.qrlayout-designer .resize-handle{position:absolute;width:8px;height:8px;background:#fff;border:2px solid var(--primary-color);border-radius:50%;bottom:-5px;right:-5px;cursor:nwse-resize;z-index:10}.qrlayout-designer .toolbar{position:absolute;top:24px;right:24px;display:flex;gap:12px;z-index:20}.qrlayout-designer .sidebar-toggle{display:none;background:var(--panel-bg);border:1px solid var(--border-color);color:var(--text-primary);width:40px;height:40px;border-radius:8px;align-items:center;justify-content:center;cursor:pointer;position:absolute;top:12px;z-index:100;box-shadow:var(--shadow-md);transition:all .3s cubic-bezier(.4,0,.2,1)}.qrlayout-designer .sidebar-toggle:hover{background-color:var(--panel-bg-alt);border-color:var(--primary-color);color:var(--primary-color)}.qrlayout-designer #toggle-left{left:12px}.qrlayout-designer #toggle-right{right:12px}@media(max-width:768px){.qrlayout-designer header{padding:16px;height:auto;flex-direction:column;gap:12px;align-items:flex-start}.qrlayout-designer header h1{font-size:1.125rem}.qrlayout-designer .logo-icon{width:28px;height:28px;font-size:.75rem}.qrlayout-designer header .btn{padding:6px 10px;font-size:.75rem;height:32px}.qrlayout-designer .sidebar,.qrlayout-designer .sidebar-right{position:fixed;top:64px;bottom:0;z-index:50;width:100%;max-width:320px;box-shadow:var(--shadow-lg);transition:transform .3s cubic-bezier(.4,0,.2,1)}.qrlayout-designer .sidebar{left:0;transform:translate(-100%)}.qrlayout-designer .sidebar-right{right:0;transform:translate(100%)}.qrlayout-designer .sidebar.show,.qrlayout-designer .sidebar-right.show{transform:translate(0)}.qrlayout-designer .sidebar-toggle{display:flex}.qrlayout-designer .preview-area{padding:20px}.qrlayout-designer .canvas-wrapper{max-width:100%;overflow:auto}@media(max-width:480px){.qrlayout-designer header h1 span{display:none}.qrlayout-designer .sidebar-toggle{width:36px;height:36px;top:8px}.qrlayout-designer #toggle-left{left:8px}.qrlayout-designer #toggle-right{right:8px}.qrlayout-designer .preview-area{padding:10px}}}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { StickerPrinter as v } from "qrlayout-core";
|
|
2
|
+
import { StickerPrinter as w } from "qrlayout-core";
|
|
3
|
+
class m {
|
|
4
|
+
constructor(e) {
|
|
5
|
+
this.selectedElementId = null, this.isDarkMode = !1, this.pxPerUnit = 1, this.container = e.element, this.printer = new v(), this.onSaveCallback = e.onSave, this.entitySchemas = e.entitySchemas || {}, this.currentLayout = e.initialLayout || {
|
|
6
|
+
id: "layout-" + Date.now(),
|
|
7
|
+
name: "New Layout",
|
|
8
|
+
targetEntity: "",
|
|
9
|
+
width: 100,
|
|
10
|
+
height: 60,
|
|
11
|
+
unit: "mm",
|
|
12
|
+
backgroundColor: "#ffffff",
|
|
13
|
+
elements: []
|
|
14
|
+
}, this.init();
|
|
15
|
+
}
|
|
16
|
+
init() {
|
|
17
|
+
this.renderTemplate(), this.cacheDOM(), this.renderEntityOptions(), this.syncInputsFromLayout(), this.bindEvents(), this.renderElementsList(), this.updatePreview();
|
|
18
|
+
}
|
|
19
|
+
renderTemplate() {
|
|
20
|
+
this.container.classList.add("qrlayout-designer"), this.container.innerHTML = `
|
|
21
|
+
<header>
|
|
22
|
+
<div data-el="header-left"></div>
|
|
23
|
+
<div style="display: flex; gap: 12px; align-items: center;">
|
|
24
|
+
<button class="btn btn-icon btn-outline" data-action="toggle-theme" title="Toggle Dark Mode">
|
|
25
|
+
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: none;"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>
|
|
26
|
+
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
|
|
27
|
+
</button>
|
|
28
|
+
<button class="btn btn-primary" data-action="save">Save Layout</button>
|
|
29
|
+
</div>
|
|
30
|
+
</header>
|
|
31
|
+
<div class="main-container">
|
|
32
|
+
<div class="edit-view" style="display: flex; flex: 1; height: 100%;">
|
|
33
|
+
<!-- LEFT SIDEBAR: CONFIG & ELEMENTS -->
|
|
34
|
+
<aside class="sidebar">
|
|
35
|
+
<!-- Configuration -->
|
|
36
|
+
<div class="sidebar-section">
|
|
37
|
+
<div class="sidebar-title">Layout Settings</div>
|
|
38
|
+
<div class="form-group">
|
|
39
|
+
<label>Target Entity</label>
|
|
40
|
+
<select data-input="entity">
|
|
41
|
+
<option value="">Select Entity...</option>
|
|
42
|
+
<!-- Populated dynamically -->
|
|
43
|
+
</select>
|
|
44
|
+
</div>
|
|
45
|
+
<div class="form-group">
|
|
46
|
+
<label>Internal Layout Name</label>
|
|
47
|
+
<input type="text" data-input="name" placeholder="Standard Badge" />
|
|
48
|
+
</div>
|
|
49
|
+
<div class="form-row">
|
|
50
|
+
<div class="form-group" style="flex: 1">
|
|
51
|
+
<label data-label="width">Width (mm)</label>
|
|
52
|
+
<input type="number" data-input="width" value="100" step="0.01" />
|
|
53
|
+
</div>
|
|
54
|
+
<div class="form-group" style="flex: 1">
|
|
55
|
+
<label data-label="height">Height (mm)</label>
|
|
56
|
+
<input type="number" data-input="height" value="60" step="0.01" />
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
<div class="form-group">
|
|
60
|
+
<label>Measurement Unit</label>
|
|
61
|
+
<select data-input="unit">
|
|
62
|
+
<option value="mm">Millimeters (mm)</option>
|
|
63
|
+
<option value="cm">Centimeters (cm)</option>
|
|
64
|
+
<option value="in">Inches (in)</option>
|
|
65
|
+
<option value="px">Pixels (px)</option>
|
|
66
|
+
</select>
|
|
67
|
+
</div>
|
|
68
|
+
<div class="form-group">
|
|
69
|
+
<label>Base Background</label>
|
|
70
|
+
<div class="color-picker-wrapper">
|
|
71
|
+
<div data-el="bg-preview" class="color-preview" style="background: #ffffff"></div>
|
|
72
|
+
<input type="text" data-input="bg" value="#ffffff" />
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<!-- Elements List -->
|
|
78
|
+
<div class="sidebar-section">
|
|
79
|
+
<div class="sidebar-title">
|
|
80
|
+
Elements
|
|
81
|
+
<div style="display: flex; gap: 6px">
|
|
82
|
+
<button class="btn btn-outline btn-sm" data-action="add-text" title="Add Text">+ Text</button>
|
|
83
|
+
<button class="btn btn-outline btn-sm" data-action="add-qr" title="Add QR">+ QR</button>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
<div data-el="elements-container" class="element-list" style="margin-top: 8px;"></div>
|
|
87
|
+
</div>
|
|
88
|
+
</aside>
|
|
89
|
+
|
|
90
|
+
<!-- CENTER: CANVAS -->
|
|
91
|
+
<main class="preview-area">
|
|
92
|
+
<button id="toggle-left" class="sidebar-toggle" title="Toggle Settings">☰</button>
|
|
93
|
+
<button id="toggle-right" class="sidebar-toggle" title="Toggle Properties" style="display: none;">✎</button>
|
|
94
|
+
|
|
95
|
+
<div class="canvas-wrapper">
|
|
96
|
+
<canvas data-el="preview-canvas"></canvas>
|
|
97
|
+
<div data-el="editor-overlay" class="editor-overlay"></div>
|
|
98
|
+
</div>
|
|
99
|
+
</main>
|
|
100
|
+
|
|
101
|
+
<!-- RIGHT SIDEBAR: PROPERTIES -->
|
|
102
|
+
<aside class="sidebar-right" data-el="property-panel" style="display: none;">
|
|
103
|
+
<div class="sidebar-section">
|
|
104
|
+
<div class="sidebar-title">Element Properties</div>
|
|
105
|
+
<div data-el="prop-content"></div>
|
|
106
|
+
<button class="btn btn-danger btn-block" data-action="delete-element" style="margin-top: 24px">Delete Element</button>
|
|
107
|
+
</div>
|
|
108
|
+
</aside>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
`;
|
|
112
|
+
}
|
|
113
|
+
cacheDOM() {
|
|
114
|
+
const e = (i) => this.container.querySelector(i), t = (i) => this.container.querySelector(`[data-input="${i}"]`);
|
|
115
|
+
this.canvas = e('[data-el="preview-canvas"]'), this.editorOverlay = e('[data-el="editor-overlay"]'), this.elementsContainer = e('[data-el="elements-container"]'), this.propertyPanel = e('[data-el="property-panel"]'), this.propContent = e('[data-el="prop-content"]'), this.leftSidebar = e(".sidebar"), this.rightSidebar = e(".sidebar-right"), this.inputs = {
|
|
116
|
+
entity: t("entity"),
|
|
117
|
+
name: t("name"),
|
|
118
|
+
width: t("width"),
|
|
119
|
+
height: t("height"),
|
|
120
|
+
unit: t("unit"),
|
|
121
|
+
labelWidth: e('[data-label="width"]'),
|
|
122
|
+
labelHeight: e('[data-label="height"]'),
|
|
123
|
+
bg: t("bg"),
|
|
124
|
+
bgPreview: e('[data-el="bg-preview"]')
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
renderEntityOptions() {
|
|
128
|
+
const e = this.inputs.entity;
|
|
129
|
+
for (; e.options.length > 1; )
|
|
130
|
+
e.remove(1);
|
|
131
|
+
Object.keys(this.entitySchemas).forEach((t) => {
|
|
132
|
+
const i = this.entitySchemas[t], n = document.createElement("option");
|
|
133
|
+
n.value = t, n.text = i.label || t, e.add(n);
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
syncInputsFromLayout() {
|
|
137
|
+
this.inputs.entity.value = this.currentLayout.targetEntity || "", this.inputs.name.value = this.currentLayout.name, this.inputs.width.value = String(this.currentLayout.width), this.inputs.height.value = String(this.currentLayout.height), this.inputs.unit.value = this.currentLayout.unit, this.inputs.labelWidth.innerText = `Width (${this.currentLayout.unit})`, this.inputs.labelHeight.innerText = `Height (${this.currentLayout.unit})`, this.inputs.bg.value = this.currentLayout.backgroundColor || "#ffffff", this.inputs.bgPreview.style.backgroundColor = this.inputs.bg.value;
|
|
138
|
+
}
|
|
139
|
+
bindEvents() {
|
|
140
|
+
this.container.querySelector('[data-action="toggle-theme"]')?.addEventListener("click", (t) => {
|
|
141
|
+
this.isDarkMode = !this.isDarkMode, this.container.classList.toggle("dark-mode", this.isDarkMode);
|
|
142
|
+
const i = t.currentTarget, n = i.querySelector(".sun-icon"), r = i.querySelector(".moon-icon");
|
|
143
|
+
this.isDarkMode ? (n.style.display = "block", r.style.display = "none") : (n.style.display = "none", r.style.display = "block");
|
|
144
|
+
}), this.container.querySelector('[data-action="export-json"]')?.addEventListener("click", () => {
|
|
145
|
+
const t = new Blob([JSON.stringify(this.currentLayout, null, 2)], { type: "application/json" }), i = URL.createObjectURL(t), n = document.createElement("a");
|
|
146
|
+
n.href = i, n.download = `${this.currentLayout.name.toLowerCase().replace(/ /g, "-")}.json`, n.click();
|
|
147
|
+
}), this.container.querySelector('[data-action="save"]')?.addEventListener("click", () => {
|
|
148
|
+
this.onSaveCallback && this.onSaveCallback(this.currentLayout);
|
|
149
|
+
}), this.container.querySelector("#toggle-left")?.addEventListener("click", () => {
|
|
150
|
+
this.leftSidebar.classList.toggle("show");
|
|
151
|
+
}), this.container.querySelector("#toggle-right")?.addEventListener("click", () => {
|
|
152
|
+
this.rightSidebar.classList.toggle("show");
|
|
153
|
+
}), this.inputs.entity.onchange = (t) => {
|
|
154
|
+
this.currentLayout.targetEntity = t.target.value, this.renderPropertyPanel(), this.updatePreview();
|
|
155
|
+
}, this.inputs.name.oninput = (t) => this.currentLayout.name = t.target.value, this.inputs.width.oninput = (t) => {
|
|
156
|
+
this.currentLayout.width = parseFloat(t.target.value) || 100, this.updatePreview();
|
|
157
|
+
}, this.inputs.height.oninput = (t) => {
|
|
158
|
+
this.currentLayout.height = parseFloat(t.target.value) || 60, this.updatePreview();
|
|
159
|
+
}, this.inputs.unit.onchange = (t) => {
|
|
160
|
+
this.currentLayout.unit = t.target.value, this.inputs.labelWidth.innerText = `Width (${this.currentLayout.unit})`, this.inputs.labelHeight.innerText = `Height (${this.currentLayout.unit})`, this.updatePreview();
|
|
161
|
+
}, this.inputs.bg.oninput = (t) => {
|
|
162
|
+
this.currentLayout.backgroundColor = t.target.value, this.inputs.bgPreview.style.backgroundColor = this.currentLayout.backgroundColor, this.updatePreview();
|
|
163
|
+
}, this.container.querySelector('[data-action="add-text"]')?.addEventListener("click", () => {
|
|
164
|
+
const t = "t" + Date.now();
|
|
165
|
+
this.currentLayout.elements.push({ id: t, type: "text", x: 10, y: 10, w: 40, h: 10, content: "New Text" }), this.selectElement(t), this.updatePreview();
|
|
166
|
+
}), this.container.querySelector('[data-action="add-qr"]')?.addEventListener("click", () => {
|
|
167
|
+
const t = "q" + Date.now();
|
|
168
|
+
this.currentLayout.elements.push({ id: t, type: "qr", x: 5, y: 5, w: 20, h: 20, content: "{{id}}" }), this.selectElement(t), this.updatePreview();
|
|
169
|
+
}), this.container.querySelector('[data-action="delete-element"]')?.addEventListener("click", () => {
|
|
170
|
+
this.currentLayout.elements = this.currentLayout.elements.filter((t) => t.id !== this.selectedElementId), this.selectElement(null), this.updatePreview();
|
|
171
|
+
}), new ResizeObserver(() => {
|
|
172
|
+
this.container.offsetWidth > 768 && (this.leftSidebar.classList.remove("show"), this.rightSidebar.classList.remove("show")), this.renderPropertyPanel();
|
|
173
|
+
}).observe(this.container);
|
|
174
|
+
}
|
|
175
|
+
async updatePreview() {
|
|
176
|
+
if (!this.canvas || !this.currentLayout) return;
|
|
177
|
+
const e = this.currentLayout.targetEntity && this.entitySchemas[this.currentLayout.targetEntity] ? this.entitySchemas[this.currentLayout.targetEntity].sampleData : {};
|
|
178
|
+
await this.printer.renderToCanvas(this.currentLayout, e, this.canvas);
|
|
179
|
+
const t = this.canvas.getBoundingClientRect();
|
|
180
|
+
this.pxPerUnit = t.width / this.currentLayout.width, this.updateEditorOverlay();
|
|
181
|
+
}
|
|
182
|
+
renderElementsList() {
|
|
183
|
+
this.elementsContainer.innerHTML = "", this.currentLayout.elements.forEach((e) => {
|
|
184
|
+
const t = document.createElement("div");
|
|
185
|
+
t.className = `element-item ${this.selectedElementId === e.id ? "active" : ""}`, t.innerHTML = `
|
|
186
|
+
<div class="element-info">
|
|
187
|
+
<span class="element-name">${e.type.toUpperCase()}</span>
|
|
188
|
+
<span class="element-sub">${String(e.content).substring(0, 20)}</span>
|
|
189
|
+
</div>
|
|
190
|
+
`, t.onclick = () => this.selectElement(e.id), this.elementsContainer.appendChild(t);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
selectElement(e) {
|
|
194
|
+
this.selectedElementId = e, this.renderElementsList(), this.renderPropertyPanel(), this.updateEditorOverlay(), e && this.container.offsetWidth <= 768 && this.rightSidebar.classList.add("show");
|
|
195
|
+
}
|
|
196
|
+
renderPropertyPanel() {
|
|
197
|
+
const e = this.container.querySelector("#toggle-right");
|
|
198
|
+
if (!this.selectedElementId) {
|
|
199
|
+
this.propertyPanel.style.display = "none", e && (e.style.display = "none");
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
this.container.offsetWidth <= 768 ? e && (e.style.display = "flex") : e && (e.style.display = "none");
|
|
203
|
+
const t = this.currentLayout.elements.find((s) => s.id === this.selectedElementId);
|
|
204
|
+
if (!t) return;
|
|
205
|
+
this.propertyPanel.style.display = "block", this.propContent.innerHTML = `
|
|
206
|
+
<div class="form-group">
|
|
207
|
+
${t.type === "qr" ? `
|
|
208
|
+
<label>Field Separator</label>
|
|
209
|
+
<input type="text" id="prop-qr-separator" placeholder="e.g. | or -" value="${t.qrSeparator || ""}">
|
|
210
|
+
` : ""}
|
|
211
|
+
<label>Content</label>
|
|
212
|
+
<textarea data-prop="content-val" rows="2">${t.content}</textarea>
|
|
213
|
+
<div class="field-buttons" data-el="field-suggestions"></div>
|
|
214
|
+
</div>
|
|
215
|
+
<div class="form-row">
|
|
216
|
+
<div class="form-group" style="flex:1;"><label>X (pos)</label><input type="number" step="0.01" data-prop="x" value="${t.x.toFixed(2)}"></div>
|
|
217
|
+
<div class="form-group" style="flex:1;"><label>Y (pos)</label><input type="number" step="0.01" data-prop="y" value="${t.y.toFixed(2)}"></div>
|
|
218
|
+
</div>
|
|
219
|
+
<div class="form-row">
|
|
220
|
+
<div class="form-group" style="flex:1;"><label>Width</label><input type="number" step="0.01" data-prop="w" value="${t.w.toFixed(2)}"></div>
|
|
221
|
+
<div class="form-group" style="flex:1;"><label>Height</label><input type="number" step="0.01" data-prop="h" value="${t.h.toFixed(2)}"></div>
|
|
222
|
+
</div>
|
|
223
|
+
${t.type === "text" ? `
|
|
224
|
+
<div style="height: 1px; background: var(--border-color); margin: 16px 0;"></div>
|
|
225
|
+
<div class="form-row">
|
|
226
|
+
<div class="form-group" style="flex:1;">
|
|
227
|
+
<label>Font Size</label>
|
|
228
|
+
<input type="number" data-prop="fontSize" value="${t.style?.fontSize || 12}">
|
|
229
|
+
</div>
|
|
230
|
+
<div class="form-group" style="flex:1;">
|
|
231
|
+
<label>Font Weight</label>
|
|
232
|
+
<select data-prop="fontWeight">
|
|
233
|
+
<option value="normal" ${t.style?.fontWeight === "normal" ? "selected" : ""}>Normal</option>
|
|
234
|
+
<option value="bold" ${t.style?.fontWeight === "bold" ? "selected" : ""}>Bold</option>
|
|
235
|
+
</select>
|
|
236
|
+
</div>
|
|
237
|
+
</div>
|
|
238
|
+
<div class="form-group">
|
|
239
|
+
<label>Horizontal Align</label>
|
|
240
|
+
<div class="toggle-group" style="width: 100%;">
|
|
241
|
+
<button class="toggle-btn prop-align-h ${t.style?.textAlign === "left" ? "active" : ""}" data-val="left" style="flex:1;">Left</button>
|
|
242
|
+
<button class="toggle-btn prop-align-h ${t.style?.textAlign === "center" ? "active" : ""}" data-val="center" style="flex:1;">Center</button>
|
|
243
|
+
<button class="toggle-btn prop-align-h ${t.style?.textAlign === "right" ? "active" : ""}" data-val="right" style="flex:1;">Right</button>
|
|
244
|
+
</div>
|
|
245
|
+
</div>
|
|
246
|
+
<div class="form-group">
|
|
247
|
+
<label>Vertical Align</label>
|
|
248
|
+
<div class="toggle-group" style="width: 100%;">
|
|
249
|
+
<button class="toggle-btn prop-align-v ${t.style?.verticalAlign === "top" ? "active" : ""}" data-val="top" style="flex:1;">Top</button>
|
|
250
|
+
<button class="toggle-btn prop-align-v ${t.style?.verticalAlign === "middle" ? "active" : ""}" data-val="middle" style="flex:1;">Middle</button>
|
|
251
|
+
<button class="toggle-btn prop-align-v ${t.style?.verticalAlign === "bottom" ? "active" : ""}" data-val="bottom" style="flex:1;">Bottom</button>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
` : ""}
|
|
255
|
+
`;
|
|
256
|
+
const i = this.propContent.querySelector('[data-el="field-suggestions"]'), n = this.currentLayout.targetEntity ? this.entitySchemas[this.currentLayout.targetEntity] : null;
|
|
257
|
+
n && i && n.fields.forEach((s) => {
|
|
258
|
+
const a = document.createElement("div");
|
|
259
|
+
a.className = "field-pill", a.innerText = `+ ${s.label}`, a.onclick = () => {
|
|
260
|
+
t.content += `{{${s.name}}}`, this.renderPropertyPanel(), this.updatePreview();
|
|
261
|
+
}, i.appendChild(a);
|
|
262
|
+
});
|
|
263
|
+
const r = this.propContent.querySelector("#prop-qr-separator");
|
|
264
|
+
r && r.addEventListener("input", (s) => {
|
|
265
|
+
t.qrSeparator = s.target.value, this.updatePreview();
|
|
266
|
+
});
|
|
267
|
+
const l = (s, a, o = !1, d) => {
|
|
268
|
+
const c = this.propContent.querySelector(`[data-prop="${s}"]`);
|
|
269
|
+
c && c.addEventListener("input", (p) => {
|
|
270
|
+
const h = p.target.value, u = o ? parseFloat(h) || 0 : h;
|
|
271
|
+
d ? (t.style || (t.style = {}), t.style[d] = u) : t[a] = u, this.updatePreview();
|
|
272
|
+
});
|
|
273
|
+
};
|
|
274
|
+
l("content-val", "content"), l("x", "x", !0), l("y", "y", !0), l("w", "w", !0), l("h", "h", !0), l("fontSize", "style", !0, "fontSize"), l("fontWeight", "style", !1, "fontWeight"), this.propContent.querySelectorAll(".prop-align-h").forEach((s) => {
|
|
275
|
+
s.addEventListener("click", () => {
|
|
276
|
+
t.style || (t.style = {}), t.style.textAlign = s.dataset.val, this.renderPropertyPanel(), this.updatePreview();
|
|
277
|
+
});
|
|
278
|
+
}), this.propContent.querySelectorAll(".prop-align-v").forEach((s) => {
|
|
279
|
+
s.addEventListener("click", () => {
|
|
280
|
+
t.style || (t.style = {}), t.style.verticalAlign = s.dataset.val, this.renderPropertyPanel(), this.updatePreview();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
updateEditorOverlay() {
|
|
285
|
+
!this.editorOverlay || !this.canvas || (this.editorOverlay.style.width = this.canvas.style.width, this.editorOverlay.style.height = this.canvas.style.height, this.editorOverlay.innerHTML = "", this.currentLayout.elements.forEach((e) => {
|
|
286
|
+
const t = document.createElement("div");
|
|
287
|
+
if (t.className = `editor-item ${this.selectedElementId === e.id ? "selected" : ""}`, t.style.left = `${e.x * this.pxPerUnit}px`, t.style.top = `${e.y * this.pxPerUnit}px`, t.style.width = `${e.w * this.pxPerUnit}px`, t.style.height = `${e.h * this.pxPerUnit}px`, this.selectedElementId === e.id) {
|
|
288
|
+
const i = document.createElement("div");
|
|
289
|
+
i.className = "resize-handle", i.onmousedown = (n) => {
|
|
290
|
+
n.preventDefault(), n.stopPropagation(), this.startElementResize(n, e);
|
|
291
|
+
}, t.appendChild(i);
|
|
292
|
+
}
|
|
293
|
+
t.onmousedown = (i) => {
|
|
294
|
+
i.preventDefault(), this.selectElement(e.id), this.startElementDrag(i, e);
|
|
295
|
+
}, this.editorOverlay.appendChild(t);
|
|
296
|
+
}));
|
|
297
|
+
}
|
|
298
|
+
startElementResize(e, t) {
|
|
299
|
+
const i = e.clientX, n = e.clientY, r = t.w, l = t.h, s = (o) => {
|
|
300
|
+
t.w = Math.max(1, r + (o.clientX - i) / this.pxPerUnit), t.h = Math.max(1, l + (o.clientY - n) / this.pxPerUnit), this.updatePreview(), this.renderPropertyPanel();
|
|
301
|
+
}, a = () => {
|
|
302
|
+
window.removeEventListener("mousemove", s), window.removeEventListener("mouseup", a);
|
|
303
|
+
};
|
|
304
|
+
window.addEventListener("mousemove", s), window.addEventListener("mouseup", a);
|
|
305
|
+
}
|
|
306
|
+
startElementDrag(e, t) {
|
|
307
|
+
const i = e.clientX, n = e.clientY, r = t.x, l = t.y, s = (o) => {
|
|
308
|
+
t.x = r + (o.clientX - i) / this.pxPerUnit, t.y = l + (o.clientY - n) / this.pxPerUnit, this.updatePreview(), this.renderPropertyPanel();
|
|
309
|
+
}, a = () => {
|
|
310
|
+
window.removeEventListener("mousemove", s), window.removeEventListener("mouseup", a);
|
|
311
|
+
};
|
|
312
|
+
window.addEventListener("mousemove", s), window.addEventListener("mouseup", a);
|
|
313
|
+
}
|
|
314
|
+
destroy() {
|
|
315
|
+
this.container.innerHTML = "", this.container.classList.remove("qrlayout-designer");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
export {
|
|
319
|
+
m as QRLayoutDesigner,
|
|
320
|
+
w as StickerPrinter
|
|
321
|
+
};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
(function(o,c){typeof exports=="object"&&typeof module<"u"?c(exports,require("qrlayout-core")):typeof define=="function"&&define.amd?define(["exports","qrlayout-core"],c):(o=typeof globalThis<"u"?globalThis:o||self,c(o.QRLayoutUI={},o.QRLayoutCore))})(this,(function(o,c){"use strict";class y{constructor(e){this.selectedElementId=null,this.isDarkMode=!1,this.pxPerUnit=1,this.container=e.element,this.printer=new c.StickerPrinter,this.onSaveCallback=e.onSave,this.entitySchemas=e.entitySchemas||{},this.currentLayout=e.initialLayout||{id:"layout-"+Date.now(),name:"New Layout",targetEntity:"",width:100,height:60,unit:"mm",backgroundColor:"#ffffff",elements:[]},this.init()}init(){this.renderTemplate(),this.cacheDOM(),this.renderEntityOptions(),this.syncInputsFromLayout(),this.bindEvents(),this.renderElementsList(),this.updatePreview()}renderTemplate(){this.container.classList.add("qrlayout-designer"),this.container.innerHTML=`
|
|
2
|
+
<header>
|
|
3
|
+
<div data-el="header-left"></div>
|
|
4
|
+
<div style="display: flex; gap: 12px; align-items: center;">
|
|
5
|
+
<button class="btn btn-icon btn-outline" data-action="toggle-theme" title="Toggle Dark Mode">
|
|
6
|
+
<svg class="sun-icon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display: none;"><circle cx="12" cy="12" r="4"/><path d="M12 2v2"/><path d="M12 20v2"/><path d="m4.93 4.93 1.41 1.41"/><path d="m17.66 17.66 1.41 1.41"/><path d="M2 12h2"/><path d="M20 12h2"/><path d="m6.34 17.66-1.41 1.41"/><path d="m19.07 4.93-1.41 1.41"/></svg>
|
|
7
|
+
<svg class="moon-icon" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z"/></svg>
|
|
8
|
+
</button>
|
|
9
|
+
<button class="btn btn-primary" data-action="save">Save Layout</button>
|
|
10
|
+
</div>
|
|
11
|
+
</header>
|
|
12
|
+
<div class="main-container">
|
|
13
|
+
<div class="edit-view" style="display: flex; flex: 1; height: 100%;">
|
|
14
|
+
<!-- LEFT SIDEBAR: CONFIG & ELEMENTS -->
|
|
15
|
+
<aside class="sidebar">
|
|
16
|
+
<!-- Configuration -->
|
|
17
|
+
<div class="sidebar-section">
|
|
18
|
+
<div class="sidebar-title">Layout Settings</div>
|
|
19
|
+
<div class="form-group">
|
|
20
|
+
<label>Target Entity</label>
|
|
21
|
+
<select data-input="entity">
|
|
22
|
+
<option value="">Select Entity...</option>
|
|
23
|
+
<!-- Populated dynamically -->
|
|
24
|
+
</select>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="form-group">
|
|
27
|
+
<label>Internal Layout Name</label>
|
|
28
|
+
<input type="text" data-input="name" placeholder="Standard Badge" />
|
|
29
|
+
</div>
|
|
30
|
+
<div class="form-row">
|
|
31
|
+
<div class="form-group" style="flex: 1">
|
|
32
|
+
<label data-label="width">Width (mm)</label>
|
|
33
|
+
<input type="number" data-input="width" value="100" step="0.01" />
|
|
34
|
+
</div>
|
|
35
|
+
<div class="form-group" style="flex: 1">
|
|
36
|
+
<label data-label="height">Height (mm)</label>
|
|
37
|
+
<input type="number" data-input="height" value="60" step="0.01" />
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
<div class="form-group">
|
|
41
|
+
<label>Measurement Unit</label>
|
|
42
|
+
<select data-input="unit">
|
|
43
|
+
<option value="mm">Millimeters (mm)</option>
|
|
44
|
+
<option value="cm">Centimeters (cm)</option>
|
|
45
|
+
<option value="in">Inches (in)</option>
|
|
46
|
+
<option value="px">Pixels (px)</option>
|
|
47
|
+
</select>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="form-group">
|
|
50
|
+
<label>Base Background</label>
|
|
51
|
+
<div class="color-picker-wrapper">
|
|
52
|
+
<div data-el="bg-preview" class="color-preview" style="background: #ffffff"></div>
|
|
53
|
+
<input type="text" data-input="bg" value="#ffffff" />
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<!-- Elements List -->
|
|
59
|
+
<div class="sidebar-section">
|
|
60
|
+
<div class="sidebar-title">
|
|
61
|
+
Elements
|
|
62
|
+
<div style="display: flex; gap: 6px">
|
|
63
|
+
<button class="btn btn-outline btn-sm" data-action="add-text" title="Add Text">+ Text</button>
|
|
64
|
+
<button class="btn btn-outline btn-sm" data-action="add-qr" title="Add QR">+ QR</button>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<div data-el="elements-container" class="element-list" style="margin-top: 8px;"></div>
|
|
68
|
+
</div>
|
|
69
|
+
</aside>
|
|
70
|
+
|
|
71
|
+
<!-- CENTER: CANVAS -->
|
|
72
|
+
<main class="preview-area">
|
|
73
|
+
<button id="toggle-left" class="sidebar-toggle" title="Toggle Settings">☰</button>
|
|
74
|
+
<button id="toggle-right" class="sidebar-toggle" title="Toggle Properties" style="display: none;">✎</button>
|
|
75
|
+
|
|
76
|
+
<div class="canvas-wrapper">
|
|
77
|
+
<canvas data-el="preview-canvas"></canvas>
|
|
78
|
+
<div data-el="editor-overlay" class="editor-overlay"></div>
|
|
79
|
+
</div>
|
|
80
|
+
</main>
|
|
81
|
+
|
|
82
|
+
<!-- RIGHT SIDEBAR: PROPERTIES -->
|
|
83
|
+
<aside class="sidebar-right" data-el="property-panel" style="display: none;">
|
|
84
|
+
<div class="sidebar-section">
|
|
85
|
+
<div class="sidebar-title">Element Properties</div>
|
|
86
|
+
<div data-el="prop-content"></div>
|
|
87
|
+
<button class="btn btn-danger btn-block" data-action="delete-element" style="margin-top: 24px">Delete Element</button>
|
|
88
|
+
</div>
|
|
89
|
+
</aside>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
`}cacheDOM(){const e=i=>this.container.querySelector(i),t=i=>this.container.querySelector(`[data-input="${i}"]`);this.canvas=e('[data-el="preview-canvas"]'),this.editorOverlay=e('[data-el="editor-overlay"]'),this.elementsContainer=e('[data-el="elements-container"]'),this.propertyPanel=e('[data-el="property-panel"]'),this.propContent=e('[data-el="prop-content"]'),this.leftSidebar=e(".sidebar"),this.rightSidebar=e(".sidebar-right"),this.inputs={entity:t("entity"),name:t("name"),width:t("width"),height:t("height"),unit:t("unit"),labelWidth:e('[data-label="width"]'),labelHeight:e('[data-label="height"]'),bg:t("bg"),bgPreview:e('[data-el="bg-preview"]')}}renderEntityOptions(){const e=this.inputs.entity;for(;e.options.length>1;)e.remove(1);Object.keys(this.entitySchemas).forEach(t=>{const i=this.entitySchemas[t],n=document.createElement("option");n.value=t,n.text=i.label||t,e.add(n)})}syncInputsFromLayout(){this.inputs.entity.value=this.currentLayout.targetEntity||"",this.inputs.name.value=this.currentLayout.name,this.inputs.width.value=String(this.currentLayout.width),this.inputs.height.value=String(this.currentLayout.height),this.inputs.unit.value=this.currentLayout.unit,this.inputs.labelWidth.innerText=`Width (${this.currentLayout.unit})`,this.inputs.labelHeight.innerText=`Height (${this.currentLayout.unit})`,this.inputs.bg.value=this.currentLayout.backgroundColor||"#ffffff",this.inputs.bgPreview.style.backgroundColor=this.inputs.bg.value}bindEvents(){this.container.querySelector('[data-action="toggle-theme"]')?.addEventListener("click",t=>{this.isDarkMode=!this.isDarkMode,this.container.classList.toggle("dark-mode",this.isDarkMode);const i=t.currentTarget,n=i.querySelector(".sun-icon"),r=i.querySelector(".moon-icon");this.isDarkMode?(n.style.display="block",r.style.display="none"):(n.style.display="none",r.style.display="block")}),this.container.querySelector('[data-action="export-json"]')?.addEventListener("click",()=>{const t=new Blob([JSON.stringify(this.currentLayout,null,2)],{type:"application/json"}),i=URL.createObjectURL(t),n=document.createElement("a");n.href=i,n.download=`${this.currentLayout.name.toLowerCase().replace(/ /g,"-")}.json`,n.click()}),this.container.querySelector('[data-action="save"]')?.addEventListener("click",()=>{this.onSaveCallback&&this.onSaveCallback(this.currentLayout)}),this.container.querySelector("#toggle-left")?.addEventListener("click",()=>{this.leftSidebar.classList.toggle("show")}),this.container.querySelector("#toggle-right")?.addEventListener("click",()=>{this.rightSidebar.classList.toggle("show")}),this.inputs.entity.onchange=t=>{this.currentLayout.targetEntity=t.target.value,this.renderPropertyPanel(),this.updatePreview()},this.inputs.name.oninput=t=>this.currentLayout.name=t.target.value,this.inputs.width.oninput=t=>{this.currentLayout.width=parseFloat(t.target.value)||100,this.updatePreview()},this.inputs.height.oninput=t=>{this.currentLayout.height=parseFloat(t.target.value)||60,this.updatePreview()},this.inputs.unit.onchange=t=>{this.currentLayout.unit=t.target.value,this.inputs.labelWidth.innerText=`Width (${this.currentLayout.unit})`,this.inputs.labelHeight.innerText=`Height (${this.currentLayout.unit})`,this.updatePreview()},this.inputs.bg.oninput=t=>{this.currentLayout.backgroundColor=t.target.value,this.inputs.bgPreview.style.backgroundColor=this.currentLayout.backgroundColor,this.updatePreview()},this.container.querySelector('[data-action="add-text"]')?.addEventListener("click",()=>{const t="t"+Date.now();this.currentLayout.elements.push({id:t,type:"text",x:10,y:10,w:40,h:10,content:"New Text"}),this.selectElement(t),this.updatePreview()}),this.container.querySelector('[data-action="add-qr"]')?.addEventListener("click",()=>{const t="q"+Date.now();this.currentLayout.elements.push({id:t,type:"qr",x:5,y:5,w:20,h:20,content:"{{id}}"}),this.selectElement(t),this.updatePreview()}),this.container.querySelector('[data-action="delete-element"]')?.addEventListener("click",()=>{this.currentLayout.elements=this.currentLayout.elements.filter(t=>t.id!==this.selectedElementId),this.selectElement(null),this.updatePreview()}),new ResizeObserver(()=>{this.container.offsetWidth>768&&(this.leftSidebar.classList.remove("show"),this.rightSidebar.classList.remove("show")),this.renderPropertyPanel()}).observe(this.container)}async updatePreview(){if(!this.canvas||!this.currentLayout)return;const e=this.currentLayout.targetEntity&&this.entitySchemas[this.currentLayout.targetEntity]?this.entitySchemas[this.currentLayout.targetEntity].sampleData:{};await this.printer.renderToCanvas(this.currentLayout,e,this.canvas);const t=this.canvas.getBoundingClientRect();this.pxPerUnit=t.width/this.currentLayout.width,this.updateEditorOverlay()}renderElementsList(){this.elementsContainer.innerHTML="",this.currentLayout.elements.forEach(e=>{const t=document.createElement("div");t.className=`element-item ${this.selectedElementId===e.id?"active":""}`,t.innerHTML=`
|
|
93
|
+
<div class="element-info">
|
|
94
|
+
<span class="element-name">${e.type.toUpperCase()}</span>
|
|
95
|
+
<span class="element-sub">${String(e.content).substring(0,20)}</span>
|
|
96
|
+
</div>
|
|
97
|
+
`,t.onclick=()=>this.selectElement(e.id),this.elementsContainer.appendChild(t)})}selectElement(e){this.selectedElementId=e,this.renderElementsList(),this.renderPropertyPanel(),this.updateEditorOverlay(),e&&this.container.offsetWidth<=768&&this.rightSidebar.classList.add("show")}renderPropertyPanel(){const e=this.container.querySelector("#toggle-right");if(!this.selectedElementId){this.propertyPanel.style.display="none",e&&(e.style.display="none");return}this.container.offsetWidth<=768?e&&(e.style.display="flex"):e&&(e.style.display="none");const t=this.currentLayout.elements.find(s=>s.id===this.selectedElementId);if(!t)return;this.propertyPanel.style.display="block",this.propContent.innerHTML=`
|
|
98
|
+
<div class="form-group">
|
|
99
|
+
${t.type==="qr"?`
|
|
100
|
+
<label>Field Separator</label>
|
|
101
|
+
<input type="text" id="prop-qr-separator" placeholder="e.g. | or -" value="${t.qrSeparator||""}">
|
|
102
|
+
`:""}
|
|
103
|
+
<label>Content</label>
|
|
104
|
+
<textarea data-prop="content-val" rows="2">${t.content}</textarea>
|
|
105
|
+
<div class="field-buttons" data-el="field-suggestions"></div>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="form-row">
|
|
108
|
+
<div class="form-group" style="flex:1;"><label>X (pos)</label><input type="number" step="0.01" data-prop="x" value="${t.x.toFixed(2)}"></div>
|
|
109
|
+
<div class="form-group" style="flex:1;"><label>Y (pos)</label><input type="number" step="0.01" data-prop="y" value="${t.y.toFixed(2)}"></div>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="form-row">
|
|
112
|
+
<div class="form-group" style="flex:1;"><label>Width</label><input type="number" step="0.01" data-prop="w" value="${t.w.toFixed(2)}"></div>
|
|
113
|
+
<div class="form-group" style="flex:1;"><label>Height</label><input type="number" step="0.01" data-prop="h" value="${t.h.toFixed(2)}"></div>
|
|
114
|
+
</div>
|
|
115
|
+
${t.type==="text"?`
|
|
116
|
+
<div style="height: 1px; background: var(--border-color); margin: 16px 0;"></div>
|
|
117
|
+
<div class="form-row">
|
|
118
|
+
<div class="form-group" style="flex:1;">
|
|
119
|
+
<label>Font Size</label>
|
|
120
|
+
<input type="number" data-prop="fontSize" value="${t.style?.fontSize||12}">
|
|
121
|
+
</div>
|
|
122
|
+
<div class="form-group" style="flex:1;">
|
|
123
|
+
<label>Font Weight</label>
|
|
124
|
+
<select data-prop="fontWeight">
|
|
125
|
+
<option value="normal" ${t.style?.fontWeight==="normal"?"selected":""}>Normal</option>
|
|
126
|
+
<option value="bold" ${t.style?.fontWeight==="bold"?"selected":""}>Bold</option>
|
|
127
|
+
</select>
|
|
128
|
+
</div>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="form-group">
|
|
131
|
+
<label>Horizontal Align</label>
|
|
132
|
+
<div class="toggle-group" style="width: 100%;">
|
|
133
|
+
<button class="toggle-btn prop-align-h ${t.style?.textAlign==="left"?"active":""}" data-val="left" style="flex:1;">Left</button>
|
|
134
|
+
<button class="toggle-btn prop-align-h ${t.style?.textAlign==="center"?"active":""}" data-val="center" style="flex:1;">Center</button>
|
|
135
|
+
<button class="toggle-btn prop-align-h ${t.style?.textAlign==="right"?"active":""}" data-val="right" style="flex:1;">Right</button>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
138
|
+
<div class="form-group">
|
|
139
|
+
<label>Vertical Align</label>
|
|
140
|
+
<div class="toggle-group" style="width: 100%;">
|
|
141
|
+
<button class="toggle-btn prop-align-v ${t.style?.verticalAlign==="top"?"active":""}" data-val="top" style="flex:1;">Top</button>
|
|
142
|
+
<button class="toggle-btn prop-align-v ${t.style?.verticalAlign==="middle"?"active":""}" data-val="middle" style="flex:1;">Middle</button>
|
|
143
|
+
<button class="toggle-btn prop-align-v ${t.style?.verticalAlign==="bottom"?"active":""}" data-val="bottom" style="flex:1;">Bottom</button>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
`:""}
|
|
147
|
+
`;const i=this.propContent.querySelector('[data-el="field-suggestions"]'),n=this.currentLayout.targetEntity?this.entitySchemas[this.currentLayout.targetEntity]:null;n&&i&&n.fields.forEach(s=>{const a=document.createElement("div");a.className="field-pill",a.innerText=`+ ${s.label}`,a.onclick=()=>{t.content+=`{{${s.name}}}`,this.renderPropertyPanel(),this.updatePreview()},i.appendChild(a)});const r=this.propContent.querySelector("#prop-qr-separator");r&&r.addEventListener("input",s=>{t.qrSeparator=s.target.value,this.updatePreview()});const l=(s,a,d=!1,h)=>{const u=this.propContent.querySelector(`[data-prop="${s}"]`);u&&u.addEventListener("input",g=>{const p=g.target.value,v=d?parseFloat(p)||0:p;h?(t.style||(t.style={}),t.style[h]=v):t[a]=v,this.updatePreview()})};l("content-val","content"),l("x","x",!0),l("y","y",!0),l("w","w",!0),l("h","h",!0),l("fontSize","style",!0,"fontSize"),l("fontWeight","style",!1,"fontWeight"),this.propContent.querySelectorAll(".prop-align-h").forEach(s=>{s.addEventListener("click",()=>{t.style||(t.style={}),t.style.textAlign=s.dataset.val,this.renderPropertyPanel(),this.updatePreview()})}),this.propContent.querySelectorAll(".prop-align-v").forEach(s=>{s.addEventListener("click",()=>{t.style||(t.style={}),t.style.verticalAlign=s.dataset.val,this.renderPropertyPanel(),this.updatePreview()})})}updateEditorOverlay(){!this.editorOverlay||!this.canvas||(this.editorOverlay.style.width=this.canvas.style.width,this.editorOverlay.style.height=this.canvas.style.height,this.editorOverlay.innerHTML="",this.currentLayout.elements.forEach(e=>{const t=document.createElement("div");if(t.className=`editor-item ${this.selectedElementId===e.id?"selected":""}`,t.style.left=`${e.x*this.pxPerUnit}px`,t.style.top=`${e.y*this.pxPerUnit}px`,t.style.width=`${e.w*this.pxPerUnit}px`,t.style.height=`${e.h*this.pxPerUnit}px`,this.selectedElementId===e.id){const i=document.createElement("div");i.className="resize-handle",i.onmousedown=n=>{n.preventDefault(),n.stopPropagation(),this.startElementResize(n,e)},t.appendChild(i)}t.onmousedown=i=>{i.preventDefault(),this.selectElement(e.id),this.startElementDrag(i,e)},this.editorOverlay.appendChild(t)}))}startElementResize(e,t){const i=e.clientX,n=e.clientY,r=t.w,l=t.h,s=d=>{t.w=Math.max(1,r+(d.clientX-i)/this.pxPerUnit),t.h=Math.max(1,l+(d.clientY-n)/this.pxPerUnit),this.updatePreview(),this.renderPropertyPanel()},a=()=>{window.removeEventListener("mousemove",s),window.removeEventListener("mouseup",a)};window.addEventListener("mousemove",s),window.addEventListener("mouseup",a)}startElementDrag(e,t){const i=e.clientX,n=e.clientY,r=t.x,l=t.y,s=d=>{t.x=r+(d.clientX-i)/this.pxPerUnit,t.y=l+(d.clientY-n)/this.pxPerUnit,this.updatePreview(),this.renderPropertyPanel()},a=()=>{window.removeEventListener("mousemove",s),window.removeEventListener("mouseup",a)};window.addEventListener("mousemove",s),window.addEventListener("mouseup",a)}destroy(){this.container.innerHTML="",this.container.classList.remove("qrlayout-designer")}}Object.defineProperty(o,"StickerPrinter",{enumerable:!0,get:()=>c.StickerPrinter}),o.QRLayoutDesigner=y,Object.defineProperty(o,Symbol.toStringTag,{value:"Module"})}));
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "qrlayout-ui",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Visual designer and UI components for QR layout generation",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"qr",
|
|
7
|
+
"qrcode",
|
|
8
|
+
"qr-code",
|
|
9
|
+
"qr-layout",
|
|
10
|
+
"qr-layout-designer",
|
|
11
|
+
"layout",
|
|
12
|
+
"qr-sticker",
|
|
13
|
+
"qr-label",
|
|
14
|
+
"generator",
|
|
15
|
+
"visual-editor",
|
|
16
|
+
"drag-drop",
|
|
17
|
+
"ui-components"
|
|
18
|
+
],
|
|
19
|
+
"author": "Shashidhar Naik <shashidharnaik8@gmail.com>",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/shashi089/qr-code-layout-generate-tool.git",
|
|
24
|
+
"directory": "packages/ui"
|
|
25
|
+
},
|
|
26
|
+
"private": false,
|
|
27
|
+
"type": "module",
|
|
28
|
+
"files": [
|
|
29
|
+
"dist"
|
|
30
|
+
],
|
|
31
|
+
"main": "./dist/qrlayout-ui.umd.js",
|
|
32
|
+
"module": "./dist/qrlayout-ui.js",
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"exports": {
|
|
35
|
+
".": {
|
|
36
|
+
"types": "./dist/index.d.ts",
|
|
37
|
+
"import": "./dist/qrlayout-ui.js",
|
|
38
|
+
"require": "./dist/qrlayout-ui.umd.js"
|
|
39
|
+
},
|
|
40
|
+
"./style.css": "./dist/qrlayout-ui.css"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"dev": "vite",
|
|
44
|
+
"build": "vite build",
|
|
45
|
+
"preview": "vite preview"
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"qrlayout-core": "^1.0.7"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"typescript": "^5.3.3",
|
|
52
|
+
"vite": "^7.2.7",
|
|
53
|
+
"vite-plugin-dts": "^4.5.4"
|
|
54
|
+
}
|
|
55
|
+
}
|