@neo-ui-kit/react 1.3.11 → 1.3.13

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.
Files changed (3) hide show
  1. package/README.md +10 -1
  2. package/dist/index.js +100 -6
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -4,6 +4,15 @@ 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.13
8
+
9
+ 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.
10
+
11
+ ## 1.3.12
12
+
13
+ - 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.
14
+ - Live-pitch measurements reached 59.7/59.0 main-thread FPS at CPU ×8/×16 in the library fixture. A karaoke-style animated native shadow still limits ×16 page updates; stable 60 FPS is not established for that scenario. See the [performance report](https://github.com/Pro100Dimka/adui/blob/main/evidence/performance-report.md).
15
+
7
16
  ## 1.3.11
8
17
 
9
18
  LevelMeter resumes its waveform when a signal arrives after silence, including when the first animation frame precedes the next sample.
@@ -42,7 +51,7 @@ import "@neo-ui-kit/react/styles.css";
42
51
  To test repository changes before publishing, run `npm run package` from the repository root and install the resulting archive in a separate application:
43
52
 
44
53
  ```bash
45
- npm install /path/to/adui/release/neo-ui-kit-react-1.3.11.tgz
54
+ npm install /path/to/adui/release/neo-ui-kit-react-1.3.13.tgz
46
55
  ```
47
56
 
48
57
  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-пакет).
package/dist/index.js CHANGED
@@ -6327,6 +6327,7 @@ function renderGlow() {
6327
6327
  scene.noteStates = kept;
6328
6328
  scene.keyStates ??= /* @__PURE__ */ new Map();
6329
6329
  const keyStates = /* @__PURE__ */ new Map();
6330
+ let keysUntil = 0;
6330
6331
  for (const key of style.keys) {
6331
6332
  const previous = scene.keyStates.get(key.midi);
6332
6333
  const active = state.pitch !== void 0 && key.midi === Math.round(state.pitch);
@@ -6334,6 +6335,7 @@ function renderGlow() {
6334
6335
  const pressed = scene.pressed?.includes(key.midi);
6335
6336
  const variant = hit ? "hit" : active ? "active" : pressed ? "pressed" : "base";
6336
6337
  const target = key[variant];
6338
+ if (previous?.variant !== variant) scene.keysDirty = true;
6337
6339
  const item = {
6338
6340
  key,
6339
6341
  target,
@@ -6346,11 +6348,12 @@ function renderGlow() {
6346
6348
  hitStart: hit && !previous?.hit ? previous ? now : scene.keyTimes?.[key.midi] ?? now : previous?.hitStart ?? now
6347
6349
  };
6348
6350
  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);
6351
+ 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
6352
  }
6351
6353
  scene.keyStates = keyStates;
6352
6354
  scene.noteTimes = scene.keyTimes = void 0;
6353
- scene.foregroundUntil = until;
6355
+ scene.keysUntil = keysUntil;
6356
+ scene.foregroundUntil = Math.max(until, keysUntil);
6354
6357
  }
6355
6358
  function drawForeground(scene, now) {
6356
6359
  if (!scene.foreground) return;
@@ -6434,6 +6437,10 @@ function renderGlow() {
6434
6437
  ctx.drawImage(painted, x - padding / dpr, top + height / 2 + (y - padding / dpr - top - height / 2) * scale, painted.width / dpr, painted.height / dpr * scale);
6435
6438
  ctx.restore();
6436
6439
  }
6440
+ const keysAnimating = now < scene.keysUntil;
6441
+ if (!scene.keysDirty && !scene.keysAnimating && !keysAnimating) return;
6442
+ scene.keysDirty = false;
6443
+ scene.keysAnimating = keysAnimating;
6437
6444
  keyCtx.clearRect(0, 0, style.keyWidth, style.keyHeight);
6438
6445
  scene.keySprites ??= /* @__PURE__ */ new Map();
6439
6446
  for (const item of scene.keyStates.values()) {
@@ -6489,7 +6496,11 @@ function renderGlow() {
6489
6496
  }
6490
6497
  function drawVoice(scene, now) {
6491
6498
  const { voiceCtx: ctx, laneWidth, laneHeight, state, voiceStyle: style, dpr } = scene;
6499
+ const active = state.pitch !== void 0 || state.beat > 0;
6500
+ if (!active && !scene.voicePainted) return;
6501
+ scene.voicePainted = active;
6492
6502
  ctx.clearRect(0, 0, laneWidth, laneHeight);
6503
+ if (!active) return;
6493
6504
  ctx.save();
6494
6505
  const head = state.lead * laneWidth - scene.headWidth / 2;
6495
6506
  const offset = laneWidth + 200;
@@ -6541,7 +6552,11 @@ function renderGlow() {
6541
6552
  }
6542
6553
  function drawTrail(scene) {
6543
6554
  const { trailCtx: ctx, laneWidth, laneHeight, state, trailStyle } = scene;
6555
+ const active = scene.trail.some((point) => point.pitch !== void 0);
6556
+ if (!active && !scene.trailPainted) return;
6557
+ scene.trailPainted = active;
6544
6558
  ctx.clearRect(0, 0, laneWidth, laneHeight);
6559
+ if (!active) return;
6545
6560
  const paths = [];
6546
6561
  let path = null;
6547
6562
  for (const point of scene.trail) {
@@ -6568,8 +6583,9 @@ function renderGlow() {
6568
6583
  }
6569
6584
  function draw(scene, now) {
6570
6585
  const { ctx, width, height, pad, radius: radius2, blur, color: color3, dpr } = scene;
6571
- ctx.clearRect(0, 0, width + pad * 2, height + pad * 2);
6572
6586
  const amount = value(scene, now);
6587
+ if (amount > 0 || scene.glowPainted) ctx.clearRect(0, 0, width + pad * 2, height + pad * 2);
6588
+ scene.glowPainted = amount > 0;
6573
6589
  if (amount > 0) {
6574
6590
  ctx.save();
6575
6591
  ctx.beginPath();
@@ -6661,6 +6677,7 @@ function renderGlow() {
6661
6677
  scene.keyCtx = scene.keyCanvas.getContext("2d");
6662
6678
  scene.noteCtx.setTransform(data.dpr, 0, 0, data.dpr, 0, 0);
6663
6679
  scene.keyCtx.setTransform(data.dpr, 0, 0, data.dpr, 0, 0);
6680
+ scene.keysDirty = true;
6664
6681
  scene.sprites?.clear();
6665
6682
  scene.keySprites?.clear();
6666
6683
  scene.filteredSprites?.clear();
@@ -6699,6 +6716,7 @@ function renderGlow() {
6699
6716
  retargetVoice(scene, now, previous);
6700
6717
  retargetForeground(scene, now);
6701
6718
  } else if (scene && data.type === "keys") {
6719
+ scene.keysDirty = true;
6702
6720
  scene.pressed = data.pressed;
6703
6721
  scene.hovered = data.hovered;
6704
6722
  retargetForeground(scene, now);
@@ -6757,18 +6775,88 @@ function createMelodyGlow(root, initialState, onReady) {
6757
6775
  for (const layer of [canvas, voiceCanvas, trailCanvas, noteCanvas, keyCanvas]) layer.setAttribute("aria-hidden", "true");
6758
6776
  let state = initialState, disposed = false, transferred = false, inView = true, revision = 0, signature = "";
6759
6777
  const visible = () => inView && !document.hidden && !root.closest("[data-ad-paused], [data-ad-offscreen]");
6760
- const visibility = () => worker.postMessage({ type: "visible", id, visible: visible() });
6778
+ let lastVisible;
6779
+ const visibility = () => {
6780
+ const next = visible();
6781
+ if (next === lastVisible) return;
6782
+ lastVisible = next;
6783
+ worker.postMessage({ type: "visible", id, visible: next });
6784
+ };
6761
6785
  const resize = new ResizeObserver(configure);
6762
6786
  const intersection = new IntersectionObserver(([entry]) => {
6763
6787
  inView = entry.isIntersecting;
6764
6788
  visibility();
6765
6789
  });
6790
+ let variableUses, variableSheets = [], nativeShadowChanged = false;
6791
+ const readVariableUses = () => {
6792
+ const uses = /* @__PURE__ */ new Map();
6793
+ const declarations = (style, selector) => {
6794
+ for (const property of style) for (const [, variable] of style.getPropertyValue(property).matchAll(/var\(\s*(--[\w-]+)/g)) {
6795
+ if (!uses.has(variable)) uses.set(variable, []);
6796
+ uses.get(variable).push({ property, selector });
6797
+ }
6798
+ };
6799
+ const visit = (rules) => {
6800
+ for (const rule of rules) {
6801
+ if (rule.selectorText?.includes("[style") || rule.conditionText?.includes("style(")) throw new Error("Style-dependent selectors require full measurement");
6802
+ if (rule.style) declarations(rule.style, rule.selectorText);
6803
+ if (rule.styleSheet) visit(rule.styleSheet.cssRules);
6804
+ else if (rule.cssRules) visit(rule.cssRules);
6805
+ }
6806
+ };
6807
+ variableSheets = [...document.styleSheets, ...document.adoptedStyleSheets ?? []];
6808
+ for (const sheet of variableSheets) visit(sheet.cssRules);
6809
+ for (let node = root; node; node = node.parentElement) declarations(node.style, node === root ? null : void 0);
6810
+ return uses;
6811
+ };
6812
+ const affectsRendering = (records) => {
6813
+ const changed = /* @__PURE__ */ new Set();
6814
+ for (const record of records) {
6815
+ if (["data-ad-paused", "data-ad-offscreen"].includes(record.attributeName)) continue;
6816
+ if (record.attributeName !== "style") return true;
6817
+ const old = document.createElement("div").style;
6818
+ old.cssText = record.oldValue ?? "";
6819
+ const current = record.target.style;
6820
+ for (const property of /* @__PURE__ */ new Set([...old, ...current])) {
6821
+ if (old.getPropertyValue(property) === current.getPropertyValue(property) && old.getPropertyPriority(property) === current.getPropertyPriority(property)) continue;
6822
+ if (["opacity", "transform", "translate", "rotate", "scale", "z-index"].includes(property)) continue;
6823
+ if (!property.startsWith("--")) return true;
6824
+ changed.add(property);
6825
+ if (current.getPropertyValue(property).includes("var(")) variableUses = void 0;
6826
+ }
6827
+ }
6828
+ if (!changed.size) return false;
6829
+ try {
6830
+ const sheets = [...document.styleSheets, ...document.adoptedStyleSheets ?? []];
6831
+ if (sheets.length !== variableSheets.length || sheets.some((sheet, i) => sheet !== variableSheets[i])) variableUses = void 0;
6832
+ variableUses ??= readVariableUses();
6833
+ for (const variable of changed) for (const use of variableUses.get(variable) ?? []) {
6834
+ if (use.property.startsWith("--")) {
6835
+ changed.add(use.property);
6836
+ continue;
6837
+ }
6838
+ if (use.property === "box-shadow" && state.beat === 0 && (use.selector === null || use.selector && !use.selector.includes(",") && root.matches(use.selector) && !root.querySelector(use.selector))) {
6839
+ nativeShadowChanged = true;
6840
+ continue;
6841
+ }
6842
+ return true;
6843
+ }
6844
+ return false;
6845
+ } catch {
6846
+ return true;
6847
+ }
6848
+ };
6766
6849
  const tokens = new MutationObserver((records) => {
6767
6850
  visibility();
6768
- if (records.some((record) => !["data-ad-paused", "data-ad-offscreen"].includes(record.attributeName))) configure();
6851
+ if (affectsRendering(records)) configure();
6769
6852
  });
6853
+ const stylesheets = new MutationObserver((records) => {
6854
+ variableUses = void 0;
6855
+ if (records.some((record) => record.target.nodeName === "STYLE" || record.target.parentElement?.nodeName === "STYLE" || [...record.addedNodes, ...record.removedNodes].some((node) => node.nodeType === 1 && node.matches('style,link[rel="stylesheet"]')))) configure();
6856
+ });
6857
+ if (document.head) stylesheets.observe(document.head, { childList: true, subtree: true, characterData: true });
6770
6858
  for (let node = root; node; node = node.parentElement)
6771
- tokens.observe(node, { attributes: true, attributeFilter: ["style", "class", "data-ad-theme", "data-ad-color-scheme", "data-ad-motion", "data-ad-paused", "data-ad-offscreen"] });
6859
+ tokens.observe(node, { attributes: true, attributeOldValue: true, attributeFilter: ["style", "class", "data-ad-theme", "data-ad-color-scheme", "data-ad-motion", "data-ad-paused", "data-ad-offscreen"] });
6772
6860
  resize.observe(root);
6773
6861
  resize.observe(lane);
6774
6862
  intersection.observe(root);
@@ -6866,6 +6954,7 @@ function createMelodyGlow(root, initialState, onReady) {
6866
6954
  };
6867
6955
  function configure() {
6868
6956
  if (disposed || !root.clientWidth || !root.clientHeight) return;
6957
+ variableUses = void 0;
6869
6958
  const temporary = [];
6870
6959
  try {
6871
6960
  const probe = document.createElement("div");
@@ -7074,6 +7163,7 @@ function createMelodyGlow(root, initialState, onReady) {
7074
7163
  resize.disconnect();
7075
7164
  intersection.disconnect();
7076
7165
  tokens.disconnect();
7166
+ stylesheets.disconnect();
7077
7167
  keyObserver.disconnect();
7078
7168
  keyboard.removeEventListener("pointerover", keysChanged);
7079
7169
  keyboard.removeEventListener("pointerout", keysChanged);
@@ -7104,6 +7194,10 @@ function createMelodyGlow(root, initialState, onReady) {
7104
7194
  if (next.notes === state.notes) delete sent.notes;
7105
7195
  if (next.hits === state.hits) delete sent.hits;
7106
7196
  state = next;
7197
+ if (nativeShadowChanged && next.beat > 0) {
7198
+ nativeShadowChanged = false;
7199
+ configure();
7200
+ }
7107
7201
  worker.postMessage({ type: "state", id, state: sent, time: performance.timeOrigin + performance.now() });
7108
7202
  }
7109
7203
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neo-ui-kit/react",
3
- "version": "1.3.11",
3
+ "version": "1.3.13",
4
4
  "description": "NEO UI — React component library",
5
5
  "type": "module",
6
6
  "license": "MIT",