@neo-ui-kit/react 1.3.12 → 1.3.14

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
@@ -4,6 +4,20 @@ Migration: replace `@ad-voice/ui` with `@neo-ui-kit/react` in dependencies and i
4
4
 
5
5
  NEO UI — React + TypeScript UI kit: glass/ruby surfaces, animated neon borders, fields, tabs, dialogs, audio controls and a melody editor.
6
6
 
7
+ ## 1.3.14
8
+
9
+ Animated borders composite their moving lights in bounded buffers instead of repainting full-size intermediate surfaces. The same masks, glow textures, speeds and overlapping-light composition are preserved.
10
+
11
+ LevelMeter computes each sample's SVG coordinates once, writes only the final contour after a delayed frame, and skips identical contours. Sampling, attack/release, SVG geometry, gradients and glow remain unchanged. CPU ×8/×16 measurements and limitations are recorded in the performance report; this does not establish a universal 60 FPS guarantee.
12
+
13
+ The unchanged SVG subtree is reused across signal updates. A committed value reaches the renderer before the next paint, including when waking from silence; accessible values still update with the signal.
14
+
15
+ LevelMeter renders its waveform in a shared worker when supported, retaining the SVG fallback and existing glow. Its accessible values remain on the main thread.
16
+
17
+ ## 1.3.13
18
+
19
+ MelodyRoll skips repainting empty glow/voice/trail layers and a settled keyboard. A disappearing voice still clears its last frame, and keyboard hover, press and hit transitions retain their final frame. See the [performance report](https://github.com/Pro100Dimka/adui/blob/main/evidence/performance-report.md) for karaoke measurements and remaining limits under CPU ×16.
20
+
7
21
  ## 1.3.12
8
22
 
9
23
  - MelodyRoll avoids repeated full style measurements for unrelated animated CSS variables and native root shadows while its beat is idle. Theme aliases still trigger reconfiguration.
@@ -47,7 +61,7 @@ import "@neo-ui-kit/react/styles.css";
47
61
  To test repository changes before publishing, run `npm run package` from the repository root and install the resulting archive in a separate application:
48
62
 
49
63
  ```bash
50
- npm install /path/to/adui/release/neo-ui-kit-react-1.3.12.tgz
64
+ npm install /path/to/adui/release/neo-ui-kit-react-1.3.14.tgz
51
65
  ```
52
66
 
53
67
  Maintainers publish new versions from `Pro100Dimka/adui` using the `release.yml` GitHub Actions workflow, a matching `v<version>` tag, and the `npm-publication` environment. npm trusts its OIDC identity; no `NPM_TOKEN` is required. The workflow checks the project and packed consumer before publishing the tested archive. Published `1.0.0` cannot be overwritten: code and README changes in npm require a new package version. See the [repository release instructions](https://github.com/Pro100Dimka/adui#npm-пакет).
@@ -20,23 +20,55 @@ function renderBorders() {
20
20
  }
21
21
  };
22
22
  const draw = (scene) => {
23
- const { ctx, width, height, points, length, radius } = scene;
23
+ const { ctx, width, height, points, length, radius, dpr } = scene;
24
+ const lights = scene.lights.map((light) => {
25
+ const index = (light.phase + scene.elapsed * light.speed / length) % 1 * 1024;
26
+ const lo = Math.floor(index), hi = (lo + 1) % 1024, weight = index - lo;
27
+ return {
28
+ x: points[lo * 2] * (1 - weight) + points[hi * 2] * weight - radius + 16,
29
+ y: points[lo * 2 + 1] * (1 - weight) + points[hi * 2 + 1] * weight - radius + 16
30
+ };
31
+ });
32
+ const regions = lights.map(({ x, y }) => ({
33
+ x: Math.max(0, Math.floor(x * dpr - 1) / dpr),
34
+ y: Math.max(0, Math.floor(y * dpr - 1) / dpr),
35
+ right: Math.min(scene.canvas.width, Math.ceil((x + radius * 2) * dpr + 1)) / dpr,
36
+ bottom: Math.min(scene.canvas.height, Math.ceil((y + radius * 2) * dpr + 1)) / dpr
37
+ }));
38
+ const [first, second] = regions;
39
+ if (second && first.x < second.right && second.x < first.right && first.y < second.bottom && second.y < first.bottom) {
40
+ regions.splice(0, 2, {
41
+ x: Math.min(first.x, second.x),
42
+ y: Math.min(first.y, second.y),
43
+ right: Math.max(first.right, second.right),
44
+ bottom: Math.max(first.bottom, second.bottom)
45
+ });
46
+ }
24
47
  ctx.clearRect(0, 0, width, height);
25
48
  for (const layer of scene.layers) {
26
49
  const paint = layer.ctx;
27
- paint.globalCompositeOperation = "source-over";
28
- paint.clearRect(0, 0, width, height);
29
- for (const light of scene.lights) {
30
- const index = (light.phase + scene.elapsed * light.speed / length) % 1 * 1024;
31
- const lo = Math.floor(index), hi = (lo + 1) % 1024, weight = index - lo;
32
- const x = points[lo * 2] * (1 - weight) + points[hi * 2] * weight;
33
- const y = points[lo * 2 + 1] * (1 - weight) + points[hi * 2 + 1] * weight;
34
- paint.drawImage(layer.texture, x - radius + 16, y - radius + 16, radius * 2, radius * 2);
35
- }
36
- paint.globalCompositeOperation = "destination-in";
37
- paint.drawImage(layer.mask, 0, 0, width, height);
38
50
  ctx.globalAlpha = layer.opacity;
39
- ctx.drawImage(layer.buffer, 0, 0, width, height);
51
+ for (const { x, y, right, bottom } of regions) {
52
+ const w = right - x, h = bottom - y;
53
+ paint.globalCompositeOperation = "source-over";
54
+ paint.clearRect(0, 0, layer.buffer.width / dpr, layer.buffer.height / dpr);
55
+ for (const light of lights)
56
+ paint.drawImage(layer.texture, light.x - x, light.y - y, radius * 2, radius * 2);
57
+ paint.globalCompositeOperation = "destination-in";
58
+ paint.drawImage(
59
+ layer.mask,
60
+ x * layer.mask.width / width,
61
+ y * layer.mask.height / height,
62
+ w * layer.mask.width / width,
63
+ h * layer.mask.height / height,
64
+ 0,
65
+ 0,
66
+ w,
67
+ h
68
+ );
69
+ const scaleX = width * dpr / scene.canvas.width, scaleY = height * dpr / scene.canvas.height;
70
+ ctx.drawImage(layer.buffer, 0, 0, w * dpr, h * dpr, x * scaleX, y * scaleY, w * scaleX, h * scaleY);
71
+ }
40
72
  }
41
73
  ctx.globalAlpha = 1;
42
74
  };
@@ -76,7 +108,10 @@ function renderBorders() {
76
108
  next.ctx = next.canvas.getContext("2d");
77
109
  next.ctx.setTransform(data.dpr, 0, 0, data.dpr, 0, 0);
78
110
  for (const layer of next.layers) {
79
- layer.buffer = new OffscreenCanvas(next.canvas.width, next.canvas.height);
111
+ layer.buffer = new OffscreenCanvas(
112
+ Math.min(next.canvas.width, Math.ceil(next.radius * 4 * data.dpr) + 4),
113
+ Math.min(next.canvas.height, Math.ceil(next.radius * 4 * data.dpr) + 4)
114
+ );
80
115
  layer.ctx = layer.buffer.getContext("2d");
81
116
  layer.ctx.setTransform(data.dpr, 0, 0, data.dpr, 0, 0);
82
117
  }
@@ -2,7 +2,7 @@ import {
2
2
  themes,
3
3
  useBorder,
4
4
  usePauseOffscreen
5
- } from "./chunk-OCH27SNY.js";
5
+ } from "./chunk-BHX5MDJL.js";
6
6
  import {
7
7
  assignRef,
8
8
  clamp,
@@ -13,6 +13,16 @@
13
13
  0 0 0.22rem color-mix(in srgb, var(--ad-red) 78%, transparent)
14
14
  );
15
15
  }
16
+ .ad-level-meter-canvas {
17
+ position: absolute;
18
+ inset: 0;
19
+ width: 100%;
20
+ height: 100%;
21
+ pointer-events: none;
22
+ }
23
+ .ad-level-meter[data-ad-canvas] > svg {
24
+ visibility: hidden;
25
+ }
16
26
  .ad-level-meter-axis {
17
27
  stroke: color-mix(in srgb, var(--ad-red) 24%, transparent);
18
28
  stroke-width: 0.7;
package/dist/core.js CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  useSmoothWheel,
12
12
  useTabShape,
13
13
  useTick
14
- } from "./chunks/chunk-OCH27SNY.js";
14
+ } from "./chunks/chunk-BHX5MDJL.js";
15
15
  import {
16
16
  LocaleProvider,
17
17
  addMessages,
package/dist/forms.js CHANGED
@@ -4,8 +4,8 @@ import {
4
4
  defaultFieldRegistry,
5
5
  useForm,
6
6
  useFormContext
7
- } from "./chunks/chunk-L6VCC7AS.js";
8
- import "./chunks/chunk-OCH27SNY.js";
7
+ } from "./chunks/chunk-VEAA27XZ.js";
8
+ import "./chunks/chunk-BHX5MDJL.js";
9
9
  import "./chunks/chunk-BJSFN52W.js";
10
10
  import "./chunks/chunk-FCOU3ODR.js";
11
11
  export {
package/dist/index.js CHANGED
@@ -62,7 +62,7 @@ import {
62
62
  useSvgId,
63
63
  variantMaterial,
64
64
  vectorElement
65
- } from "./chunks/chunk-L6VCC7AS.js";
65
+ } from "./chunks/chunk-VEAA27XZ.js";
66
66
  import {
67
67
  ThemeProvider,
68
68
  createNeonRenderer,
@@ -79,7 +79,7 @@ import {
79
79
  useTabShape,
80
80
  useThemePalette,
81
81
  useTick
82
- } from "./chunks/chunk-OCH27SNY.js";
82
+ } from "./chunks/chunk-BHX5MDJL.js";
83
83
  import {
84
84
  LocaleProvider,
85
85
  addMessages,
@@ -5968,20 +5968,262 @@ function PlaybackTrack({ audio, playing, position: controlledPosition, disabled,
5968
5968
  }
5969
5969
 
5970
5970
  // src/components/media/LevelMeter/LevelMeter.tsx
5971
- import { useEffect as useEffect13, useRef as useRef27 } from "react";
5971
+ import { useEffect as useEffect13, useLayoutEffect as useLayoutEffect5, useMemo as useMemo5, useRef as useRef27 } from "react";
5972
5972
  import { jsx as jsx48, jsxs as jsxs39 } from "react/jsx-runtime";
5973
5973
  var SAMPLES = 64;
5974
5974
  var WIDTH = 256;
5975
5975
  var HEIGHT = 24;
5976
5976
  var MID2 = HEIGHT / 2;
5977
5977
  var STEP = WIDTH / (SAMPLES - 1);
5978
+ var X = Array.from({ length: SAMPLES + 1 }, (_, i) => (i * STEP).toFixed(2));
5978
5979
  var SAMPLE_MS = 28;
5980
+ var waveSample = (level) => {
5981
+ const height = 1 + level ** 0.68 * (MID2 - 3);
5982
+ return { level, upper: (MID2 - height).toFixed(2), lower: (MID2 + height).toFixed(2) };
5983
+ };
5979
5984
  var wavePath2 = (samples) => {
5980
- const point = (i, level, side) => `${(i * STEP).toFixed(2)} ${(MID2 + side * (1 + level ** 0.68 * (MID2 - 3))).toFixed(2)}`;
5981
- const upper = samples.map((level, i) => point(i, level, -1));
5982
- const lower = samples.map((level, i) => point(i, level, 1)).reverse();
5985
+ const upper = samples.map((sample, i) => `${X[i]} ${sample.upper}`);
5986
+ const lower = samples.map((sample, i) => `${X[i]} ${sample.lower}`).reverse();
5983
5987
  return `M${upper.join("L")}L${lower.join("L")}Z`;
5984
5988
  };
5989
+ function renderMeters() {
5990
+ const scenes = /* @__PURE__ */ new Map();
5991
+ onmessage = ({ data }) => {
5992
+ if (data.remove !== void 0) {
5993
+ scenes.delete(data.remove);
5994
+ return;
5995
+ }
5996
+ const completed = [], failed = [];
5997
+ for (const frame of data.frames ?? []) {
5998
+ try {
5999
+ let scene = scenes.get(frame.id);
6000
+ if (frame.canvas) {
6001
+ const ctx2 = frame.canvas.getContext("2d");
6002
+ if (!ctx2 || !frame.style) throw new Error("Canvas unavailable");
6003
+ scene = { canvas: frame.canvas, ctx: ctx2, style: frame.style, fill: "", path: new Path2D(), revision: frame.revision };
6004
+ scenes.set(frame.id, scene);
6005
+ frame.canvas.addEventListener("contextlost", () => {
6006
+ scenes.delete(frame.id);
6007
+ postMessage({ failed: [frame.id] });
6008
+ });
6009
+ }
6010
+ if (!scene) continue;
6011
+ const { canvas, ctx } = scene;
6012
+ if (frame.style) {
6013
+ scene.style = frame.style;
6014
+ scene.revision = frame.revision;
6015
+ canvas.width = frame.style.width;
6016
+ canvas.height = frame.style.height;
6017
+ scene.fill = frame.style.fill;
6018
+ if (frame.style.stops.length) {
6019
+ const gradient = ctx.createLinearGradient(0, 0, 260.06, 0);
6020
+ for (const stop of frame.style.stops) gradient.addColorStop(stop.at, stop.color);
6021
+ scene.fill = gradient;
6022
+ }
6023
+ }
6024
+ if (frame.d !== void 0) scene.path = new Path2D(frame.d);
6025
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
6026
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
6027
+ ctx.scale(canvas.width / 256, canvas.height / 24);
6028
+ ctx.globalAlpha = scene.style.lineAlpha;
6029
+ ctx.strokeStyle = scene.style.stroke;
6030
+ ctx.lineWidth = scene.style.lineWidth;
6031
+ ctx.beginPath();
6032
+ ctx.moveTo(0, 12);
6033
+ ctx.lineTo(256, 12);
6034
+ ctx.stroke();
6035
+ ctx.translate(frame.offset, 0);
6036
+ ctx.globalAlpha = scene.style.fillAlpha;
6037
+ ctx.fillStyle = scene.fill;
6038
+ ctx.fill(scene.path);
6039
+ completed.push({ id: frame.id, revision: scene.revision });
6040
+ } catch {
6041
+ scenes.delete(frame.id);
6042
+ failed.push(frame.id);
6043
+ }
6044
+ }
6045
+ postMessage({ ack: true, completed, failed });
6046
+ };
6047
+ }
6048
+ var meterWorker;
6049
+ var meterUrl;
6050
+ var meterSerial = 0;
6051
+ var meterBusy = false;
6052
+ var meterQueued = false;
6053
+ var meterClients = /* @__PURE__ */ new Map();
6054
+ var meterFrames = /* @__PURE__ */ new Map();
6055
+ function stopMeters() {
6056
+ meterWorker?.terminate();
6057
+ if (meterUrl) URL.revokeObjectURL(meterUrl);
6058
+ meterWorker = void 0;
6059
+ meterUrl = void 0;
6060
+ meterBusy = false;
6061
+ meterFrames.clear();
6062
+ }
6063
+ function failMeters() {
6064
+ for (const client of [...meterClients.values()]) client.dispose();
6065
+ stopMeters();
6066
+ }
6067
+ function sendMeter(frame) {
6068
+ if (frame) {
6069
+ const pending = meterFrames.get(frame.id);
6070
+ if (pending) Object.assign(pending, frame);
6071
+ else meterFrames.set(frame.id, frame);
6072
+ }
6073
+ if (meterQueued || meterBusy || !meterWorker || !meterFrames.size) return;
6074
+ meterQueued = true;
6075
+ queueMicrotask(() => {
6076
+ meterQueued = false;
6077
+ if (!meterWorker || meterBusy || !meterFrames.size) return;
6078
+ const frames = [...meterFrames.values()];
6079
+ meterFrames.clear();
6080
+ meterBusy = true;
6081
+ try {
6082
+ meterWorker.postMessage({ frames }, frames.flatMap((frame2) => frame2.canvas ? [frame2.canvas] : []));
6083
+ } catch {
6084
+ failMeters();
6085
+ }
6086
+ });
6087
+ }
6088
+ function startMeters() {
6089
+ if (meterWorker) return meterWorker;
6090
+ try {
6091
+ meterUrl = URL.createObjectURL(new Blob([`(${renderMeters.toString()})()`], { type: "text/javascript" }));
6092
+ const worker = new Worker(meterUrl);
6093
+ meterWorker = worker;
6094
+ worker.onerror = () => {
6095
+ if (meterWorker === worker) failMeters();
6096
+ };
6097
+ worker.onmessage = ({ data }) => {
6098
+ if (meterWorker !== worker) return;
6099
+ if (data.ack) meterBusy = false;
6100
+ for (const id of data.failed ?? []) meterClients.get(id)?.dispose();
6101
+ for (const item of data.completed ?? []) meterClients.get(item.id)?.ready(item.revision);
6102
+ sendMeter();
6103
+ };
6104
+ return worker;
6105
+ } catch {
6106
+ stopMeters();
6107
+ return void 0;
6108
+ }
6109
+ }
6110
+ function waveCanvas(shape, initial) {
6111
+ if (typeof Worker === "undefined" || typeof OffscreenCanvas === "undefined" || typeof Path2D === "undefined" || typeof MutationObserver === "undefined" || typeof HTMLCanvasElement === "undefined" || !HTMLCanvasElement.prototype.transferControlToOffscreen) return null;
6112
+ const svg = shape.ownerSVGElement, root = svg?.parentElement;
6113
+ const axis = svg?.querySelector("line");
6114
+ if (!svg || !root || !axis) return null;
6115
+ const canvas = document.createElement("canvas");
6116
+ const worker = startMeters();
6117
+ if (!worker) return null;
6118
+ const id = ++meterSerial;
6119
+ canvas.className = "ad-level-meter-canvas";
6120
+ canvas.setAttribute("aria-hidden", "true");
6121
+ root.append(canvas);
6122
+ const ancestors = [];
6123
+ for (let node = root; node; node = node.parentElement) ancestors.push(node);
6124
+ let disposed = false, enabled = false, queued = false, ready = false, transferred = false, revision = 0;
6125
+ let current = initial, offset = 0, sent = "";
6126
+ const native = () => {
6127
+ root.removeAttribute("data-ad-canvas");
6128
+ canvas.hidden = true;
6129
+ shape.setAttribute("d", current);
6130
+ shape.setAttribute("transform", `translate(${offset} 0)`);
6131
+ };
6132
+ const draw = (d, shift) => {
6133
+ current = d;
6134
+ offset = shift;
6135
+ if (!enabled || disposed) return false;
6136
+ sendMeter({ id, revision, offset, ...sent !== d ? { d } : {} });
6137
+ sent = d;
6138
+ return ready;
6139
+ };
6140
+ function update() {
6141
+ if (disposed) return;
6142
+ const surface = getComputedStyle(svg), wave = getComputedStyle(shape), line2 = getComputedStyle(axis);
6143
+ const host = getComputedStyle(root);
6144
+ const gradient = svg.querySelector("linearGradient");
6145
+ const width = parseFloat(surface.width), height = parseFloat(surface.height);
6146
+ const unsupported = [surface.transform, surface.backgroundImage, wave.stroke, wave.filter, wave.clipPath, wave.maskImage, line2.strokeDasharray].some((value) => value !== "none") || line2.strokeLinecap !== "butt" || wave.fill.startsWith("url(") && (!gradient || wave.fill.match(/#([^"')]+)["']?\)$/)?.[1] !== gradient.id || ["gradientUnits", "gradientTransform", "x1", "x2", "y1", "y2"].some((name) => gradient.hasAttribute(name))) || [surface, host].some((style) => ["Top", "Right", "Bottom", "Left"].some((side) => parseFloat(style.getPropertyValue(`padding-${side.toLowerCase()}`)) > 0 || parseFloat(style.getPropertyValue(`border-${side.toLowerCase()}-width`)) > 0)) || [svg, ...ancestors].some((node) => node.getAnimations?.({ subtree: node === svg }).some((animation) => animation.playState === "running"));
6147
+ enabled = width > 0 && height > 0 && !unsupported;
6148
+ if (!enabled) {
6149
+ native();
6150
+ return;
6151
+ }
6152
+ const ratio = devicePixelRatio || 1;
6153
+ canvas.style.filter = surface.filter;
6154
+ canvas.style.opacity = surface.opacity;
6155
+ canvas.style.backgroundColor = surface.backgroundColor;
6156
+ const stops = wave.fill.startsWith("url(") ? [...svg.querySelectorAll("stop")].map((stop) => {
6157
+ const style = getComputedStyle(stop);
6158
+ return { at: stop.offset.baseVal, color: `color-mix(in srgb, ${style.stopColor} ${Number(style.stopOpacity) * 100}%, transparent)` };
6159
+ }) : [];
6160
+ try {
6161
+ const surface2 = transferred ? void 0 : canvas.transferControlToOffscreen();
6162
+ transferred = true;
6163
+ ready = false;
6164
+ revision++;
6165
+ native();
6166
+ sent = current;
6167
+ sendMeter({ id, revision, d: current, offset, ...surface2 ? { canvas: surface2 } : {}, style: {
6168
+ width: Math.ceil(width * ratio),
6169
+ height: Math.ceil(height * ratio),
6170
+ stroke: line2.stroke,
6171
+ lineWidth: parseFloat(line2.strokeWidth),
6172
+ lineAlpha: Number(line2.opacity) * Number(line2.strokeOpacity),
6173
+ fillAlpha: Number(wave.opacity) * Number(wave.fillOpacity),
6174
+ fill: wave.fill,
6175
+ stops
6176
+ } });
6177
+ } catch {
6178
+ dispose();
6179
+ }
6180
+ }
6181
+ function queue() {
6182
+ if (queued || disposed) return;
6183
+ queued = true;
6184
+ queueMicrotask(() => {
6185
+ queued = false;
6186
+ if (!disposed) update();
6187
+ });
6188
+ }
6189
+ const resized = createResizeObserver(queue);
6190
+ resized.observe(svg);
6191
+ const styles = new MutationObserver(queue);
6192
+ for (const node of ancestors) styles.observe(node, { attributes: true, attributeFilter: ["style", "class", "data-ad-theme", "data-ad-color-scheme"] });
6193
+ styles.observe(svg, { subtree: true, childList: true, attributes: true, attributeFilter: ["style", "class", "fill", "stroke", "stop-color", "stop-opacity", "offset"] });
6194
+ styles.observe(document.head, { subtree: true, childList: true, characterData: true });
6195
+ const transition = (event) => {
6196
+ if (event.target instanceof Element && (event.target.contains(svg) || svg.contains(event.target))) queue();
6197
+ };
6198
+ const events = ["transitionrun", "transitionend", "transitioncancel", "animationstart", "animationend", "animationcancel"];
6199
+ for (const event of events) document.addEventListener(event, transition);
6200
+ window.addEventListener("resize", queue);
6201
+ canvas.addEventListener("contextlost", dispose);
6202
+ function dispose() {
6203
+ if (disposed) return;
6204
+ disposed = true;
6205
+ enabled = false;
6206
+ resized.disconnect();
6207
+ styles.disconnect();
6208
+ window.removeEventListener("resize", queue);
6209
+ canvas.removeEventListener("contextlost", dispose);
6210
+ for (const event of events) document.removeEventListener(event, transition);
6211
+ meterFrames.delete(id);
6212
+ meterClients.delete(id);
6213
+ if (meterWorker === worker) worker?.postMessage({ remove: id });
6214
+ if (!meterClients.size) stopMeters();
6215
+ native();
6216
+ canvas.remove();
6217
+ }
6218
+ meterClients.set(id, { dispose, ready(next) {
6219
+ if (disposed || !enabled || next !== revision || ready) return;
6220
+ ready = true;
6221
+ canvas.hidden = false;
6222
+ root.setAttribute("data-ad-canvas", "");
6223
+ } });
6224
+ update();
6225
+ return { draw, dispose };
6226
+ }
5985
6227
  var loudness = (analyser, buffer) => {
5986
6228
  analyser.getFloatTimeDomainData(buffer);
5987
6229
  let sum = 0;
@@ -6002,15 +6244,16 @@ function LevelMeter({
6002
6244
  const target = useRef27(0);
6003
6245
  const wake = useRef27(() => void 0);
6004
6246
  const level = clamp(value) / 100;
6005
- useEffect13(() => {
6247
+ useLayoutEffect5(() => {
6006
6248
  target.current = active ? level : 0;
6007
6249
  if (active && level > 0) wake.current();
6008
6250
  }, [active, level]);
6009
6251
  useEffect13(() => {
6010
6252
  const shape = path.current;
6011
6253
  if (!shape) return;
6012
- const samples = Array.from({ length: SAMPLES + 1 }, () => 0);
6013
- shape.setAttribute("d", wavePath2(samples));
6254
+ const samples = Array.from({ length: SAMPLES + 1 }, () => waveSample(0));
6255
+ let drawnPath = wavePath2(samples);
6256
+ shape.setAttribute("d", drawnPath);
6014
6257
  wake.current = () => void 0;
6015
6258
  if (!active) return;
6016
6259
  let context;
@@ -6024,6 +6267,8 @@ function LevelMeter({
6024
6267
  source.connect(analyser);
6025
6268
  }
6026
6269
  const buffer = new Float32Array(analyser?.fftSize ?? 0);
6270
+ const painter = waveCanvas(shape, drawnPath);
6271
+ let nativePath = drawnPath;
6027
6272
  let envelope = 0;
6028
6273
  let carry = 0;
6029
6274
  let last = performance.now();
@@ -6032,24 +6277,32 @@ function LevelMeter({
6032
6277
  carry += Math.min(250, now - last);
6033
6278
  last = now;
6034
6279
  const input = analyser ? loudness(analyser, buffer) : target.current;
6280
+ const sampled = carry >= SAMPLE_MS;
6035
6281
  while (carry >= SAMPLE_MS) {
6036
6282
  carry -= SAMPLE_MS;
6037
6283
  envelope += (input - envelope) * (input > envelope ? 0.3 : 0.11);
6038
6284
  if (envelope < 1e-3) envelope = 0;
6039
6285
  samples.shift();
6040
- samples.push(envelope);
6041
- shape.setAttribute("d", wavePath2(samples));
6286
+ samples.push(waveSample(envelope));
6287
+ }
6288
+ if (sampled) {
6289
+ const nextPath = wavePath2(samples);
6290
+ drawnPath = nextPath;
6042
6291
  }
6043
- if (!analyser && input === 0 && envelope === 0 && samples.every((sample) => sample === 0)) {
6044
- shape.setAttribute("transform", "translate(0 0)");
6292
+ const quiet = !analyser && input === 0 && envelope === 0 && samples.every((sample) => sample.level === 0);
6293
+ const shift = quiet ? 0 : Number((-(carry / SAMPLE_MS) * STEP).toFixed(3));
6294
+ if (!painter?.draw(drawnPath, shift)) {
6295
+ if (nativePath !== drawnPath) {
6296
+ shape.setAttribute("d", drawnPath);
6297
+ nativePath = drawnPath;
6298
+ }
6299
+ shape.setAttribute("transform", `translate(${shift} 0)`);
6300
+ }
6301
+ if (quiet) {
6045
6302
  stop?.();
6046
6303
  stop = void 0;
6047
6304
  return;
6048
6305
  }
6049
- shape.setAttribute(
6050
- "transform",
6051
- `translate(${(-(carry / SAMPLE_MS) * STEP).toFixed(3)} 0)`
6052
- );
6053
6306
  };
6054
6307
  wake.current = () => {
6055
6308
  if (stop) return;
@@ -6061,10 +6314,35 @@ function LevelMeter({
6061
6314
  return () => {
6062
6315
  wake.current = () => void 0;
6063
6316
  stop?.();
6317
+ painter?.dispose();
6064
6318
  source?.disconnect();
6065
6319
  void context?.close();
6066
6320
  };
6067
6321
  }, [active, stream]);
6322
+ const waveform = useMemo5(() => /* @__PURE__ */ jsxs39(
6323
+ "svg",
6324
+ {
6325
+ viewBox: `0 0 ${WIDTH} ${HEIGHT}`,
6326
+ preserveAspectRatio: "none",
6327
+ "aria-hidden": "true",
6328
+ children: [
6329
+ /* @__PURE__ */ jsx48("defs", { children: /* @__PURE__ */ jsxs39("linearGradient", { id: `${id}-wave`, children: [
6330
+ /* @__PURE__ */ jsx48("stop", { stopColor: "var(--ad-primary-700)" }),
6331
+ /* @__PURE__ */ jsx48("stop", { offset: "0.52", stopColor: "var(--ad-red)" }),
6332
+ /* @__PURE__ */ jsx48("stop", { offset: "1", stopColor: "var(--ad-secondary-100)" })
6333
+ ] }) }),
6334
+ /* @__PURE__ */ jsx48("line", { className: "ad-level-meter-axis", x2: WIDTH, y1: MID2, y2: MID2 }),
6335
+ /* @__PURE__ */ jsx48(
6336
+ "path",
6337
+ {
6338
+ ref: path,
6339
+ className: "ad-level-meter-wave",
6340
+ fill: `url(#${id}-wave)`
6341
+ }
6342
+ )
6343
+ ]
6344
+ }
6345
+ ), [id]);
6068
6346
  return /* @__PURE__ */ jsx48(
6069
6347
  "div",
6070
6348
  {
@@ -6076,30 +6354,7 @@ function LevelMeter({
6076
6354
  "aria-valuenow": stream ? void 0 : Math.round(active ? level * 100 : 0),
6077
6355
  "data-active": active,
6078
6356
  "data-compact": compact || void 0,
6079
- children: /* @__PURE__ */ jsxs39(
6080
- "svg",
6081
- {
6082
- viewBox: `0 0 ${WIDTH} ${HEIGHT}`,
6083
- preserveAspectRatio: "none",
6084
- "aria-hidden": "true",
6085
- children: [
6086
- /* @__PURE__ */ jsx48("defs", { children: /* @__PURE__ */ jsxs39("linearGradient", { id: `${id}-wave`, children: [
6087
- /* @__PURE__ */ jsx48("stop", { stopColor: "var(--ad-primary-700)" }),
6088
- /* @__PURE__ */ jsx48("stop", { offset: "0.52", stopColor: "var(--ad-red)" }),
6089
- /* @__PURE__ */ jsx48("stop", { offset: "1", stopColor: "var(--ad-secondary-100)" })
6090
- ] }) }),
6091
- /* @__PURE__ */ jsx48("line", { className: "ad-level-meter-axis", x2: WIDTH, y1: MID2, y2: MID2 }),
6092
- /* @__PURE__ */ jsx48(
6093
- "path",
6094
- {
6095
- ref: path,
6096
- className: "ad-level-meter-wave",
6097
- fill: `url(#${id}-wave)`
6098
- }
6099
- )
6100
- ]
6101
- }
6102
- )
6357
+ children: waveform
6103
6358
  }
6104
6359
  );
6105
6360
  }
@@ -6147,7 +6402,7 @@ var KaraokeLyrics = ({
6147
6402
  );
6148
6403
 
6149
6404
  // src/components/media/MelodyRoll/MelodyRoll.tsx
6150
- import { useEffect as useEffect14, useLayoutEffect as useLayoutEffect5, useMemo as useMemo5, useRef as useRef28, useState as useState13 } from "react";
6405
+ import { useEffect as useEffect14, useLayoutEffect as useLayoutEffect6, useMemo as useMemo6, useRef as useRef28, useState as useState13 } from "react";
6151
6406
 
6152
6407
  // src/components/media/MelodyRoll/melody-glow.js
6153
6408
  function renderGlow() {
@@ -6327,6 +6582,7 @@ function renderGlow() {
6327
6582
  scene.noteStates = kept;
6328
6583
  scene.keyStates ??= /* @__PURE__ */ new Map();
6329
6584
  const keyStates = /* @__PURE__ */ new Map();
6585
+ let keysUntil = 0;
6330
6586
  for (const key of style.keys) {
6331
6587
  const previous = scene.keyStates.get(key.midi);
6332
6588
  const active = state.pitch !== void 0 && key.midi === Math.round(state.pitch);
@@ -6334,6 +6590,7 @@ function renderGlow() {
6334
6590
  const pressed = scene.pressed?.includes(key.midi);
6335
6591
  const variant = hit ? "hit" : active ? "active" : pressed ? "pressed" : "base";
6336
6592
  const target = key[variant];
6593
+ if (previous?.variant !== variant) scene.keysDirty = true;
6337
6594
  const item = {
6338
6595
  key,
6339
6596
  target,
@@ -6346,11 +6603,12 @@ function renderGlow() {
6346
6603
  hitStart: hit && !previous?.hit ? previous ? now : scene.keyTimes?.[key.midi] ?? now : previous?.hitStart ?? now
6347
6604
  };
6348
6605
  keyStates.set(key.midi, item);
6349
- until = Math.max(until, ...["color", "shadows", "shift"].map((name) => item[name].start + item[name].duration), hit && key.hitDuration > 0 ? item.hitStart + key.hitDuration * key.hitIterations : 0);
6606
+ keysUntil = Math.max(keysUntil, ...["color", "shadows", "shift"].map((name) => item[name].start + item[name].duration), hit && key.hitDuration > 0 ? item.hitStart + key.hitDuration * key.hitIterations : 0);
6350
6607
  }
6351
6608
  scene.keyStates = keyStates;
6352
6609
  scene.noteTimes = scene.keyTimes = void 0;
6353
- scene.foregroundUntil = until;
6610
+ scene.keysUntil = keysUntil;
6611
+ scene.foregroundUntil = Math.max(until, keysUntil);
6354
6612
  }
6355
6613
  function drawForeground(scene, now) {
6356
6614
  if (!scene.foreground) return;
@@ -6434,6 +6692,10 @@ function renderGlow() {
6434
6692
  ctx.drawImage(painted, x - padding / dpr, top + height / 2 + (y - padding / dpr - top - height / 2) * scale, painted.width / dpr, painted.height / dpr * scale);
6435
6693
  ctx.restore();
6436
6694
  }
6695
+ const keysAnimating = now < scene.keysUntil;
6696
+ if (!scene.keysDirty && !scene.keysAnimating && !keysAnimating) return;
6697
+ scene.keysDirty = false;
6698
+ scene.keysAnimating = keysAnimating;
6437
6699
  keyCtx.clearRect(0, 0, style.keyWidth, style.keyHeight);
6438
6700
  scene.keySprites ??= /* @__PURE__ */ new Map();
6439
6701
  for (const item of scene.keyStates.values()) {
@@ -6489,7 +6751,11 @@ function renderGlow() {
6489
6751
  }
6490
6752
  function drawVoice(scene, now) {
6491
6753
  const { voiceCtx: ctx, laneWidth, laneHeight, state, voiceStyle: style, dpr } = scene;
6754
+ const active = state.pitch !== void 0 || state.beat > 0;
6755
+ if (!active && !scene.voicePainted) return;
6756
+ scene.voicePainted = active;
6492
6757
  ctx.clearRect(0, 0, laneWidth, laneHeight);
6758
+ if (!active) return;
6493
6759
  ctx.save();
6494
6760
  const head = state.lead * laneWidth - scene.headWidth / 2;
6495
6761
  const offset = laneWidth + 200;
@@ -6541,7 +6807,11 @@ function renderGlow() {
6541
6807
  }
6542
6808
  function drawTrail(scene) {
6543
6809
  const { trailCtx: ctx, laneWidth, laneHeight, state, trailStyle } = scene;
6810
+ const active = scene.trail.some((point) => point.pitch !== void 0);
6811
+ if (!active && !scene.trailPainted) return;
6812
+ scene.trailPainted = active;
6544
6813
  ctx.clearRect(0, 0, laneWidth, laneHeight);
6814
+ if (!active) return;
6545
6815
  const paths = [];
6546
6816
  let path = null;
6547
6817
  for (const point of scene.trail) {
@@ -6568,8 +6838,9 @@ function renderGlow() {
6568
6838
  }
6569
6839
  function draw(scene, now) {
6570
6840
  const { ctx, width, height, pad, radius: radius2, blur, color: color3, dpr } = scene;
6571
- ctx.clearRect(0, 0, width + pad * 2, height + pad * 2);
6572
6841
  const amount = value(scene, now);
6842
+ if (amount > 0 || scene.glowPainted) ctx.clearRect(0, 0, width + pad * 2, height + pad * 2);
6843
+ scene.glowPainted = amount > 0;
6573
6844
  if (amount > 0) {
6574
6845
  ctx.save();
6575
6846
  ctx.beginPath();
@@ -6661,6 +6932,7 @@ function renderGlow() {
6661
6932
  scene.keyCtx = scene.keyCanvas.getContext("2d");
6662
6933
  scene.noteCtx.setTransform(data.dpr, 0, 0, data.dpr, 0, 0);
6663
6934
  scene.keyCtx.setTransform(data.dpr, 0, 0, data.dpr, 0, 0);
6935
+ scene.keysDirty = true;
6664
6936
  scene.sprites?.clear();
6665
6937
  scene.keySprites?.clear();
6666
6938
  scene.filteredSprites?.clear();
@@ -6699,6 +6971,7 @@ function renderGlow() {
6699
6971
  retargetVoice(scene, now, previous);
6700
6972
  retargetForeground(scene, now);
6701
6973
  } else if (scene && data.type === "keys") {
6974
+ scene.keysDirty = true;
6702
6975
  scene.pressed = data.pressed;
6703
6976
  scene.hovered = data.hovered;
6704
6977
  retargetForeground(scene, now);
@@ -7236,7 +7509,7 @@ var MelodyRoll = ({
7236
7509
  const glow3 = useRef28(null);
7237
7510
  const [glowReady, setGlowReady] = useState13(false);
7238
7511
  const [span, setSpan] = useControllable(windowProp, defaultWindow, onWindowChange);
7239
- const indexed = useMemo5(() => {
7512
+ const indexed = useMemo6(() => {
7240
7513
  let low2 = 60, high2 = 64;
7241
7514
  const blocks = [];
7242
7515
  for (let i = 0; i < notes.length; i += 128) {
@@ -7260,7 +7533,7 @@ var MelodyRoll = ({
7260
7533
  const x = (seconds) => (seconds - from) / span * 100;
7261
7534
  const rangeStart = glowReady ? origin : from;
7262
7535
  const rangeEnd = glowReady ? origin + span * 2 : from + span;
7263
- const visible = useMemo5(() => indexed.blocks.filter((block) => block.end >= rangeStart && block.start <= rangeEnd).flatMap((block) => block.notes.filter((note) => note.end >= rangeStart && note.start <= rangeEnd)), [indexed, rangeStart, rangeEnd]);
7536
+ const visible = useMemo6(() => indexed.blocks.filter((block) => block.end >= rangeStart && block.start <= rangeEnd).flatMap((block) => block.notes.filter((note) => note.end >= rangeStart && note.start <= rangeEnd)), [indexed, rangeStart, rangeEnd]);
7264
7537
  const phases = glowReady ? "" : visible.map((note) => note.end < from || note.start > from + span ? "x" : position >= note.start && position <= note.end ? "n" : position > note.end ? "p" : "-").join("");
7265
7538
  const hits = hitIds?.size ? visible.map((note) => hitIds.has(note.id) ? "h" : "-").join("") : "";
7266
7539
  const glowState = { beat: clamp(beat, 0, 1), pitch: livePitch, high, rows, lead, accuracy: clamp(accuracy, 0, 1), level: clamp(level, 0, 1), hit, position, span, notes: visible, hits, showLyrics };
@@ -7272,11 +7545,11 @@ var MelodyRoll = ({
7272
7545
  glow3.current = null;
7273
7546
  };
7274
7547
  }, []);
7275
- useLayoutEffect5(() => {
7548
+ useLayoutEffect6(() => {
7276
7549
  glow3.current?.update(glowState);
7277
7550
  }, [beat, livePitch, high, rows, lead, accuracy, level, hit, position, span, visible, hits, showLyrics]);
7278
7551
  const nativeHits = glowReady ? "" : hits;
7279
- const noteElements = useMemo5(() => glowReady ? void 0 : visible.map((note, i) => {
7552
+ const noteElements = useMemo6(() => glowReady ? void 0 : visible.map((note, i) => {
7280
7553
  const hidden = phases[i] === "x";
7281
7554
  return /* @__PURE__ */ jsx50(
7282
7555
  "span",
@@ -7316,7 +7589,7 @@ var MelodyRoll = ({
7316
7589
  setSpan(clamp(span * (event.deltaY > 0 ? 1.12 : 1 / 1.12), 2, 20));
7317
7590
  };
7318
7591
  const nativeState = glowReady ? null : glowState;
7319
- const content = useMemo5(() => /* @__PURE__ */ jsxs41("div", { className: "ad-melody-roll-content", children: [
7592
+ const content = useMemo6(() => /* @__PURE__ */ jsxs41("div", { className: "ad-melody-roll-content", children: [
7320
7593
  /* @__PURE__ */ jsx50(
7321
7594
  PianoKeyboard,
7322
7595
  {
@@ -7374,7 +7647,7 @@ var MelodyRoll = ({
7374
7647
  import { useRef as useRef30 } from "react";
7375
7648
 
7376
7649
  // src/components/layout/FloatingPanel/useFloatingPanel.ts
7377
- import { useCallback as useCallback2, useEffect as useEffect15, useLayoutEffect as useLayoutEffect6, useRef as useRef29, useState as useState14 } from "react";
7650
+ import { useCallback as useCallback2, useEffect as useEffect15, useLayoutEffect as useLayoutEffect7, useRef as useRef29, useState as useState14 } from "react";
7378
7651
  var resizeEdges = ["n", "s", "e", "w", "ne", "nw", "se", "sw"];
7379
7652
  var threshold = 3;
7380
7653
  var panelControls = "button, a, input, select, textarea, [role=slider], [role=button], [role=switch], [role=listbox], [contenteditable=true], .ad-rotary-knob";
@@ -7386,7 +7659,7 @@ var useFloatingPanel = (frameRef, options = {}) => {
7386
7659
  const latest = useRef29(options);
7387
7660
  latest.current = options;
7388
7661
  const saved = options.layout;
7389
- useLayoutEffect6(() => {
7662
+ useLayoutEffect7(() => {
7390
7663
  drag.current = null;
7391
7664
  setLive(null);
7392
7665
  }, [saved?.left, saved?.top, saved?.width, saved?.height]);
@@ -7548,7 +7821,7 @@ var FloatingPanel = ({ layout: saved, onLayoutChange, defaultSize, limits, onDra
7548
7821
  };
7549
7822
 
7550
7823
  // src/components/media/Sparkline/Sparkline.tsx
7551
- import { useMemo as useMemo6, useRef as useRef31 } from "react";
7824
+ import { useMemo as useMemo7, useRef as useRef31 } from "react";
7552
7825
  import { Fragment as Fragment5, jsx as jsx52, jsxs as jsxs43 } from "react/jsx-runtime";
7553
7826
  var DEMO3 = [12, 23, 17, 31, 43, 24, 28, 20, 41, 29, 51, 34, 38, 22, 31, 16, 23];
7554
7827
  var Sparkline = (p) => {
@@ -7556,7 +7829,7 @@ var Sparkline = (p) => {
7556
7829
  const id = useSvgId();
7557
7830
  const ref = useRef31(null);
7558
7831
  usePauseOffscreen(ref);
7559
- const { line: line2, lastX, lastY } = useMemo6(() => {
7832
+ const { line: line2, lastX, lastY } = useMemo7(() => {
7560
7833
  if (!values.length) return { line: "", lastX: 0, lastY: 0 };
7561
7834
  let min = p.fit ? Infinity : 0;
7562
7835
  let max = p.fit ? -Infinity : 1;
@@ -7628,7 +7901,7 @@ import { useRef as useRef32 } from "react";
7628
7901
  import { jsx as jsx53, jsxs as jsxs44 } from "react/jsx-runtime";
7629
7902
  var POINTS = 23;
7630
7903
  var STEP2 = 65 / (POINTS - 1);
7631
- var X = Array.from({ length: POINTS }, (_, k) => k * STEP2 / 65 * 600);
7904
+ var X2 = Array.from({ length: POINTS }, (_, k) => k * STEP2 / 65 * 600);
7632
7905
  var strand = (j, t) => {
7633
7906
  const ys = Array.from({ length: POINTS + 2 }, (_, k) => {
7634
7907
  const i = (k - 1) * STEP2;
@@ -7637,7 +7910,7 @@ var strand = (j, t) => {
7637
7910
  let d = `M0 ${ys[1].toFixed(2)}`;
7638
7911
  for (let k = 1; k < POINTS; k += 1) {
7639
7912
  const y0 = ys[k - 1], y1 = ys[k], y2 = ys[k + 1], y3 = ys[k + 2];
7640
- const x1 = X[k - 1], x2 = X[k], third = (x2 - x1) / 3;
7913
+ const x1 = X2[k - 1], x2 = X2[k], third = (x2 - x1) / 3;
7641
7914
  d += `C${(x1 + third).toFixed(2)} ${(y1 + (y2 - y0) / 6).toFixed(2)} ${(x2 - third).toFixed(2)} ${(y2 - (y3 - y1) / 6).toFixed(2)} ${x2.toFixed(2)} ${y2.toFixed(2)}`;
7642
7915
  }
7643
7916
  return d;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neo-ui-kit/react",
3
- "version": "1.3.12",
3
+ "version": "1.3.14",
4
4
  "description": "NEO UI — React component library",
5
5
  "type": "module",
6
6
  "license": "MIT",