@ashraf_mizo/htmlcanvas 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/bin/cli.js ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+
3
+ // CLI entry point for npx @ashraf_mizo/htmlcanvas
4
+ // Starts the editor server and opens the browser
5
+
6
+ import { fileURLToPath } from 'url';
7
+ import path from 'path';
8
+ import { fork } from 'child_process';
9
+
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+ const serverPath = path.join(__dirname, '..', 'server.js');
13
+
14
+ console.log('Starting HTMLCanvas editor...');
15
+
16
+ const child = fork(serverPath, [], {
17
+ cwd: path.join(__dirname, '..'),
18
+ stdio: 'inherit'
19
+ });
20
+
21
+ child.on('error', (err) => {
22
+ console.error('Failed to start:', err.message);
23
+ process.exit(1);
24
+ });
25
+
26
+ // Forward termination signals
27
+ process.on('SIGINT', () => child.kill('SIGINT'));
28
+ process.on('SIGTERM', () => child.kill('SIGTERM'));
@@ -0,0 +1,211 @@
1
+ // editor/alignment.js — Alignment & distribution tools
2
+ //
3
+ // Exports:
4
+ // alignElements(elements, hcIds, direction, iframeDoc)
5
+ // distributeElements(elements, hcIds, axis, iframeDoc)
6
+ //
7
+ // Works with both single elements (align to parent) and multi-selection.
8
+ // All changes go through the Command Pattern for undo/redo.
9
+
10
+ import { push } from './history.js';
11
+ import { recordChange } from './serializer.js';
12
+
13
+ // ── Helpers ─────────────────────────────────────────────────────────────────
14
+
15
+ function getRect(el) {
16
+ return el.getBoundingClientRect();
17
+ }
18
+
19
+ function getParentRect(el) {
20
+ const parent = el.parentElement;
21
+ return parent ? parent.getBoundingClientRect() : { left: 0, top: 0, width: 1000, height: 1000, right: 1000, bottom: 1000 };
22
+ }
23
+
24
+ /**
25
+ * Snapshot inline positioning styles for undo.
26
+ */
27
+ function snapPos(el) {
28
+ return {
29
+ left: el.style.left || '',
30
+ top: el.style.top || '',
31
+ transform: el.style.transform || '',
32
+ };
33
+ }
34
+
35
+ function restorePos(el, snap) {
36
+ if (snap.left) el.style.left = snap.left; else el.style.removeProperty('left');
37
+ if (snap.top) el.style.top = snap.top; else el.style.removeProperty('top');
38
+ if (snap.transform) el.style.transform = snap.transform; else el.style.removeProperty('transform');
39
+ }
40
+
41
+ /**
42
+ * Parse translate(Xpx, Ypx) from inline transform.
43
+ */
44
+ function parseTranslate(el) {
45
+ const t = el.style.transform || '';
46
+ const m = t.match(/translate\(\s*(-?[\d.]+)px\s*,\s*(-?[\d.]+)px\s*\)/);
47
+ return m ? { x: parseFloat(m[1]), y: parseFloat(m[2]) } : { x: 0, y: 0 };
48
+ }
49
+
50
+ /**
51
+ * Sets only the translate portion of transform, preserving rotate/scale.
52
+ */
53
+ function setTranslateOnly(el, tx, ty) {
54
+ let t = el.style.transform || '';
55
+ t = t.replace(/translate\([^)]*\)/g, '').trim();
56
+ const prefix = (tx !== 0 || ty !== 0) ? `translate(${tx}px, ${ty}px)` : '';
57
+ el.style.transform = [prefix, t].filter(Boolean).join(' ') || '';
58
+ if (!el.style.transform) el.style.removeProperty('transform');
59
+ }
60
+
61
+ /**
62
+ * Move element by delta pixels (adjusts translate).
63
+ */
64
+ function moveBy(el, dx, dy) {
65
+ const cur = parseTranslate(el);
66
+ setTranslateOnly(el, cur.x + dx, cur.y + dy);
67
+ }
68
+
69
+ // ── Alignment ────────────────────────────────────────────────────────────────
70
+
71
+ /**
72
+ * Aligns elements to a direction. If multiple elements, aligns to group bounds.
73
+ * If single element, aligns to parent bounds.
74
+ *
75
+ * @param {Element[]} elements
76
+ * @param {string[]} hcIds
77
+ * @param {'left'|'center'|'right'|'top'|'middle'|'bottom'} direction
78
+ */
79
+ export function alignElements(elements, hcIds, direction) {
80
+ if (!elements || elements.length === 0) return;
81
+
82
+ const befores = elements.map(el => snapPos(el));
83
+ const rects = elements.map(el => getRect(el));
84
+
85
+ let ref;
86
+ if (elements.length === 1) {
87
+ // Align to parent
88
+ ref = getParentRect(elements[0]);
89
+ } else {
90
+ // Align to group bounding box
91
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
92
+ for (const r of rects) {
93
+ minX = Math.min(minX, r.left);
94
+ minY = Math.min(minY, r.top);
95
+ maxX = Math.max(maxX, r.right);
96
+ maxY = Math.max(maxY, r.bottom);
97
+ }
98
+ ref = { left: minX, top: minY, right: maxX, bottom: maxY, width: maxX - minX, height: maxY - minY };
99
+ }
100
+
101
+ for (let i = 0; i < elements.length; i++) {
102
+ const el = elements[i];
103
+ const r = rects[i];
104
+ let dx = 0, dy = 0;
105
+
106
+ switch (direction) {
107
+ case 'left': dx = ref.left - r.left; break;
108
+ case 'center': dx = (ref.left + ref.width / 2) - (r.left + r.width / 2); break;
109
+ case 'right': dx = ref.right - r.right; break;
110
+ case 'top': dy = ref.top - r.top; break;
111
+ case 'middle': dy = (ref.top + ref.height / 2) - (r.top + r.height / 2); break;
112
+ case 'bottom': dy = ref.bottom - r.bottom; break;
113
+ }
114
+
115
+ if (dx !== 0 || dy !== 0) moveBy(el, dx, dy);
116
+ }
117
+
118
+ const afters = elements.map(el => snapPos(el));
119
+
120
+ const cmd = {
121
+ description: `Align ${direction}`,
122
+ execute() {
123
+ for (let i = 0; i < elements.length; i++) {
124
+ restorePos(elements[i], afters[i]);
125
+ recordChange(hcIds[i], elements[i].outerHTML);
126
+ }
127
+ },
128
+ undo() {
129
+ for (let i = 0; i < elements.length; i++) {
130
+ restorePos(elements[i], befores[i]);
131
+ recordChange(hcIds[i], elements[i].outerHTML);
132
+ }
133
+ }
134
+ };
135
+
136
+ // Already applied, so push without re-executing
137
+ // push() calls execute() which re-applies (harmless no-op since styles are already set)
138
+ push(cmd);
139
+ }
140
+
141
+ // ── Distribution ────────────────────────────────────────────────────────────
142
+
143
+ /**
144
+ * Distributes elements evenly along an axis.
145
+ * Requires 3+ elements for meaningful distribution.
146
+ *
147
+ * @param {Element[]} elements
148
+ * @param {string[]} hcIds
149
+ * @param {'horizontal'|'vertical'} axis
150
+ */
151
+ export function distributeElements(elements, hcIds, axis) {
152
+ if (!elements || elements.length < 3) return;
153
+
154
+ const befores = elements.map(el => snapPos(el));
155
+ const rects = elements.map(el => getRect(el));
156
+
157
+ // Sort by position along axis
158
+ const indexed = rects.map((r, i) => ({ r, i }));
159
+ if (axis === 'horizontal') {
160
+ indexed.sort((a, b) => a.r.left - b.r.left);
161
+ } else {
162
+ indexed.sort((a, b) => a.r.top - b.r.top);
163
+ }
164
+
165
+ const first = indexed[0].r;
166
+ const last = indexed[indexed.length - 1].r;
167
+
168
+ if (axis === 'horizontal') {
169
+ const totalWidth = indexed.reduce((s, { r }) => s + r.width, 0);
170
+ const totalSpace = (last.right - first.left) - totalWidth;
171
+ const gap = totalSpace / (elements.length - 1);
172
+
173
+ let cursor = first.left;
174
+ for (const { r, i } of indexed) {
175
+ const dx = cursor - r.left;
176
+ if (dx !== 0) moveBy(elements[i], dx, 0);
177
+ cursor += r.width + gap;
178
+ }
179
+ } else {
180
+ const totalHeight = indexed.reduce((s, { r }) => s + r.height, 0);
181
+ const totalSpace = (last.bottom - first.top) - totalHeight;
182
+ const gap = totalSpace / (elements.length - 1);
183
+
184
+ let cursor = first.top;
185
+ for (const { r, i } of indexed) {
186
+ const dy = cursor - r.top;
187
+ if (dy !== 0) moveBy(elements[i], 0, dy);
188
+ cursor += r.height + gap;
189
+ }
190
+ }
191
+
192
+ const afters = elements.map(el => snapPos(el));
193
+
194
+ const cmd = {
195
+ description: `Distribute ${axis}`,
196
+ execute() {
197
+ for (let i = 0; i < elements.length; i++) {
198
+ restorePos(elements[i], afters[i]);
199
+ recordChange(hcIds[i], elements[i].outerHTML);
200
+ }
201
+ },
202
+ undo() {
203
+ for (let i = 0; i < elements.length; i++) {
204
+ restorePos(elements[i], befores[i]);
205
+ recordChange(hcIds[i], elements[i].outerHTML);
206
+ }
207
+ }
208
+ };
209
+
210
+ push(cmd);
211
+ }