@aznabee/freehand-ui 0.1.6 → 0.1.7

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 CHANGED
@@ -6,7 +6,7 @@ Wrap any existing web element with a beautiful, responsive, hand-drawn doodle la
6
6
 
7
7
  ## What it does
8
8
 
9
- `freehand-ui` adds a subtle SVG overlay on top of your UI - thin pen strokes, handwritten notes, arrows, and small decorations. It does **not** change your HTML layout or block clicks (`pointer-events: none`).
9
+ `freehand-ui` adds a subtle SVG overlay on top of your UI - thin pen strokes, handwritten notes, arrows, and small decorations. It does not change your element's own styling and never blocks clicks (`pointer-events: none`).
10
10
 
11
11
  **Good for:** call-to-action buttons, feature cards, onboarding hints, playful marketing UI.
12
12
 
@@ -19,6 +19,8 @@ Wrap any existing web element with a beautiful, responsive, hand-drawn doodle la
19
19
  | **Localised** | Notes in the visitor's own language, right-to-left included |
20
20
  | **Responsive** | Stays aligned on resize, scroll, and reflow |
21
21
 
22
+ **DOM note:** the element is wrapped in a plain positioning `<div>` so the overlay can be mounted beside it (last child, so it paints on top) - the wrapped element keeps its own display and, in a flex/grid parent, its own placement, but a selector depending on it being a *direct* child of its original parent (`.parent > .card`, `:nth-child`) will no longer match. The overlay is also clipped by an `overflow: hidden` ancestor, since it no longer escapes the page via `position: fixed`.
23
+
22
24
  **Live on [Aznabee.com](https://aznabee.com)** - see it in production on the real product.
23
25
 
24
26
  ## ![Basic hand-drawn border around a card](docs/images/basic.png)
@@ -255,7 +257,7 @@ A `dotted` arrow spends its dash pattern on the dots, and one pattern cannot bot
255
257
  Notes:
256
258
 
257
259
  - **`prefers-reduced-motion: reduce` turns both gestures off** and leaves the arrow fully drawn.
258
- - The overlay is rebuilt on every scroll and resize. Animations are placed by how long the overlay has been on the page rather than restarted, so scrolling never replays a draw or jolts a lean mid-cycle.
260
+ - The overlay moves with the element for free through ordinary layout - scrolling, a sibling loading in above it - and is only rebuilt when the element's own size actually changes. Animations are placed by how long the overlay has been on the page rather than restarted, so repositioning never replays a draw or jolts a lean mid-cycle.
259
261
 
260
262
  ### Decorations
261
263
 
@@ -454,7 +456,7 @@ Then open:
454
456
  - **Framework agnostic** - plain JavaScript at the core; React is an optional entry point
455
457
  - **SVG based** - hand-drawn paths, not CSS borders
456
458
  - **Non-invasive** - `pointer-events: none`, no layout changes
457
- - **Responsive** - `ResizeObserver`, scroll listeners, and `requestAnimationFrame`
459
+ - **Responsive** - absolutely positioned beside the element, so it tracks scroll and layout shifts for free; `ResizeObserver` covers the rest
458
460
  - **Lightweight** - zero runtime dependencies
459
461
 
460
462
  ---
@@ -399,20 +399,6 @@ function isValidPosition(position) {
399
399
  "center"
400
400
  ].includes(position);
401
401
  }
402
- function getScrollableAncestors(element) {
403
- const ancestors = [];
404
- let node = element.parentElement;
405
- while (node && node !== document.documentElement) {
406
- const style = getComputedStyle(node);
407
- const overflow = style.overflow + style.overflowY + style.overflowX;
408
- if (/(auto|scroll|overlay)/.test(overflow)) {
409
- ancestors.push(node);
410
- }
411
- node = node.parentElement;
412
- }
413
- ancestors.push(window);
414
- return ancestors;
415
- }
416
402
  function resolveOverlayZIndex(element, override) {
417
403
  if (override != null) return override;
418
404
  const { zIndex, position } = getComputedStyle(element);
@@ -431,13 +417,45 @@ function resolveOverlayZIndex(element, override) {
431
417
  }
432
418
  return null;
433
419
  }
420
+ var ITEM_PLACEMENT_PROPERTIES = [
421
+ "order",
422
+ "flexGrow",
423
+ "flexShrink",
424
+ "flexBasis",
425
+ "alignSelf",
426
+ "justifySelf",
427
+ "gridColumn",
428
+ "gridRow",
429
+ "gridArea"
430
+ ];
431
+ function isInlineDisplay(computed) {
432
+ return computed.display.startsWith("inline");
433
+ }
434
434
  function insertOverlaySvg(svg, element) {
435
435
  const parent = element.parentNode;
436
436
  if (!parent) {
437
437
  document.body.appendChild(svg);
438
- return;
438
+ return null;
439
439
  }
440
- parent.insertBefore(svg, element.nextSibling);
440
+ const wrapper = document.createElement("div");
441
+ wrapper.style.position = "relative";
442
+ const computed = getComputedStyle(element);
443
+ wrapper.style.display = isInlineDisplay(computed) ? "inline-block" : "block";
444
+ for (const property of ITEM_PLACEMENT_PROPERTIES) {
445
+ const value = computed[property];
446
+ if (value) wrapper.style[property] = value;
447
+ }
448
+ parent.replaceChild(wrapper, element);
449
+ wrapper.appendChild(element);
450
+ wrapper.appendChild(svg);
451
+ return wrapper;
452
+ }
453
+ function removeOverlaySvg(svg, wrapper, element) {
454
+ if (svg.parentNode) {
455
+ svg.parentNode.removeChild(svg);
456
+ }
457
+ if (!wrapper || !wrapper.parentNode) return;
458
+ wrapper.parentNode.replaceChild(element, wrapper);
441
459
  }
442
460
  function syncOverlayStacking(svg, element, zIndexOverride) {
443
461
  const zIndex = resolveOverlayZIndex(element, zIndexOverride);
@@ -1325,14 +1343,13 @@ function createOverlaySvg(left, top, width, height) {
1325
1343
  svg.setAttribute("width", String(width));
1326
1344
  svg.setAttribute("height", String(height));
1327
1345
  svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
1328
- svg.style.position = "fixed";
1346
+ svg.style.position = "absolute";
1329
1347
  svg.style.left = `${left}px`;
1330
1348
  svg.style.top = `${top}px`;
1331
1349
  svg.style.width = `${width}px`;
1332
1350
  svg.style.height = `${height}px`;
1333
1351
  svg.style.pointerEvents = "none";
1334
1352
  svg.style.overflow = "visible";
1335
- svg.style.willChange = "transform";
1336
1353
  return svg;
1337
1354
  }
1338
1355
 
@@ -1881,13 +1898,11 @@ var DoodleOverlay = class {
1881
1898
  this.seed = Math.floor(Math.random() * 1e9);
1882
1899
  this.startedAt = now();
1883
1900
  this.svg = null;
1901
+ this.wrapper = null;
1884
1902
  this.resizeObserver = null;
1885
1903
  this.mutationObserver = null;
1886
1904
  this.pendingFrame = null;
1887
- this.pendingRepositionFrame = null;
1888
- this.basePosition = { x: 0, y: 0 };
1889
1905
  this.listeners = [];
1890
- this.onScroll = this.scheduleReposition.bind(this);
1891
1906
  this.onResize = this.scheduleUpdate.bind(this);
1892
1907
  this.onLanguageChange = this.scheduleUpdate.bind(this);
1893
1908
  this.childElements = [];
@@ -1895,7 +1910,7 @@ var DoodleOverlay = class {
1895
1910
  }
1896
1911
  mount() {
1897
1912
  this.svg = createOverlaySvg(0, 0, 0, 0);
1898
- insertOverlaySvg(this.svg, this.element);
1913
+ this.wrapper = insertOverlaySvg(this.svg, this.element);
1899
1914
  syncOverlayStacking(this.svg, this.element, this.options.zIndex);
1900
1915
  this.resizeObserver = new ResizeObserver(this.onResize);
1901
1916
  this.resizeObserver.observe(this.element);
@@ -1908,10 +1923,6 @@ var DoodleOverlay = class {
1908
1923
  subtree: true,
1909
1924
  attributes: true
1910
1925
  });
1911
- for (const target of getScrollableAncestors(this.element)) {
1912
- target.addEventListener("scroll", this.onScroll, { passive: true });
1913
- this.listeners.push({ target, type: "scroll", handler: this.onScroll });
1914
- }
1915
1926
  window.addEventListener("resize", this.onResize, { passive: true });
1916
1927
  this.listeners.push({
1917
1928
  target: window,
@@ -1947,46 +1958,6 @@ var DoodleOverlay = class {
1947
1958
  this.update();
1948
1959
  });
1949
1960
  }
1950
- scheduleReposition() {
1951
- if (this.pendingFrame != null || this.pendingRepositionFrame != null) {
1952
- return;
1953
- }
1954
- this.pendingRepositionFrame = scheduleFrame(() => {
1955
- this.pendingRepositionFrame = null;
1956
- this.reposition();
1957
- });
1958
- }
1959
- /**
1960
- * Cheap scroll-driven path: slide the overlay to the element's new
1961
- * viewport position without touching its contents. Scrolling can't change
1962
- * an element's size, so the rects, viewBox and every hand-drawn stroke
1963
- * inside them are still valid - only the overlay's position needs to move.
1964
- *
1965
- * That move is a `transform`, not `left`/`top`. `left`/`top` sit in the
1966
- * same box-geometry pass as layout, so the browser re-checks layout on
1967
- * every write; `transform` is composited, so the scroll path never
1968
- * touches layout at all.
1969
- */
1970
- reposition() {
1971
- if (!this.svg || !this.element.isConnected) return;
1972
- const { padding, radius } = this.options;
1973
- const targets = this.getTargets();
1974
- const margin = this.overlayMargin();
1975
- const absoluteRects = targets.map(
1976
- (target) => getElementBounds(target, padding, radius)
1977
- );
1978
- const overlayRect = unionRects(absoluteRects, margin);
1979
- const currentWidth = parseFloat(this.svg.style.width) || 0;
1980
- const currentHeight = parseFloat(this.svg.style.height) || 0;
1981
- const resized = Math.abs(overlayRect.width - currentWidth) > 0.5 || Math.abs(overlayRect.height - currentHeight) > 0.5;
1982
- if (resized) {
1983
- this.update();
1984
- return;
1985
- }
1986
- const dx = overlayRect.x - this.basePosition.x;
1987
- const dy = overlayRect.y - this.basePosition.y;
1988
- this.svg.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
1989
- }
1990
1961
  getTargets() {
1991
1962
  if (this.options.children) {
1992
1963
  const nodes = this.element.querySelectorAll(this.options.children);
@@ -2006,15 +1977,14 @@ var DoodleOverlay = class {
2006
1977
  const overlayRect = unionRects(absoluteRects, margin);
2007
1978
  clearSvg(this.svg);
2008
1979
  syncOverlayStacking(this.svg, this.element, this.options.zIndex);
1980
+ const origin = this.wrapper ? this.wrapper.getBoundingClientRect() : { left: 0, top: 0 };
2009
1981
  this.svg.setAttribute("width", String(overlayRect.width));
2010
1982
  this.svg.setAttribute("height", String(overlayRect.height));
2011
1983
  this.svg.setAttribute("viewBox", `0 0 ${overlayRect.width} ${overlayRect.height}`);
2012
- this.svg.style.left = `${overlayRect.x}px`;
2013
- this.svg.style.top = `${overlayRect.y}px`;
1984
+ this.svg.style.left = `${overlayRect.x - origin.left}px`;
1985
+ this.svg.style.top = `${overlayRect.y - origin.top}px`;
2014
1986
  this.svg.style.width = `${overlayRect.width}px`;
2015
1987
  this.svg.style.height = `${overlayRect.height}px`;
2016
- this.svg.style.transform = "";
2017
- this.basePosition = { x: overlayRect.x, y: overlayRect.y };
2018
1988
  const strokeStyle = { color, strokeWidth, roughness, opacity };
2019
1989
  const noteStyle = {
2020
1990
  color,
@@ -2090,10 +2060,6 @@ var DoodleOverlay = class {
2090
2060
  cancelFrame(this.pendingFrame);
2091
2061
  this.pendingFrame = null;
2092
2062
  }
2093
- if (this.pendingRepositionFrame != null) {
2094
- cancelFrame(this.pendingRepositionFrame);
2095
- this.pendingRepositionFrame = null;
2096
- }
2097
2063
  if (this.resizeObserver) {
2098
2064
  this.resizeObserver.disconnect();
2099
2065
  this.resizeObserver = null;
@@ -2110,10 +2076,11 @@ var DoodleOverlay = class {
2110
2076
  target.removeEventListener(type, handler);
2111
2077
  }
2112
2078
  this.listeners = [];
2113
- if (this.svg?.parentNode) {
2114
- this.svg.parentNode.removeChild(this.svg);
2079
+ if (this.svg) {
2080
+ removeOverlaySvg(this.svg, this.wrapper, this.element);
2115
2081
  }
2116
2082
  this.svg = null;
2083
+ this.wrapper = null;
2117
2084
  instances.delete(this.element);
2118
2085
  }
2119
2086
  };
@@ -374,20 +374,6 @@ function isValidPosition(position) {
374
374
  "center"
375
375
  ].includes(position);
376
376
  }
377
- function getScrollableAncestors(element) {
378
- const ancestors = [];
379
- let node = element.parentElement;
380
- while (node && node !== document.documentElement) {
381
- const style = getComputedStyle(node);
382
- const overflow = style.overflow + style.overflowY + style.overflowX;
383
- if (/(auto|scroll|overlay)/.test(overflow)) {
384
- ancestors.push(node);
385
- }
386
- node = node.parentElement;
387
- }
388
- ancestors.push(window);
389
- return ancestors;
390
- }
391
377
  function resolveOverlayZIndex(element, override) {
392
378
  if (override != null) return override;
393
379
  const { zIndex, position } = getComputedStyle(element);
@@ -406,13 +392,45 @@ function resolveOverlayZIndex(element, override) {
406
392
  }
407
393
  return null;
408
394
  }
395
+ var ITEM_PLACEMENT_PROPERTIES = [
396
+ "order",
397
+ "flexGrow",
398
+ "flexShrink",
399
+ "flexBasis",
400
+ "alignSelf",
401
+ "justifySelf",
402
+ "gridColumn",
403
+ "gridRow",
404
+ "gridArea"
405
+ ];
406
+ function isInlineDisplay(computed) {
407
+ return computed.display.startsWith("inline");
408
+ }
409
409
  function insertOverlaySvg(svg, element) {
410
410
  const parent = element.parentNode;
411
411
  if (!parent) {
412
412
  document.body.appendChild(svg);
413
- return;
413
+ return null;
414
414
  }
415
- parent.insertBefore(svg, element.nextSibling);
415
+ const wrapper = document.createElement("div");
416
+ wrapper.style.position = "relative";
417
+ const computed = getComputedStyle(element);
418
+ wrapper.style.display = isInlineDisplay(computed) ? "inline-block" : "block";
419
+ for (const property of ITEM_PLACEMENT_PROPERTIES) {
420
+ const value = computed[property];
421
+ if (value) wrapper.style[property] = value;
422
+ }
423
+ parent.replaceChild(wrapper, element);
424
+ wrapper.appendChild(element);
425
+ wrapper.appendChild(svg);
426
+ return wrapper;
427
+ }
428
+ function removeOverlaySvg(svg, wrapper, element) {
429
+ if (svg.parentNode) {
430
+ svg.parentNode.removeChild(svg);
431
+ }
432
+ if (!wrapper || !wrapper.parentNode) return;
433
+ wrapper.parentNode.replaceChild(element, wrapper);
416
434
  }
417
435
  function syncOverlayStacking(svg, element, zIndexOverride) {
418
436
  const zIndex = resolveOverlayZIndex(element, zIndexOverride);
@@ -1300,14 +1318,13 @@ function createOverlaySvg(left, top, width, height) {
1300
1318
  svg.setAttribute("width", String(width));
1301
1319
  svg.setAttribute("height", String(height));
1302
1320
  svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
1303
- svg.style.position = "fixed";
1321
+ svg.style.position = "absolute";
1304
1322
  svg.style.left = `${left}px`;
1305
1323
  svg.style.top = `${top}px`;
1306
1324
  svg.style.width = `${width}px`;
1307
1325
  svg.style.height = `${height}px`;
1308
1326
  svg.style.pointerEvents = "none";
1309
1327
  svg.style.overflow = "visible";
1310
- svg.style.willChange = "transform";
1311
1328
  return svg;
1312
1329
  }
1313
1330
 
@@ -1856,13 +1873,11 @@ var DoodleOverlay = class {
1856
1873
  this.seed = Math.floor(Math.random() * 1e9);
1857
1874
  this.startedAt = now();
1858
1875
  this.svg = null;
1876
+ this.wrapper = null;
1859
1877
  this.resizeObserver = null;
1860
1878
  this.mutationObserver = null;
1861
1879
  this.pendingFrame = null;
1862
- this.pendingRepositionFrame = null;
1863
- this.basePosition = { x: 0, y: 0 };
1864
1880
  this.listeners = [];
1865
- this.onScroll = this.scheduleReposition.bind(this);
1866
1881
  this.onResize = this.scheduleUpdate.bind(this);
1867
1882
  this.onLanguageChange = this.scheduleUpdate.bind(this);
1868
1883
  this.childElements = [];
@@ -1870,7 +1885,7 @@ var DoodleOverlay = class {
1870
1885
  }
1871
1886
  mount() {
1872
1887
  this.svg = createOverlaySvg(0, 0, 0, 0);
1873
- insertOverlaySvg(this.svg, this.element);
1888
+ this.wrapper = insertOverlaySvg(this.svg, this.element);
1874
1889
  syncOverlayStacking(this.svg, this.element, this.options.zIndex);
1875
1890
  this.resizeObserver = new ResizeObserver(this.onResize);
1876
1891
  this.resizeObserver.observe(this.element);
@@ -1883,10 +1898,6 @@ var DoodleOverlay = class {
1883
1898
  subtree: true,
1884
1899
  attributes: true
1885
1900
  });
1886
- for (const target of getScrollableAncestors(this.element)) {
1887
- target.addEventListener("scroll", this.onScroll, { passive: true });
1888
- this.listeners.push({ target, type: "scroll", handler: this.onScroll });
1889
- }
1890
1901
  window.addEventListener("resize", this.onResize, { passive: true });
1891
1902
  this.listeners.push({
1892
1903
  target: window,
@@ -1922,46 +1933,6 @@ var DoodleOverlay = class {
1922
1933
  this.update();
1923
1934
  });
1924
1935
  }
1925
- scheduleReposition() {
1926
- if (this.pendingFrame != null || this.pendingRepositionFrame != null) {
1927
- return;
1928
- }
1929
- this.pendingRepositionFrame = scheduleFrame(() => {
1930
- this.pendingRepositionFrame = null;
1931
- this.reposition();
1932
- });
1933
- }
1934
- /**
1935
- * Cheap scroll-driven path: slide the overlay to the element's new
1936
- * viewport position without touching its contents. Scrolling can't change
1937
- * an element's size, so the rects, viewBox and every hand-drawn stroke
1938
- * inside them are still valid - only the overlay's position needs to move.
1939
- *
1940
- * That move is a `transform`, not `left`/`top`. `left`/`top` sit in the
1941
- * same box-geometry pass as layout, so the browser re-checks layout on
1942
- * every write; `transform` is composited, so the scroll path never
1943
- * touches layout at all.
1944
- */
1945
- reposition() {
1946
- if (!this.svg || !this.element.isConnected) return;
1947
- const { padding, radius } = this.options;
1948
- const targets = this.getTargets();
1949
- const margin = this.overlayMargin();
1950
- const absoluteRects = targets.map(
1951
- (target) => getElementBounds(target, padding, radius)
1952
- );
1953
- const overlayRect = unionRects(absoluteRects, margin);
1954
- const currentWidth = parseFloat(this.svg.style.width) || 0;
1955
- const currentHeight = parseFloat(this.svg.style.height) || 0;
1956
- const resized = Math.abs(overlayRect.width - currentWidth) > 0.5 || Math.abs(overlayRect.height - currentHeight) > 0.5;
1957
- if (resized) {
1958
- this.update();
1959
- return;
1960
- }
1961
- const dx = overlayRect.x - this.basePosition.x;
1962
- const dy = overlayRect.y - this.basePosition.y;
1963
- this.svg.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
1964
- }
1965
1936
  getTargets() {
1966
1937
  if (this.options.children) {
1967
1938
  const nodes = this.element.querySelectorAll(this.options.children);
@@ -1981,15 +1952,14 @@ var DoodleOverlay = class {
1981
1952
  const overlayRect = unionRects(absoluteRects, margin);
1982
1953
  clearSvg(this.svg);
1983
1954
  syncOverlayStacking(this.svg, this.element, this.options.zIndex);
1955
+ const origin = this.wrapper ? this.wrapper.getBoundingClientRect() : { left: 0, top: 0 };
1984
1956
  this.svg.setAttribute("width", String(overlayRect.width));
1985
1957
  this.svg.setAttribute("height", String(overlayRect.height));
1986
1958
  this.svg.setAttribute("viewBox", `0 0 ${overlayRect.width} ${overlayRect.height}`);
1987
- this.svg.style.left = `${overlayRect.x}px`;
1988
- this.svg.style.top = `${overlayRect.y}px`;
1959
+ this.svg.style.left = `${overlayRect.x - origin.left}px`;
1960
+ this.svg.style.top = `${overlayRect.y - origin.top}px`;
1989
1961
  this.svg.style.width = `${overlayRect.width}px`;
1990
1962
  this.svg.style.height = `${overlayRect.height}px`;
1991
- this.svg.style.transform = "";
1992
- this.basePosition = { x: overlayRect.x, y: overlayRect.y };
1993
1963
  const strokeStyle = { color, strokeWidth, roughness, opacity };
1994
1964
  const noteStyle = {
1995
1965
  color,
@@ -2065,10 +2035,6 @@ var DoodleOverlay = class {
2065
2035
  cancelFrame(this.pendingFrame);
2066
2036
  this.pendingFrame = null;
2067
2037
  }
2068
- if (this.pendingRepositionFrame != null) {
2069
- cancelFrame(this.pendingRepositionFrame);
2070
- this.pendingRepositionFrame = null;
2071
- }
2072
2038
  if (this.resizeObserver) {
2073
2039
  this.resizeObserver.disconnect();
2074
2040
  this.resizeObserver = null;
@@ -2085,10 +2051,11 @@ var DoodleOverlay = class {
2085
2051
  target.removeEventListener(type, handler);
2086
2052
  }
2087
2053
  this.listeners = [];
2088
- if (this.svg?.parentNode) {
2089
- this.svg.parentNode.removeChild(this.svg);
2054
+ if (this.svg) {
2055
+ removeOverlaySvg(this.svg, this.wrapper, this.element);
2090
2056
  }
2091
2057
  this.svg = null;
2058
+ this.wrapper = null;
2092
2059
  instances.delete(this.element);
2093
2060
  }
2094
2061
  };
package/dist/react.cjs CHANGED
@@ -404,20 +404,6 @@ function isValidPosition(position) {
404
404
  "center"
405
405
  ].includes(position);
406
406
  }
407
- function getScrollableAncestors(element) {
408
- const ancestors = [];
409
- let node = element.parentElement;
410
- while (node && node !== document.documentElement) {
411
- const style = getComputedStyle(node);
412
- const overflow = style.overflow + style.overflowY + style.overflowX;
413
- if (/(auto|scroll|overlay)/.test(overflow)) {
414
- ancestors.push(node);
415
- }
416
- node = node.parentElement;
417
- }
418
- ancestors.push(window);
419
- return ancestors;
420
- }
421
407
  function resolveOverlayZIndex(element, override) {
422
408
  if (override != null) return override;
423
409
  const { zIndex, position } = getComputedStyle(element);
@@ -436,13 +422,45 @@ function resolveOverlayZIndex(element, override) {
436
422
  }
437
423
  return null;
438
424
  }
425
+ var ITEM_PLACEMENT_PROPERTIES = [
426
+ "order",
427
+ "flexGrow",
428
+ "flexShrink",
429
+ "flexBasis",
430
+ "alignSelf",
431
+ "justifySelf",
432
+ "gridColumn",
433
+ "gridRow",
434
+ "gridArea"
435
+ ];
436
+ function isInlineDisplay(computed) {
437
+ return computed.display.startsWith("inline");
438
+ }
439
439
  function insertOverlaySvg(svg, element) {
440
440
  const parent = element.parentNode;
441
441
  if (!parent) {
442
442
  document.body.appendChild(svg);
443
- return;
443
+ return null;
444
444
  }
445
- parent.insertBefore(svg, element.nextSibling);
445
+ const wrapper = document.createElement("div");
446
+ wrapper.style.position = "relative";
447
+ const computed = getComputedStyle(element);
448
+ wrapper.style.display = isInlineDisplay(computed) ? "inline-block" : "block";
449
+ for (const property of ITEM_PLACEMENT_PROPERTIES) {
450
+ const value = computed[property];
451
+ if (value) wrapper.style[property] = value;
452
+ }
453
+ parent.replaceChild(wrapper, element);
454
+ wrapper.appendChild(element);
455
+ wrapper.appendChild(svg);
456
+ return wrapper;
457
+ }
458
+ function removeOverlaySvg(svg, wrapper, element) {
459
+ if (svg.parentNode) {
460
+ svg.parentNode.removeChild(svg);
461
+ }
462
+ if (!wrapper || !wrapper.parentNode) return;
463
+ wrapper.parentNode.replaceChild(element, wrapper);
446
464
  }
447
465
  function syncOverlayStacking(svg, element, zIndexOverride) {
448
466
  const zIndex = resolveOverlayZIndex(element, zIndexOverride);
@@ -1330,14 +1348,13 @@ function createOverlaySvg(left, top, width, height) {
1330
1348
  svg.setAttribute("width", String(width));
1331
1349
  svg.setAttribute("height", String(height));
1332
1350
  svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
1333
- svg.style.position = "fixed";
1351
+ svg.style.position = "absolute";
1334
1352
  svg.style.left = `${left}px`;
1335
1353
  svg.style.top = `${top}px`;
1336
1354
  svg.style.width = `${width}px`;
1337
1355
  svg.style.height = `${height}px`;
1338
1356
  svg.style.pointerEvents = "none";
1339
1357
  svg.style.overflow = "visible";
1340
- svg.style.willChange = "transform";
1341
1358
  return svg;
1342
1359
  }
1343
1360
 
@@ -1886,13 +1903,11 @@ var DoodleOverlay = class {
1886
1903
  this.seed = Math.floor(Math.random() * 1e9);
1887
1904
  this.startedAt = now();
1888
1905
  this.svg = null;
1906
+ this.wrapper = null;
1889
1907
  this.resizeObserver = null;
1890
1908
  this.mutationObserver = null;
1891
1909
  this.pendingFrame = null;
1892
- this.pendingRepositionFrame = null;
1893
- this.basePosition = { x: 0, y: 0 };
1894
1910
  this.listeners = [];
1895
- this.onScroll = this.scheduleReposition.bind(this);
1896
1911
  this.onResize = this.scheduleUpdate.bind(this);
1897
1912
  this.onLanguageChange = this.scheduleUpdate.bind(this);
1898
1913
  this.childElements = [];
@@ -1900,7 +1915,7 @@ var DoodleOverlay = class {
1900
1915
  }
1901
1916
  mount() {
1902
1917
  this.svg = createOverlaySvg(0, 0, 0, 0);
1903
- insertOverlaySvg(this.svg, this.element);
1918
+ this.wrapper = insertOverlaySvg(this.svg, this.element);
1904
1919
  syncOverlayStacking(this.svg, this.element, this.options.zIndex);
1905
1920
  this.resizeObserver = new ResizeObserver(this.onResize);
1906
1921
  this.resizeObserver.observe(this.element);
@@ -1913,10 +1928,6 @@ var DoodleOverlay = class {
1913
1928
  subtree: true,
1914
1929
  attributes: true
1915
1930
  });
1916
- for (const target of getScrollableAncestors(this.element)) {
1917
- target.addEventListener("scroll", this.onScroll, { passive: true });
1918
- this.listeners.push({ target, type: "scroll", handler: this.onScroll });
1919
- }
1920
1931
  window.addEventListener("resize", this.onResize, { passive: true });
1921
1932
  this.listeners.push({
1922
1933
  target: window,
@@ -1952,46 +1963,6 @@ var DoodleOverlay = class {
1952
1963
  this.update();
1953
1964
  });
1954
1965
  }
1955
- scheduleReposition() {
1956
- if (this.pendingFrame != null || this.pendingRepositionFrame != null) {
1957
- return;
1958
- }
1959
- this.pendingRepositionFrame = scheduleFrame(() => {
1960
- this.pendingRepositionFrame = null;
1961
- this.reposition();
1962
- });
1963
- }
1964
- /**
1965
- * Cheap scroll-driven path: slide the overlay to the element's new
1966
- * viewport position without touching its contents. Scrolling can't change
1967
- * an element's size, so the rects, viewBox and every hand-drawn stroke
1968
- * inside them are still valid - only the overlay's position needs to move.
1969
- *
1970
- * That move is a `transform`, not `left`/`top`. `left`/`top` sit in the
1971
- * same box-geometry pass as layout, so the browser re-checks layout on
1972
- * every write; `transform` is composited, so the scroll path never
1973
- * touches layout at all.
1974
- */
1975
- reposition() {
1976
- if (!this.svg || !this.element.isConnected) return;
1977
- const { padding, radius } = this.options;
1978
- const targets = this.getTargets();
1979
- const margin = this.overlayMargin();
1980
- const absoluteRects = targets.map(
1981
- (target) => getElementBounds(target, padding, radius)
1982
- );
1983
- const overlayRect = unionRects(absoluteRects, margin);
1984
- const currentWidth = parseFloat(this.svg.style.width) || 0;
1985
- const currentHeight = parseFloat(this.svg.style.height) || 0;
1986
- const resized = Math.abs(overlayRect.width - currentWidth) > 0.5 || Math.abs(overlayRect.height - currentHeight) > 0.5;
1987
- if (resized) {
1988
- this.update();
1989
- return;
1990
- }
1991
- const dx = overlayRect.x - this.basePosition.x;
1992
- const dy = overlayRect.y - this.basePosition.y;
1993
- this.svg.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
1994
- }
1995
1966
  getTargets() {
1996
1967
  if (this.options.children) {
1997
1968
  const nodes = this.element.querySelectorAll(this.options.children);
@@ -2011,15 +1982,14 @@ var DoodleOverlay = class {
2011
1982
  const overlayRect = unionRects(absoluteRects, margin);
2012
1983
  clearSvg(this.svg);
2013
1984
  syncOverlayStacking(this.svg, this.element, this.options.zIndex);
1985
+ const origin = this.wrapper ? this.wrapper.getBoundingClientRect() : { left: 0, top: 0 };
2014
1986
  this.svg.setAttribute("width", String(overlayRect.width));
2015
1987
  this.svg.setAttribute("height", String(overlayRect.height));
2016
1988
  this.svg.setAttribute("viewBox", `0 0 ${overlayRect.width} ${overlayRect.height}`);
2017
- this.svg.style.left = `${overlayRect.x}px`;
2018
- this.svg.style.top = `${overlayRect.y}px`;
1989
+ this.svg.style.left = `${overlayRect.x - origin.left}px`;
1990
+ this.svg.style.top = `${overlayRect.y - origin.top}px`;
2019
1991
  this.svg.style.width = `${overlayRect.width}px`;
2020
1992
  this.svg.style.height = `${overlayRect.height}px`;
2021
- this.svg.style.transform = "";
2022
- this.basePosition = { x: overlayRect.x, y: overlayRect.y };
2023
1993
  const strokeStyle = { color, strokeWidth, roughness, opacity };
2024
1994
  const noteStyle = {
2025
1995
  color,
@@ -2095,10 +2065,6 @@ var DoodleOverlay = class {
2095
2065
  cancelFrame(this.pendingFrame);
2096
2066
  this.pendingFrame = null;
2097
2067
  }
2098
- if (this.pendingRepositionFrame != null) {
2099
- cancelFrame(this.pendingRepositionFrame);
2100
- this.pendingRepositionFrame = null;
2101
- }
2102
2068
  if (this.resizeObserver) {
2103
2069
  this.resizeObserver.disconnect();
2104
2070
  this.resizeObserver = null;
@@ -2115,10 +2081,11 @@ var DoodleOverlay = class {
2115
2081
  target.removeEventListener(type, handler);
2116
2082
  }
2117
2083
  this.listeners = [];
2118
- if (this.svg?.parentNode) {
2119
- this.svg.parentNode.removeChild(this.svg);
2084
+ if (this.svg) {
2085
+ removeOverlaySvg(this.svg, this.wrapper, this.element);
2120
2086
  }
2121
2087
  this.svg = null;
2088
+ this.wrapper = null;
2122
2089
  instances.delete(this.element);
2123
2090
  }
2124
2091
  };
package/dist/react.js CHANGED
@@ -387,20 +387,6 @@ function isValidPosition(position) {
387
387
  "center"
388
388
  ].includes(position);
389
389
  }
390
- function getScrollableAncestors(element) {
391
- const ancestors = [];
392
- let node = element.parentElement;
393
- while (node && node !== document.documentElement) {
394
- const style = getComputedStyle(node);
395
- const overflow = style.overflow + style.overflowY + style.overflowX;
396
- if (/(auto|scroll|overlay)/.test(overflow)) {
397
- ancestors.push(node);
398
- }
399
- node = node.parentElement;
400
- }
401
- ancestors.push(window);
402
- return ancestors;
403
- }
404
390
  function resolveOverlayZIndex(element, override) {
405
391
  if (override != null) return override;
406
392
  const { zIndex, position } = getComputedStyle(element);
@@ -419,13 +405,45 @@ function resolveOverlayZIndex(element, override) {
419
405
  }
420
406
  return null;
421
407
  }
408
+ var ITEM_PLACEMENT_PROPERTIES = [
409
+ "order",
410
+ "flexGrow",
411
+ "flexShrink",
412
+ "flexBasis",
413
+ "alignSelf",
414
+ "justifySelf",
415
+ "gridColumn",
416
+ "gridRow",
417
+ "gridArea"
418
+ ];
419
+ function isInlineDisplay(computed) {
420
+ return computed.display.startsWith("inline");
421
+ }
422
422
  function insertOverlaySvg(svg, element) {
423
423
  const parent = element.parentNode;
424
424
  if (!parent) {
425
425
  document.body.appendChild(svg);
426
- return;
426
+ return null;
427
427
  }
428
- parent.insertBefore(svg, element.nextSibling);
428
+ const wrapper = document.createElement("div");
429
+ wrapper.style.position = "relative";
430
+ const computed = getComputedStyle(element);
431
+ wrapper.style.display = isInlineDisplay(computed) ? "inline-block" : "block";
432
+ for (const property of ITEM_PLACEMENT_PROPERTIES) {
433
+ const value = computed[property];
434
+ if (value) wrapper.style[property] = value;
435
+ }
436
+ parent.replaceChild(wrapper, element);
437
+ wrapper.appendChild(element);
438
+ wrapper.appendChild(svg);
439
+ return wrapper;
440
+ }
441
+ function removeOverlaySvg(svg, wrapper, element) {
442
+ if (svg.parentNode) {
443
+ svg.parentNode.removeChild(svg);
444
+ }
445
+ if (!wrapper || !wrapper.parentNode) return;
446
+ wrapper.parentNode.replaceChild(element, wrapper);
429
447
  }
430
448
  function syncOverlayStacking(svg, element, zIndexOverride) {
431
449
  const zIndex = resolveOverlayZIndex(element, zIndexOverride);
@@ -1313,14 +1331,13 @@ function createOverlaySvg(left, top, width, height) {
1313
1331
  svg.setAttribute("width", String(width));
1314
1332
  svg.setAttribute("height", String(height));
1315
1333
  svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
1316
- svg.style.position = "fixed";
1334
+ svg.style.position = "absolute";
1317
1335
  svg.style.left = `${left}px`;
1318
1336
  svg.style.top = `${top}px`;
1319
1337
  svg.style.width = `${width}px`;
1320
1338
  svg.style.height = `${height}px`;
1321
1339
  svg.style.pointerEvents = "none";
1322
1340
  svg.style.overflow = "visible";
1323
- svg.style.willChange = "transform";
1324
1341
  return svg;
1325
1342
  }
1326
1343
 
@@ -1869,13 +1886,11 @@ var DoodleOverlay = class {
1869
1886
  this.seed = Math.floor(Math.random() * 1e9);
1870
1887
  this.startedAt = now();
1871
1888
  this.svg = null;
1889
+ this.wrapper = null;
1872
1890
  this.resizeObserver = null;
1873
1891
  this.mutationObserver = null;
1874
1892
  this.pendingFrame = null;
1875
- this.pendingRepositionFrame = null;
1876
- this.basePosition = { x: 0, y: 0 };
1877
1893
  this.listeners = [];
1878
- this.onScroll = this.scheduleReposition.bind(this);
1879
1894
  this.onResize = this.scheduleUpdate.bind(this);
1880
1895
  this.onLanguageChange = this.scheduleUpdate.bind(this);
1881
1896
  this.childElements = [];
@@ -1883,7 +1898,7 @@ var DoodleOverlay = class {
1883
1898
  }
1884
1899
  mount() {
1885
1900
  this.svg = createOverlaySvg(0, 0, 0, 0);
1886
- insertOverlaySvg(this.svg, this.element);
1901
+ this.wrapper = insertOverlaySvg(this.svg, this.element);
1887
1902
  syncOverlayStacking(this.svg, this.element, this.options.zIndex);
1888
1903
  this.resizeObserver = new ResizeObserver(this.onResize);
1889
1904
  this.resizeObserver.observe(this.element);
@@ -1896,10 +1911,6 @@ var DoodleOverlay = class {
1896
1911
  subtree: true,
1897
1912
  attributes: true
1898
1913
  });
1899
- for (const target of getScrollableAncestors(this.element)) {
1900
- target.addEventListener("scroll", this.onScroll, { passive: true });
1901
- this.listeners.push({ target, type: "scroll", handler: this.onScroll });
1902
- }
1903
1914
  window.addEventListener("resize", this.onResize, { passive: true });
1904
1915
  this.listeners.push({
1905
1916
  target: window,
@@ -1935,46 +1946,6 @@ var DoodleOverlay = class {
1935
1946
  this.update();
1936
1947
  });
1937
1948
  }
1938
- scheduleReposition() {
1939
- if (this.pendingFrame != null || this.pendingRepositionFrame != null) {
1940
- return;
1941
- }
1942
- this.pendingRepositionFrame = scheduleFrame(() => {
1943
- this.pendingRepositionFrame = null;
1944
- this.reposition();
1945
- });
1946
- }
1947
- /**
1948
- * Cheap scroll-driven path: slide the overlay to the element's new
1949
- * viewport position without touching its contents. Scrolling can't change
1950
- * an element's size, so the rects, viewBox and every hand-drawn stroke
1951
- * inside them are still valid - only the overlay's position needs to move.
1952
- *
1953
- * That move is a `transform`, not `left`/`top`. `left`/`top` sit in the
1954
- * same box-geometry pass as layout, so the browser re-checks layout on
1955
- * every write; `transform` is composited, so the scroll path never
1956
- * touches layout at all.
1957
- */
1958
- reposition() {
1959
- if (!this.svg || !this.element.isConnected) return;
1960
- const { padding, radius } = this.options;
1961
- const targets = this.getTargets();
1962
- const margin = this.overlayMargin();
1963
- const absoluteRects = targets.map(
1964
- (target) => getElementBounds(target, padding, radius)
1965
- );
1966
- const overlayRect = unionRects(absoluteRects, margin);
1967
- const currentWidth = parseFloat(this.svg.style.width) || 0;
1968
- const currentHeight = parseFloat(this.svg.style.height) || 0;
1969
- const resized = Math.abs(overlayRect.width - currentWidth) > 0.5 || Math.abs(overlayRect.height - currentHeight) > 0.5;
1970
- if (resized) {
1971
- this.update();
1972
- return;
1973
- }
1974
- const dx = overlayRect.x - this.basePosition.x;
1975
- const dy = overlayRect.y - this.basePosition.y;
1976
- this.svg.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
1977
- }
1978
1949
  getTargets() {
1979
1950
  if (this.options.children) {
1980
1951
  const nodes = this.element.querySelectorAll(this.options.children);
@@ -1994,15 +1965,14 @@ var DoodleOverlay = class {
1994
1965
  const overlayRect = unionRects(absoluteRects, margin);
1995
1966
  clearSvg(this.svg);
1996
1967
  syncOverlayStacking(this.svg, this.element, this.options.zIndex);
1968
+ const origin = this.wrapper ? this.wrapper.getBoundingClientRect() : { left: 0, top: 0 };
1997
1969
  this.svg.setAttribute("width", String(overlayRect.width));
1998
1970
  this.svg.setAttribute("height", String(overlayRect.height));
1999
1971
  this.svg.setAttribute("viewBox", `0 0 ${overlayRect.width} ${overlayRect.height}`);
2000
- this.svg.style.left = `${overlayRect.x}px`;
2001
- this.svg.style.top = `${overlayRect.y}px`;
1972
+ this.svg.style.left = `${overlayRect.x - origin.left}px`;
1973
+ this.svg.style.top = `${overlayRect.y - origin.top}px`;
2002
1974
  this.svg.style.width = `${overlayRect.width}px`;
2003
1975
  this.svg.style.height = `${overlayRect.height}px`;
2004
- this.svg.style.transform = "";
2005
- this.basePosition = { x: overlayRect.x, y: overlayRect.y };
2006
1976
  const strokeStyle = { color, strokeWidth, roughness, opacity };
2007
1977
  const noteStyle = {
2008
1978
  color,
@@ -2078,10 +2048,6 @@ var DoodleOverlay = class {
2078
2048
  cancelFrame(this.pendingFrame);
2079
2049
  this.pendingFrame = null;
2080
2050
  }
2081
- if (this.pendingRepositionFrame != null) {
2082
- cancelFrame(this.pendingRepositionFrame);
2083
- this.pendingRepositionFrame = null;
2084
- }
2085
2051
  if (this.resizeObserver) {
2086
2052
  this.resizeObserver.disconnect();
2087
2053
  this.resizeObserver = null;
@@ -2098,10 +2064,11 @@ var DoodleOverlay = class {
2098
2064
  target.removeEventListener(type, handler);
2099
2065
  }
2100
2066
  this.listeners = [];
2101
- if (this.svg?.parentNode) {
2102
- this.svg.parentNode.removeChild(this.svg);
2067
+ if (this.svg) {
2068
+ removeOverlaySvg(this.svg, this.wrapper, this.element);
2103
2069
  }
2104
2070
  this.svg = null;
2071
+ this.wrapper = null;
2105
2072
  instances.delete(this.element);
2106
2073
  }
2107
2074
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aznabee/freehand-ui",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Wrap any existing web element with a beautiful, responsive, hand-drawn doodle layer",
5
5
  "type": "module",
6
6
  "main": "./dist/freehand-ui.cjs",