@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 +28 -0
- package/editor/alignment.js +211 -0
- package/editor/assets.js +724 -0
- package/editor/clipboard.js +177 -0
- package/editor/coords.js +121 -0
- package/editor/crop.js +325 -0
- package/editor/cssVars.js +134 -0
- package/editor/domModel.js +161 -0
- package/editor/editor.css +1996 -0
- package/editor/editor.js +833 -0
- package/editor/guides.js +513 -0
- package/editor/history.js +135 -0
- package/editor/index.html +540 -0
- package/editor/layers.js +389 -0
- package/editor/logo-final.svg +21 -0
- package/editor/logo-toolbar.svg +21 -0
- package/editor/manipulation.js +864 -0
- package/editor/multiSelect.js +436 -0
- package/editor/properties.js +1583 -0
- package/editor/selection.js +432 -0
- package/editor/serializer.js +160 -0
- package/editor/shortcuts.js +143 -0
- package/editor/slidePanel.js +361 -0
- package/editor/slides.js +101 -0
- package/editor/snap.js +98 -0
- package/editor/textEdit.js +538 -0
- package/editor/zoom.js +96 -0
- package/package.json +28 -0
- package/server.js +588 -0
package/editor/layers.js
ADDED
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
// editor/layers.js — Layer manager panel (right sidebar)
|
|
2
|
+
//
|
|
3
|
+
// Exports:
|
|
4
|
+
// initLayers(iframe, iframeDoc, canvasArea) — set up layer panel after iframe-ready
|
|
5
|
+
// buildLayerTree(iframeDoc, panelBody) — populate layer panel from DOM
|
|
6
|
+
// makeZIndexCommand(element, hcId, oldZ, newZ) — Command factory for z-index reorder
|
|
7
|
+
// getTopZIndex(parent) — returns highest z-index + 1 among siblings
|
|
8
|
+
// notifyLayersChanged() — dispatches event to rebuild layer tree
|
|
9
|
+
//
|
|
10
|
+
// SortableJS is expected as window.Sortable (loaded via <script> tag before editor.js).
|
|
11
|
+
|
|
12
|
+
import { push } from './history.js';
|
|
13
|
+
import { recordChange } from './serializer.js';
|
|
14
|
+
|
|
15
|
+
// ── Module state ──────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
let _iframeDoc = null;
|
|
18
|
+
let _rightContainer = null;
|
|
19
|
+
|
|
20
|
+
// Lock/hide state maps (keyed by hcId)
|
|
21
|
+
const _lockedElements = new Set();
|
|
22
|
+
const _hiddenElements = new Set();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns true if an element is locked (non-selectable, non-draggable).
|
|
26
|
+
*/
|
|
27
|
+
export function isLocked(hcId) {
|
|
28
|
+
return _lockedElements.has(hcId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Returns true if an element is hidden.
|
|
33
|
+
*/
|
|
34
|
+
export function isHidden(hcId) {
|
|
35
|
+
return _hiddenElements.has(hcId);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Toggles lock state on an element.
|
|
40
|
+
*/
|
|
41
|
+
export function toggleLock(hcId) {
|
|
42
|
+
if (_lockedElements.has(hcId)) _lockedElements.delete(hcId);
|
|
43
|
+
else _lockedElements.add(hcId);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Toggles visibility on an element.
|
|
48
|
+
*/
|
|
49
|
+
export function toggleHide(hcId, iframeDoc) {
|
|
50
|
+
if (_hiddenElements.has(hcId)) {
|
|
51
|
+
_hiddenElements.delete(hcId);
|
|
52
|
+
const el = iframeDoc?.querySelector(`[data-hc-id="${hcId}"]`);
|
|
53
|
+
if (el) el.style.visibility = '';
|
|
54
|
+
} else {
|
|
55
|
+
_hiddenElements.add(hcId);
|
|
56
|
+
const el = iframeDoc?.querySelector(`[data-hc-id="${hcId}"]`);
|
|
57
|
+
if (el) el.style.visibility = 'hidden';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Layer ordering helpers ────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Scans all direct children of a parent element and returns the highest
|
|
65
|
+
* z-index + 1. Used by insert/paste commands to place new elements on top.
|
|
66
|
+
*/
|
|
67
|
+
export function getTopZIndex(parent) {
|
|
68
|
+
if (!parent) return 1;
|
|
69
|
+
let max = 0;
|
|
70
|
+
for (const child of parent.children) {
|
|
71
|
+
const z = parseInt(child.style.zIndex, 10);
|
|
72
|
+
if (!isNaN(z) && z > max) max = z;
|
|
73
|
+
}
|
|
74
|
+
return max + 1;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Dispatches an event that triggers a layer tree rebuild.
|
|
79
|
+
* Call after any DOM insertion, deletion, or reorder.
|
|
80
|
+
*/
|
|
81
|
+
export function notifyLayersChanged() {
|
|
82
|
+
window.dispatchEvent(new CustomEvent('hc:layers-changed'));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Label builder ─────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Returns a friendly display label for a DOM element.
|
|
89
|
+
* Priority: tag-specific labels > class name > tag name
|
|
90
|
+
*/
|
|
91
|
+
function buildLabel(element) {
|
|
92
|
+
const tag = element.tagName.toLowerCase();
|
|
93
|
+
|
|
94
|
+
// Tag-specific friendly names
|
|
95
|
+
if (tag === 'img') {
|
|
96
|
+
const alt = element.getAttribute('alt');
|
|
97
|
+
if (alt) return `Image: ${alt.slice(0, 20)}`;
|
|
98
|
+
const src = element.getAttribute('src') || '';
|
|
99
|
+
const filename = src.split('/').pop().split('?')[0];
|
|
100
|
+
return filename ? `Image: ${filename.slice(0, 20)}` : 'Image';
|
|
101
|
+
}
|
|
102
|
+
if (tag === 'svg') return 'Shape';
|
|
103
|
+
if (tag === 'video') return 'Video';
|
|
104
|
+
if (tag === 'iframe') return 'Embed';
|
|
105
|
+
if (tag === 'a') return 'Link';
|
|
106
|
+
if (tag === 'ul' || tag === 'ol') return 'List';
|
|
107
|
+
if (tag === 'table') return 'Table';
|
|
108
|
+
if (tag === 'hr') return 'Divider';
|
|
109
|
+
|
|
110
|
+
// Text elements: show truncated text content
|
|
111
|
+
if (tag === 'h1' || tag === 'h2' || tag === 'h3' || tag === 'h4' || tag === 'h5' || tag === 'h6') {
|
|
112
|
+
const text = element.textContent.trim().slice(0, 24);
|
|
113
|
+
return text ? `${tag.toUpperCase()}: ${text}` : tag.toUpperCase();
|
|
114
|
+
}
|
|
115
|
+
if (tag === 'p' || tag === 'span' || tag === 'em' || tag === 'strong') {
|
|
116
|
+
const text = element.textContent.trim().slice(0, 24);
|
|
117
|
+
if (text) return text;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Class-based labels (first meaningful class)
|
|
121
|
+
if (element.classList && element.classList.length > 0) {
|
|
122
|
+
const cls = element.classList[0];
|
|
123
|
+
// Convert class to readable name: "heading-xl" -> "Heading Xl"
|
|
124
|
+
const readable = cls.replace(/[-_]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
125
|
+
return readable;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return `<${tag}>`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Recursive tree builder ────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Recursively builds layer items for an element's children.
|
|
135
|
+
* Only includes elements with a data-hc-id attribute.
|
|
136
|
+
*/
|
|
137
|
+
function buildChildItems(parentElement, listEl, depth) {
|
|
138
|
+
const allChildren = Array.from(parentElement.children)
|
|
139
|
+
.filter(c => c.getAttribute('data-hc-id'));
|
|
140
|
+
|
|
141
|
+
// Sort by z-index descending (highest = front = top of list).
|
|
142
|
+
// When z-index is equal or unset, later DOM order = on top (CSS paint order).
|
|
143
|
+
const children = allChildren.sort((a, b) => {
|
|
144
|
+
const zA = parseInt(a.style.zIndex, 10);
|
|
145
|
+
const zB = parseInt(b.style.zIndex, 10);
|
|
146
|
+
const hasA = !isNaN(zA);
|
|
147
|
+
const hasB = !isNaN(zB);
|
|
148
|
+
|
|
149
|
+
// Both have z-index: sort descending
|
|
150
|
+
if (hasA && hasB) {
|
|
151
|
+
if (zB !== zA) return zB - zA;
|
|
152
|
+
// Same z-index: later DOM order = higher
|
|
153
|
+
return allChildren.indexOf(b) - allChildren.indexOf(a);
|
|
154
|
+
}
|
|
155
|
+
// Only one has z-index: it wins
|
|
156
|
+
if (hasA && !hasB) return -1;
|
|
157
|
+
if (!hasA && hasB) return 1;
|
|
158
|
+
// Neither has z-index: later DOM order = higher (on top)
|
|
159
|
+
return allChildren.indexOf(b) - allChildren.indexOf(a);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
for (const child of children) {
|
|
163
|
+
const hcId = child.getAttribute('data-hc-id');
|
|
164
|
+
const label = buildLabel(child);
|
|
165
|
+
const z = child.style.zIndex;
|
|
166
|
+
|
|
167
|
+
const li = document.createElement('li');
|
|
168
|
+
li.className = 'layer-item';
|
|
169
|
+
li.dataset.layerHcid = hcId;
|
|
170
|
+
li.style.paddingLeft = `${8 + depth * 12}px`;
|
|
171
|
+
li.title = label + (z ? ` (z: ${z})` : '');
|
|
172
|
+
|
|
173
|
+
// Label span
|
|
174
|
+
const labelSpan = document.createElement('span');
|
|
175
|
+
labelSpan.className = 'layer-item-label';
|
|
176
|
+
labelSpan.textContent = label;
|
|
177
|
+
li.appendChild(labelSpan);
|
|
178
|
+
|
|
179
|
+
// Hide toggle
|
|
180
|
+
const hideBtn = document.createElement('button');
|
|
181
|
+
hideBtn.className = 'layer-toggle' + (_hiddenElements.has(hcId) ? ' active' : '');
|
|
182
|
+
hideBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>';
|
|
183
|
+
hideBtn.title = 'Toggle visibility';
|
|
184
|
+
hideBtn.addEventListener('click', (e) => {
|
|
185
|
+
e.stopPropagation();
|
|
186
|
+
toggleHide(hcId, _iframeDoc);
|
|
187
|
+
hideBtn.classList.toggle('active');
|
|
188
|
+
});
|
|
189
|
+
li.appendChild(hideBtn);
|
|
190
|
+
|
|
191
|
+
// Lock toggle
|
|
192
|
+
const lockBtn = document.createElement('button');
|
|
193
|
+
lockBtn.className = 'layer-toggle' + (_lockedElements.has(hcId) ? ' active' : '');
|
|
194
|
+
lockBtn.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>';
|
|
195
|
+
lockBtn.title = 'Toggle lock';
|
|
196
|
+
lockBtn.addEventListener('click', (e) => {
|
|
197
|
+
e.stopPropagation();
|
|
198
|
+
toggleLock(hcId);
|
|
199
|
+
lockBtn.classList.toggle('active');
|
|
200
|
+
});
|
|
201
|
+
li.appendChild(lockBtn);
|
|
202
|
+
|
|
203
|
+
listEl.appendChild(li);
|
|
204
|
+
|
|
205
|
+
if (child.children.length > 0) {
|
|
206
|
+
buildChildItems(child, listEl, depth + 1);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ── Public: buildLayerTree ────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Clears the container and populates it with the layer tree from iframeDoc.
|
|
215
|
+
* For slide decks: grouped by slide with collapsible <details>.
|
|
216
|
+
* For regular HTML: flat list from body children.
|
|
217
|
+
*/
|
|
218
|
+
export function buildLayerTree(iframeDoc, container) {
|
|
219
|
+
_iframeDoc = iframeDoc;
|
|
220
|
+
container.innerHTML = '';
|
|
221
|
+
|
|
222
|
+
const pages = iframeDoc.querySelectorAll('.page');
|
|
223
|
+
|
|
224
|
+
if (pages.length > 0) {
|
|
225
|
+
pages.forEach((page, idx) => {
|
|
226
|
+
const details = document.createElement('details');
|
|
227
|
+
details.open = true;
|
|
228
|
+
details.className = 'layer-group';
|
|
229
|
+
|
|
230
|
+
const summary = document.createElement('summary');
|
|
231
|
+
summary.className = 'layer-group-label';
|
|
232
|
+
summary.textContent = `Slide ${idx + 1}`;
|
|
233
|
+
details.appendChild(summary);
|
|
234
|
+
|
|
235
|
+
const ul = document.createElement('ul');
|
|
236
|
+
ul.className = 'layer-list';
|
|
237
|
+
buildChildItems(page, ul, 0);
|
|
238
|
+
details.appendChild(ul);
|
|
239
|
+
|
|
240
|
+
container.appendChild(details);
|
|
241
|
+
});
|
|
242
|
+
} else {
|
|
243
|
+
const ul = document.createElement('ul');
|
|
244
|
+
ul.className = 'layer-list';
|
|
245
|
+
if (iframeDoc.body) {
|
|
246
|
+
buildChildItems(iframeDoc.body, ul, 0);
|
|
247
|
+
}
|
|
248
|
+
container.appendChild(ul);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ── Public: makeZIndexCommand ─────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
export function makeZIndexCommand(element, hcId, oldZ, newZ) {
|
|
255
|
+
return {
|
|
256
|
+
description: 'Reorder layer',
|
|
257
|
+
execute() {
|
|
258
|
+
element.style.zIndex = String(newZ);
|
|
259
|
+
recordChange(hcId, element.outerHTML);
|
|
260
|
+
},
|
|
261
|
+
undo() {
|
|
262
|
+
if (!oldZ || oldZ === '') {
|
|
263
|
+
element.style.removeProperty('z-index');
|
|
264
|
+
} else {
|
|
265
|
+
element.style.zIndex = oldZ;
|
|
266
|
+
}
|
|
267
|
+
recordChange(hcId, element.outerHTML);
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── Public: initLayers ────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
export function initLayers(iframe, iframeDoc, canvasArea) {
|
|
275
|
+
_rightContainer = document.querySelector('.panel-section-right-layers');
|
|
276
|
+
if (!_rightContainer) return;
|
|
277
|
+
|
|
278
|
+
// Build the initial tree
|
|
279
|
+
buildLayerTree(iframeDoc, _rightContainer);
|
|
280
|
+
|
|
281
|
+
// ── Click layer item to select element ────────────────────────────────────
|
|
282
|
+
_rightContainer.addEventListener('click', (e) => {
|
|
283
|
+
// Don't handle clicks on toggle buttons
|
|
284
|
+
if (e.target.closest('.layer-toggle')) return;
|
|
285
|
+
|
|
286
|
+
const item = e.target.closest('.layer-item');
|
|
287
|
+
if (!item) return;
|
|
288
|
+
|
|
289
|
+
const hcId = item.dataset.layerHcid;
|
|
290
|
+
if (!hcId || !_iframeDoc) return;
|
|
291
|
+
|
|
292
|
+
// Locked elements can still be selected from layers panel (for inspection)
|
|
293
|
+
const el = _iframeDoc.querySelector(`[data-hc-id="${hcId}"]`);
|
|
294
|
+
if (!el) return;
|
|
295
|
+
|
|
296
|
+
window.dispatchEvent(new CustomEvent('hc:select-request', {
|
|
297
|
+
detail: { hcId, element: el }
|
|
298
|
+
}));
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// ── Selection sync: highlight active layer ────────────────────────────────
|
|
302
|
+
window.addEventListener('hc:selection-changed', (e) => {
|
|
303
|
+
const { hcId } = e.detail || {};
|
|
304
|
+
|
|
305
|
+
_rightContainer.querySelectorAll('.layer-item.active').forEach(el => {
|
|
306
|
+
el.classList.remove('active');
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
if (!hcId) return;
|
|
310
|
+
|
|
311
|
+
const item = _rightContainer.querySelector(`[data-layer-hcid="${hcId}"]`);
|
|
312
|
+
if (item) {
|
|
313
|
+
item.classList.add('active');
|
|
314
|
+
// Scroll only within the right panel — do NOT use scrollIntoView() as it
|
|
315
|
+
// can scroll the canvas area when the panel has no independent scroll context.
|
|
316
|
+
const itemTop = item.offsetTop;
|
|
317
|
+
const itemBottom = itemTop + item.offsetHeight;
|
|
318
|
+
const panelTop = _rightContainer.scrollTop;
|
|
319
|
+
const panelBottom = panelTop + _rightContainer.clientHeight;
|
|
320
|
+
if (itemTop < panelTop) {
|
|
321
|
+
_rightContainer.scrollTop = itemTop - 4;
|
|
322
|
+
} else if (itemBottom > panelBottom) {
|
|
323
|
+
_rightContainer.scrollTop = itemBottom - _rightContainer.clientHeight + 4;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
window.addEventListener('hc:selection-cleared', () => {
|
|
329
|
+
_rightContainer.querySelectorAll('.layer-item.active').forEach(el => {
|
|
330
|
+
el.classList.remove('active');
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// ── SortableJS on each .layer-list ────────────────────────────────────────
|
|
335
|
+
_initSortable(_rightContainer, iframeDoc);
|
|
336
|
+
|
|
337
|
+
// ── Rebuild on DOM changes ────────────────────────────────────────────────
|
|
338
|
+
window.addEventListener('hc:element-moved', () => {
|
|
339
|
+
buildLayerTree(iframeDoc, _rightContainer);
|
|
340
|
+
_initSortable(_rightContainer, iframeDoc);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
window.addEventListener('hc:layers-changed', () => {
|
|
344
|
+
buildLayerTree(iframeDoc, _rightContainer);
|
|
345
|
+
_initSortable(_rightContainer, iframeDoc);
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ── Internal: SortableJS init ─────────────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
function _initSortable(container, iframeDoc) {
|
|
352
|
+
if (typeof window.Sortable === 'undefined') {
|
|
353
|
+
console.warn('layers.js: SortableJS not loaded.');
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const lists = container.querySelectorAll('.layer-list');
|
|
358
|
+
lists.forEach((ul) => {
|
|
359
|
+
window.Sortable.create(ul, {
|
|
360
|
+
animation: 150,
|
|
361
|
+
ghostClass: 'sortable-ghost',
|
|
362
|
+
chosenClass: 'sortable-chosen',
|
|
363
|
+
|
|
364
|
+
onEnd(evt) {
|
|
365
|
+
const { oldIndex, newIndex } = evt;
|
|
366
|
+
if (oldIndex === newIndex) return;
|
|
367
|
+
|
|
368
|
+
const items = ul.querySelectorAll('.layer-item');
|
|
369
|
+
const total = items.length;
|
|
370
|
+
|
|
371
|
+
items.forEach((item, i) => {
|
|
372
|
+
const hcId = item.dataset.layerHcid;
|
|
373
|
+
if (!hcId || !_iframeDoc) return;
|
|
374
|
+
|
|
375
|
+
const el = _iframeDoc.querySelector(`[data-hc-id="${hcId}"]`);
|
|
376
|
+
if (!el) return;
|
|
377
|
+
|
|
378
|
+
const newZ = total - i;
|
|
379
|
+
const oldZ = el.style.zIndex || '';
|
|
380
|
+
|
|
381
|
+
if (i === newIndex) {
|
|
382
|
+
const cmd = makeZIndexCommand(el, hcId, oldZ, String(newZ));
|
|
383
|
+
push(cmd);
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 92">
|
|
2
|
+
<defs>
|
|
3
|
+
<style>
|
|
4
|
+
@import url('https://fonts.googleapis.com/css2?family=Podkova:wght@700&display=swap');
|
|
5
|
+
</style>
|
|
6
|
+
</defs>
|
|
7
|
+
|
|
8
|
+
<!-- Icon: 48x48, centred horizontally -->
|
|
9
|
+
<svg x="76" y="0" width="48" height="48" viewBox="0 0 80 78">
|
|
10
|
+
<g transform="translate(20, 8)">
|
|
11
|
+
<g transform="translate(8, 36) rotate(12)">
|
|
12
|
+
<rect x="-8" y="-36" width="16" height="72" rx="5" fill="#000"/>
|
|
13
|
+
</g>
|
|
14
|
+
<rect x="22" y="32" width="36" height="12" rx="4" fill="#000"/>
|
|
15
|
+
<circle cx="29.5" cy="55" r="7.5" fill="#000"/>
|
|
16
|
+
</g>
|
|
17
|
+
</svg>
|
|
18
|
+
|
|
19
|
+
<!-- Typography: 8px gap after icon -->
|
|
20
|
+
<text x="100" y="82" text-anchor="middle" font-family="'Podkova', serif" font-weight="700" font-size="32" letter-spacing="-0.5" fill="#000">Ashraf<tspan fill="#E87420">.</tspan>Builds</text>
|
|
21
|
+
</svg>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 92">
|
|
2
|
+
<defs>
|
|
3
|
+
<style>
|
|
4
|
+
@import url('https://fonts.googleapis.com/css2?family=Podkova:wght@700&display=swap');
|
|
5
|
+
</style>
|
|
6
|
+
</defs>
|
|
7
|
+
|
|
8
|
+
<!-- Icon: 48x48, centred horizontally -->
|
|
9
|
+
<svg x="76" y="0" width="48" height="48" viewBox="0 0 80 78">
|
|
10
|
+
<g transform="translate(20, 8)">
|
|
11
|
+
<g transform="translate(8, 36) rotate(12)">
|
|
12
|
+
<rect x="-8" y="-36" width="16" height="72" rx="5" fill="#E87420"/>
|
|
13
|
+
</g>
|
|
14
|
+
<rect x="22" y="32" width="36" height="12" rx="4" fill="#E87420"/>
|
|
15
|
+
<circle cx="29.5" cy="55" r="7.5" fill="#E87420"/>
|
|
16
|
+
</g>
|
|
17
|
+
</svg>
|
|
18
|
+
|
|
19
|
+
<!-- Typography -->
|
|
20
|
+
<text x="100" y="82" text-anchor="middle" font-family="'Podkova', serif" font-weight="700" font-size="32" letter-spacing="-0.5" fill="#fff">Ashraf<tspan fill="#E87420">.</tspan>Builds</text>
|
|
21
|
+
</svg>
|