@neo-ui-kit/react 1.3.8 → 1.3.10

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,19 @@ 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.10
8
+
9
+ - MelodyRoll renders notes, lyrics, key effects and voice effects in a shared worker while keeping native keyboard input and focus.
10
+ - Bounded sprite caches and stable DOM reduce playback work; CSS/SVG fallback remains available.
11
+ - CPU ×8 measurements are near 60 FPS; ×16 page updates measured 49.5–59.8 FPS while worker rendering stayed near 60. Stable 60 FPS for the complete scene is not yet established. See the [performance report](https://github.com/Pro100Dimka/adui/blob/main/evidence/performance-report.md).
12
+
13
+ ## 1.3.9
14
+
15
+ - Rounded field variants share the existing neon outline; outlined and underlined fields have transparent backgrounds, and filled fields have no animated border.
16
+ - Faster DataTable sorting, cached playground runtime builds and shared editor sources.
17
+ - The shared motion clock defaults to 60 FPS; NeonWaves stars render in the existing worker where supported, with hidden effects paused.
18
+ - MelodyRoll indexes note blocks and limits style invalidation while retaining its effects. Stable 60 FPS under CPU ×8/×16 throttling is **not achieved**; the latest full diagnostic measured about 35.4/6.3 FPS. See the [performance report](https://github.com/Pro100Dimka/adui/blob/main/evidence/performance-report.md) for scenarios and limitations.
19
+
7
20
  ## Install
8
21
 
9
22
  Live docs: https://pro100dimka.github.io/adui/#/components/overview
@@ -25,7 +38,7 @@ import "@neo-ui-kit/react/styles.css";
25
38
  To test repository changes before publishing, run `npm run package` from the repository root and install the resulting archive in a separate application:
26
39
 
27
40
  ```bash
28
- npm install /path/to/adui/release/neo-ui-kit-react-1.3.8.tgz
41
+ npm install /path/to/adui/release/neo-ui-kit-react-1.3.10.tgz
29
42
  ```
30
43
 
31
44
  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-пакет).
@@ -68,6 +81,10 @@ export function SettingsCard() {
68
81
 
69
82
  Fields that declare `value` / `defaultValue` support controlled and uncontrolled values; checkboxes and switches use `checked` / `defaultChecked`. Dialog supports `open` / `defaultOpen` and `onOpenChange`; Popover requires controlled `open`. See each component's API for its supported state and event names; a change callback reports a user action, while the owner applies the new controlled value.
70
83
 
84
+ `InputBase` provides the shared field appearance: `variant="outlined"` has a rounded, glowing outline and a transparent background; `filled` has a glass-like fill with a soft accent highlight and a static thin border; `underlined` has only a glowing bottom line and no background. Outlined and underlined use the shared `AnimatedBorder`, including its motion and offscreen controls; filled has no animated border. Colours follow the active theme. Outlined and underlined fields remain transparent on hover, focus and when read-only. TextField, Select, Autocomplete, NumberField, TextArea, TagInput and PeoplePicker share the same radius at each size, including fields with wrapped chips. Existing adornments and floating labels are supported.
85
+
86
+ Rounded field variants use `--ad-radius-pill`, so floating labels and taller chip rows keep the capsule shape instead of retaining a radius calculated from the shorter base height.
87
+
71
88
  The package ships ESM only. Import styles once and wrap the UI in `ThemeProvider`; no playground CSS or router is required. `@neo-ui-kit/react/core`, `/forms`, `/editor`, and `/router` share the same provider contexts with the root entry.
72
89
 
73
90
  The library code is licensed under [MIT](./LICENSE). Bundled third-party materials retain their respective licenses and notices: `dist/fonts/melodix/OFL-PlayfairDisplay.txt` for the font and `dist/components/artwork/QuantumField/UPSTREAM-LICENSE.txt` for Quantum Fields.
@@ -164,6 +181,10 @@ For an unpaginated large dataset use `virtualize maxHeight={480}`. The table mou
164
181
 
165
182
  ## Responsiveness
166
183
 
184
+ The shared animation clock defaults to 60 fps, including border and wave workers. At full rate it follows browser animation frames without an extra timer; `setMotionFrameRate(30)` remains available when an application explicitly prefers less decorative work. Hidden/offscreen work still pauses. The rate is a scheduling target, not a minimum guaranteed on every device or screen.
185
+
186
+ NeonWaves draws stars in its existing wave worker, preserving their positions, sizes and easing. Browsers without worker canvas support share nine CSS twinkle phases instead; the entire decoration pauses offscreen. QuantumField's WebGL renderer targets 60 fps; its CPU canvas fallback retains the 24/30 fps quality limits to protect the UI thread. The separate QuantumBackground visualizer keeps its own adaptive-quality controls. CPU ×8/×16 measurements and remaining limits are recorded in [the performance report](../../evidence/performance-report.md).
187
+
167
188
  The package pauses decorative work outside the viewport and in hidden tabs. Its shared motion clock stops when no visible work remains; consumers do not need a separate animation scheduler. Playback updates are isolated from transport and volume controls. Audio analysis and loader image processing yield between short CPU work slices, and unmounting cancels stale results. Processed loader images are capped at 2048px on the longest side; turning background removal off restores the original upload.
168
189
 
169
190
  Router updates are immediate by default. Opt into native snapshot transitions with `transition` only when their temporary interruption of pointer interaction suits your screen; normal CSS entrances do not require it.
@@ -172,6 +193,10 @@ Animated border geometry is initialized and resized in small batches on the shar
172
193
 
173
194
  Keep `rows`, `columns`, `options`, and `initialValues` immutable: replace them when their contents change. Stable references allow expensive derived results to be reused across selection, navigation, and other local UI changes. Do not memoize every prop automatically.
174
195
 
196
+ MelodyRoll also expects an immutable `notes` array: keep its reference stable during playback and replace it after editing notes. It caches pitch bounds and time bounds for blocks of 128 notes, skipping blocks outside the visible window. Input order and overlapping notes are preserved; heavily interleaved unsorted data can still require a full scan. The moving strip uses nearby time coordinates instead of ever-growing song offsets. Beat, voice level and accuracy updates are scoped to the elements that use them, avoiding restyling the whole keyboard and note strip. These changes preserve the existing glow, transitions, lyrics, seeking and wheel zoom; CPU ×8/×16 stress results are in the performance report.
197
+
198
+ Version 1.3.10 renders notes, lyrics, key effects, beat glow, voice marker and the three-layer voice trail through a shared OffscreenCanvas worker. Two adjacent note windows bound the data sent to it; unchanged note batches are not resent on each tick. Native keyboard buttons retain pointer input, keyboard activation and focus. CSS supplies colours, shadows, timing and text geometry; readable webfont sources are loaded into the worker before handoff. Note sprites are cached (512 base sprites; filtered sprites limited to 8 × 1024² physical pixels), and structural DOM stays unchanged during playback. CSS/SVG remains active until readiness and returns on failure, unavailable APIs/CSP, or unsupported styles/font sources. Hidden/paused scenes stop drawing; the last unmount terminates the worker. Canvas and CSS antialiasing differ slightly. In repeated 30-second runs, page updates reached about 59.9–60 FPS at CPU ×8 and 49.5–59.5 FPS at ×16; the worker drawing loop stayed near 60 FPS. Stable 60 FPS for the complete scene is **not yet established**. See the [performance report](https://github.com/Pro100Dimka/adui/blob/main/evidence/performance-report.md) for measurements and limits.
199
+
175
200
  For large tables, use `pageSize` or opt into `virtualize` to bound mounted rows. Virtualization bounds rendering, not the cost of client-side sorting/filtering: use the manual server contracts for datasets that are too large to process locally. Open Select/Autocomplete lists are not virtualized: filter very large datasets before supplying options. `PeoplePicker.onSearch(query, signal)` receives an optional `AbortSignal`; forward it to your data client to cancel superseded requests. Expensive custom renderers, validators, and aggregates still need their own work budget.
176
201
 
177
202
  ## Entry points
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  themes,
3
+ useBorder,
3
4
  usePauseOffscreen
4
- } from "./chunk-YVRQTI7B.js";
5
+ } from "./chunk-OCH27SNY.js";
5
6
  import {
6
7
  assignRef,
7
8
  clamp,
@@ -351,6 +352,28 @@ var Popover = (p) => {
351
352
  );
352
353
  };
353
354
 
355
+ // src/components/effects/AnimatedBorder/AnimatedBorder.tsx
356
+ import { createElement as createElement3, useRef as useRef3 } from "react";
357
+ function AnimatedBorder({
358
+ as = "div",
359
+ shell = false,
360
+ round = false,
361
+ children,
362
+ ...props
363
+ }) {
364
+ const ref = useRef3(null);
365
+ useBorder(ref, true, shell, round);
366
+ usePauseOffscreen(ref);
367
+ return createElement3(
368
+ as,
369
+ {
370
+ ...mark("AnimatedBorder", props),
371
+ ref
372
+ },
373
+ children
374
+ );
375
+ }
376
+
354
377
  // src/components/controls/InputBase/InputBase.tsx
355
378
  import { forwardRef } from "react";
356
379
  import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
@@ -383,6 +406,7 @@ var InputBase = /* @__PURE__ */ forwardRef(function InputBase2({
383
406
  "data-filled": filled || void 0,
384
407
  onClick,
385
408
  children: [
409
+ variant !== "filled" && !disabled && !readOnly && /* @__PURE__ */ jsx5(AnimatedBorder, { className: "ad-input-border" }),
386
410
  startAdornment && /* @__PURE__ */ jsx5("span", { className: "ad-input-adornment", "data-position": "start", children: startAdornment }),
387
411
  /* @__PURE__ */ jsxs2("span", { className: "ad-input-base-content", children: [
388
412
  label && /* @__PURE__ */ jsx5("span", { className: "ad-input-label", id: labelId, children: label }),
@@ -418,7 +442,7 @@ var hsvToHex = ({ h, s, v }) => {
418
442
  };
419
443
 
420
444
  // src/components/controls/ColorPicker/ColorPicker.tsx
421
- import { useEffect, useRef as useRef3, useState } from "react";
445
+ import { useEffect, useRef as useRef4, useState } from "react";
422
446
 
423
447
  // src/components/controls/internal.tsx
424
448
  import { Fragment, useId as useId2, useLayoutEffect as useLayoutEffect2 } from "react";
@@ -664,7 +688,7 @@ function ColorPicker({ value: controlled, defaultValue = "#ff244c", onValueChang
664
688
  useEffect(() => {
665
689
  if (p.disabled) setOpen(false);
666
690
  }, [p.disabled]);
667
- const box = useRef3(null);
691
+ const box = useRef4(null);
668
692
  const ids = useFieldIds(p.label, p.description, void 0, p);
669
693
  const hex = normalizeHex(value) ?? "#ff244c";
670
694
  if (inline)
@@ -690,7 +714,7 @@ function ColorPicker({ value: controlled, defaultValue = "#ff244c", onValueChang
690
714
  }
691
715
 
692
716
  // src/components/controls/shared.tsx
693
- import { useEffect as useEffect2, useRef as useRef4 } from "react";
717
+ import { useEffect as useEffect2, useRef as useRef5 } from "react";
694
718
  import { jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
695
719
  function buttonView(p, name = "Button") {
696
720
  const {
@@ -764,7 +788,7 @@ function BooleanControl({
764
788
  p.defaultChecked ?? false,
765
789
  p.onValueChange
766
790
  );
767
- const input = useRef4(null);
791
+ const input = useRef5(null);
768
792
  useEffect2(() => {
769
793
  if (input.current) input.current.indeterminate = !!p.indeterminate;
770
794
  }, [p.indeterminate]);
@@ -797,11 +821,11 @@ var Button = /* @__PURE__ */ forwardRef2(function Button2(p, ref) {
797
821
  });
798
822
 
799
823
  // src/components/controls/FilePicker/FilePicker.tsx
800
- import { useEffect as useEffect3, useRef as useRef5, useState as useState2 } from "react";
824
+ import { useEffect as useEffect3, useRef as useRef6, useState as useState2 } from "react";
801
825
  import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
802
826
  var FilePicker = (p) => {
803
827
  const tr = useTr();
804
- const input = useRef5(null), [names, setNames] = useState2(""), [preview, setPreview] = useState2(), [over, setOver] = useState2(false);
828
+ const input = useRef6(null), [names, setNames] = useState2(""), [preview, setPreview] = useState2(), [over, setOver] = useState2(false);
805
829
  useEffect3(() => () => {
806
830
  if (preview) URL.revokeObjectURL(preview);
807
831
  }, [preview]);
@@ -949,7 +973,7 @@ var Tab = /* @__PURE__ */ forwardRef3(function Tab2({
949
973
  });
950
974
 
951
975
  // src/components/controls/Tabs/Tabs.tsx
952
- import { useLayoutEffect as useLayoutEffect3, useRef as useRef6, useState as useState3 } from "react";
976
+ import { useLayoutEffect as useLayoutEffect3, useRef as useRef7, useState as useState3 } from "react";
953
977
  import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
954
978
  function Tabs(p) {
955
979
  const tr = useTr();
@@ -963,7 +987,7 @@ function Tabs(p) {
963
987
  p.defaultValue ?? items.find((item) => !item.disabled)?.value ?? "",
964
988
  p.onValueChange
965
989
  );
966
- const buttons = useRef6([]);
990
+ const buttons = useRef7([]);
967
991
  const selected = items.findIndex((item) => item.value === value && !item.disabled);
968
992
  const firstEnabled = items.findIndex((item) => !item.disabled);
969
993
  const fallback = items[firstEnabled]?.value;
@@ -972,7 +996,7 @@ function Tabs(p) {
972
996
  }, [p.value, selected, fallback, setValue]);
973
997
  const [place2, setPlace] = useState3();
974
998
  const [moving, setMoving] = useState3(false);
975
- const first = useRef6(true);
999
+ const first = useRef7(true);
976
1000
  useLayoutEffect3(() => {
977
1001
  const tab = buttons.current[selected];
978
1002
  if (!tab) return setPlace(void 0);
@@ -1401,7 +1425,7 @@ function Chip({ label, icon, avatar, onRemove, removeLabel, onClick, selected, d
1401
1425
  import { useState as useState4 } from "react";
1402
1426
 
1403
1427
  // src/components/controls/ChipField.tsx
1404
- import { useRef as useRef7 } from "react";
1428
+ import { useRef as useRef8 } from "react";
1405
1429
  import { jsx as jsx19, jsxs as jsxs11 } from "react/jsx-runtime";
1406
1430
  function ChipField({
1407
1431
  className,
@@ -1425,7 +1449,7 @@ function ChipField({
1425
1449
  inputProps
1426
1450
  }) {
1427
1451
  const ids = useFieldIds(label, description || error, inputProps?.id, inputProps);
1428
- const input = useRef7(null);
1452
+ const input = useRef8(null);
1429
1453
  const floating = labelPlacement === "floating" && !!label;
1430
1454
  const hasChips = Array.isArray(chips) ? chips.length > 0 : !!chips;
1431
1455
  return /* @__PURE__ */ jsx19(FieldFrame, { ids, className: `ad-chip-field ${className}`, label: floating ? void 0 : label, required, description, error, children: /* @__PURE__ */ jsx19(
@@ -1552,7 +1576,7 @@ function TagInput({
1552
1576
  }
1553
1577
 
1554
1578
  // src/components/controls/PeoplePicker/PeoplePicker.tsx
1555
- import { useEffect as useEffect4, useId as useId3, useRef as useRef8, useState as useState5 } from "react";
1579
+ import { useEffect as useEffect4, useId as useId3, useRef as useRef9, useState as useState5 } from "react";
1556
1580
  import { jsx as jsx21, jsxs as jsxs13 } from "react/jsx-runtime";
1557
1581
  var searchDelay = 220;
1558
1582
  function PeoplePicker({
@@ -1574,9 +1598,9 @@ function PeoplePicker({
1574
1598
  const [found, setFound] = useState5(null);
1575
1599
  const [loading, setLoading] = useState5(false);
1576
1600
  const [searchError, setSearchError] = useState5();
1577
- const box = useRef8(null);
1601
+ const box = useRef9(null);
1578
1602
  const listId = `${useId3()}-people`;
1579
- const search = useRef8(onSearch);
1603
+ const search = useRef9(onSearch);
1580
1604
  search.current = onSearch;
1581
1605
  const remote = !!onSearch;
1582
1606
  const needle = query.trim().toLowerCase();
@@ -1741,7 +1765,7 @@ function PeoplePicker({
1741
1765
  }
1742
1766
 
1743
1767
  // src/components/controls/DatePicker/DatePicker.tsx
1744
- import { useEffect as useEffect5, useMemo as useMemo3, useRef as useRef9, useState as useState6 } from "react";
1768
+ import { useEffect as useEffect5, useMemo as useMemo3, useRef as useRef10, useState as useState6 } from "react";
1745
1769
  import { jsx as jsx22, jsxs as jsxs14 } from "react/jsx-runtime";
1746
1770
  var iso = (date) => `${String(date.getFullYear()).padStart(4, "0")}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
1747
1771
  var calendarDate = (year, month, day) => {
@@ -1771,7 +1795,7 @@ function DatePicker({ value: controlled, defaultValue = "", onValueChange, min,
1771
1795
  }, [p.disabled]);
1772
1796
  const [text, setText] = useState6(null);
1773
1797
  const [month, setMonth] = useState6(() => selected ?? /* @__PURE__ */ new Date());
1774
- const box = useRef9(null);
1798
+ const box = useRef10(null);
1775
1799
  const ids = useFieldIds(p.label, p.description || p.error, void 0, p);
1776
1800
  const format = useMemo3(() => new Intl.DateTimeFormat(locale, { day: "2-digit", month: "2-digit", year: "numeric" }), [locale]);
1777
1801
  const weekdays = useMemo3(
@@ -1881,7 +1905,7 @@ function DatePicker({ value: controlled, defaultValue = "", onValueChange, min,
1881
1905
  }
1882
1906
 
1883
1907
  // src/components/controls/Autocomplete/Autocomplete.tsx
1884
- import { useEffect as useEffect6, useMemo as useMemo4, useRef as useRef10, useState as useState7 } from "react";
1908
+ import { useEffect as useEffect6, useMemo as useMemo4, useRef as useRef11, useState as useState7 } from "react";
1885
1909
  import { Fragment as Fragment6, jsx as jsx23, jsxs as jsxs15 } from "react/jsx-runtime";
1886
1910
  var defaultOptions = ["WASAPI Shared", "WASAPI Exclusive", "ASIO"];
1887
1911
  var Autocomplete = ({
@@ -1915,8 +1939,8 @@ var Autocomplete = ({
1915
1939
  );
1916
1940
  const [open, setOpen] = useState7(false);
1917
1941
  const [active, setActive] = useState7(0);
1918
- const inputNode = useRef10(null);
1919
- const box = useRef10(null);
1942
+ const inputNode = useRef11(null);
1943
+ const box = useRef11(null);
1920
1944
  const ids = useFieldIds(label, description || error, input.id, input);
1921
1945
  const locked = !!(input.disabled || input.readOnly);
1922
1946
  useEffect6(() => {
@@ -2155,7 +2179,7 @@ var NumberField = ({
2155
2179
  };
2156
2180
 
2157
2181
  // src/components/controls/Select/Select.tsx
2158
- import { forwardRef as forwardRef6, useEffect as useEffect7, useMemo as useMemo5, useRef as useRef11, useState as useState8 } from "react";
2182
+ import { forwardRef as forwardRef6, useEffect as useEffect7, useMemo as useMemo5, useRef as useRef12, useState as useState8 } from "react";
2159
2183
  import { Fragment as Fragment8, jsx as jsx26, jsxs as jsxs17 } from "react/jsx-runtime";
2160
2184
  var defaultKey = (value) => typeof value === "object" && value !== null ? JSON.stringify(value) : String(value);
2161
2185
  var textOf = (option) => option.text ?? (typeof option.label === "string" || typeof option.label === "number" ? String(option.label) : "");
@@ -2183,8 +2207,8 @@ var Select = /* @__PURE__ */ forwardRef6(function Select2(p, ref) {
2183
2207
  setQuery("");
2184
2208
  }
2185
2209
  }, [p.disabled]);
2186
- const anchor = useRef11(null);
2187
- const box = useRef11(null);
2210
+ const anchor = useRef12(null);
2211
+ const box = useRef12(null);
2188
2212
  const keyedOptions = useMemo5(() => options.map((option) => ({ option, key: keyOf(option.value) })), [options, keyOf]);
2189
2213
  const selectedKey = value === void 0 ? void 0 : keyOf(value);
2190
2214
  const selected = keyedOptions.find(({ key }) => key === selectedKey)?.option;
@@ -2354,10 +2378,10 @@ var Select = /* @__PURE__ */ forwardRef6(function Select2(p, ref) {
2354
2378
  });
2355
2379
 
2356
2380
  // src/components/effects/ImageShine/ImageShine.tsx
2357
- import { useRef as useRef12 } from "react";
2381
+ import { useRef as useRef13 } from "react";
2358
2382
  import { jsx as jsx27, jsxs as jsxs18 } from "react/jsx-runtime";
2359
2383
  function ImageShine({ src, glow = true, label, style, ...p }) {
2360
- const pauseRef = useRef12(null);
2384
+ const pauseRef = useRef13(null);
2361
2385
  usePauseOffscreen(pauseRef);
2362
2386
  return /* @__PURE__ */ jsxs18(
2363
2387
  "span",
@@ -2482,7 +2506,7 @@ import {
2482
2506
  useEffect as useEffect8,
2483
2507
  useLayoutEffect as useLayoutEffect4,
2484
2508
  useMemo as useMemo6,
2485
- useRef as useRef13,
2509
+ useRef as useRef14,
2486
2510
  useState as useState9
2487
2511
  } from "react";
2488
2512
 
@@ -2562,17 +2586,17 @@ function paintCanvas(key, width, height, painting) {
2562
2586
  import { jsx as jsx29, jsxs as jsxs20 } from "react/jsx-runtime";
2563
2587
  var RotaryKnob = (p) => {
2564
2588
  const tr = useTr();
2565
- const rootRef = useRef13(null);
2566
- const baseRef = useRef13(null);
2567
- const feedbackRef = useRef13(null);
2568
- const rotorRef = useRef13(null);
2569
- const controlRef = useRef13(null);
2570
- const readoutRef = useRef13(null);
2571
- const controllerRef = useRef13(null);
2572
- const onChangeRef = useRef13(p.onValueChange);
2573
- const onCommitRef = useRef13(p.onValueCommit);
2574
- const disabledRef = useRef13(!!p.disabled);
2575
- const readOnlyRef = useRef13(!!p.readOnly);
2589
+ const rootRef = useRef14(null);
2590
+ const baseRef = useRef14(null);
2591
+ const feedbackRef = useRef14(null);
2592
+ const rotorRef = useRef14(null);
2593
+ const controlRef = useRef14(null);
2594
+ const readoutRef = useRef14(null);
2595
+ const controllerRef = useRef14(null);
2596
+ const onChangeRef = useRef14(p.onValueChange);
2597
+ const onCommitRef = useRef14(p.onValueCommit);
2598
+ const disabledRef = useRef14(!!p.disabled);
2599
+ const readOnlyRef = useRef14(!!p.readOnly);
2576
2600
  const numberFormat = useMemo6(
2577
2601
  () => new Intl.NumberFormat("ru-RU", { maximumFractionDigits: 1 }),
2578
2602
  []
@@ -2581,14 +2605,14 @@ var RotaryKnob = (p) => {
2581
2605
  const span = (p.max ?? 100) - min || 1;
2582
2606
  const toPosition = (value) => clamp((value - min) / span * 100);
2583
2607
  const toValue = (position) => Math.round((min + position / 100 * span) * 1e6) / 1e6;
2584
- const controlledPosition = useRef13(void 0);
2608
+ const controlledPosition = useRef14(void 0);
2585
2609
  controlledPosition.current = p.value === void 0 ? void 0 : toPosition(p.value);
2586
2610
  const displayScale = p.displayScale ?? 1;
2587
2611
  const format = (value) => `${numberFormat.format(value * displayScale)}${p.suffix ?? "%"}`;
2588
- const scale = useRef13({ toValue, format });
2612
+ const scale = useRef14({ toValue, format });
2589
2613
  scale.current = { toValue, format };
2590
- const stepRef = useRef13(1);
2591
- const fineStepRef = useRef13(0.1);
2614
+ const stepRef = useRef14(1);
2615
+ const fineStepRef = useRef14(0.1);
2592
2616
  onChangeRef.current = p.onValueChange;
2593
2617
  onCommitRef.current = p.onValueCommit;
2594
2618
  disabledRef.current = !!p.disabled;
@@ -2596,8 +2620,8 @@ var RotaryKnob = (p) => {
2596
2620
  stepRef.current = Math.max(1e-3, (p.step ?? span / 100) / span * 100);
2597
2621
  fineStepRef.current = Math.max(1e-3, (p.fineStep ?? span / 1e3) / span * 100);
2598
2622
  const initial = toPosition(p.value ?? p.defaultValue ?? min + span * 0.67);
2599
- const initialRef = useRef13(initial);
2600
- const resetRef = useRef13(void 0);
2623
+ const initialRef = useRef14(initial);
2624
+ const resetRef = useRef14(void 0);
2601
2625
  resetRef.current = p.resetValue === void 0 ? void 0 : toPosition(p.resetValue);
2602
2626
  const diameter = p.diameter ?? { xs: 84, sm: 124, md: 220, lg: 320 }[normalizeSize(p.size) ?? "md"];
2603
2627
  useLayoutEffect4(() => {
@@ -3277,7 +3301,7 @@ var RotaryKnob = (p) => {
3277
3301
  controllerRef.current?.setValue(toPosition(p.value), false);
3278
3302
  }, [p.value, min, span]);
3279
3303
  const [draft, setDraft] = useState9(null);
3280
- const cancelled = useRef13(false);
3304
+ const cancelled = useRef14(false);
3281
3305
  const editable = !p.disabled && !p.readOnly;
3282
3306
  const applyDraft = () => {
3283
3307
  if (cancelled.current) return;
@@ -3396,7 +3420,7 @@ import {
3396
3420
  useContext,
3397
3421
  useEffect as useEffect9,
3398
3422
  useMemo as useMemo7,
3399
- useRef as useRef14,
3423
+ useRef as useRef15,
3400
3424
  useState as useState10
3401
3425
  } from "react";
3402
3426
  import { jsx as jsx30 } from "react/jsx-runtime";
@@ -3416,8 +3440,8 @@ var setPath = (value, path, next) => {
3416
3440
  return root;
3417
3441
  };
3418
3442
  function useForm(options) {
3419
- const [values, setValues] = useState10(options.initialValues), [errors, setErrors] = useState10({}), [touched, setTouchedState] = useState10({}), [submitting, setSubmitting] = useState10(false), latest = useRef14(values), touchedRef = useRef14(touched), validationId = useRef14(0), submittingRef = useRef14(false), settings = useRef14(options), initialKey = useMemo7(() => JSON.stringify(options.initialValues), [options.initialValues]);
3420
- const previousInitialKey = useRef14(initialKey);
3443
+ const [values, setValues] = useState10(options.initialValues), [errors, setErrors] = useState10({}), [touched, setTouchedState] = useState10({}), [submitting, setSubmitting] = useState10(false), latest = useRef15(values), touchedRef = useRef15(touched), validationId = useRef15(0), submittingRef = useRef15(false), settings = useRef15(options), initialKey = useMemo7(() => JSON.stringify(options.initialValues), [options.initialValues]);
3444
+ const previousInitialKey = useRef15(initialKey);
3421
3445
  settings.current = options;
3422
3446
  useEffect9(() => {
3423
3447
  if (previousInitialKey.current === initialKey) return;
@@ -3623,6 +3647,7 @@ export {
3623
3647
  Avatar,
3624
3648
  Popover,
3625
3649
  variantMaterial,
3650
+ AnimatedBorder,
3626
3651
  InputBase,
3627
3652
  normalizeHex,
3628
3653
  hexToHsv,
@@ -12,7 +12,7 @@ import {
12
12
  // src/core/border-worker.js
13
13
  function renderBorders() {
14
14
  const scenes = /* @__PURE__ */ new Map();
15
- let frame = null, fps = 30, last2 = -Infinity;
15
+ let frame = null, fps = 60, last2 = -Infinity;
16
16
  const release = (scene) => {
17
17
  for (const layer of scene.layers) {
18
18
  layer.mask.close();
@@ -91,7 +91,7 @@ var worker = null;
91
91
  var workerUrl = null;
92
92
  var serial = 0;
93
93
  var unavailable = false;
94
- var frameRate = 30;
94
+ var frameRate = 60;
95
95
  var clients = /* @__PURE__ */ new Map();
96
96
  var visibilityChanged = () => {
97
97
  for (const client of clients.values()) client.visibility();
@@ -466,7 +466,7 @@ function createNeonGeometry() {
466
466
  var neonGeometry = createNeonGeometry();
467
467
  function renderNeon(createGeometry) {
468
468
  const geometry = createGeometry(), scenes = /* @__PURE__ */ new Map();
469
- let frame = null, fps = 30, last2 = -Infinity;
469
+ let frame = null, fps = 60, last2 = -Infinity;
470
470
  function draw(scene) {
471
471
  const { ctx, width, height, padX, padY, sx, sy, options, time, gradient } = scene;
472
472
  ctx.clearRect(-padX, -padY, width + padX * 2, height + padY * 2);
@@ -551,6 +551,27 @@ function renderNeon(createGeometry) {
551
551
  );
552
552
  ctx.restore();
553
553
  }
554
+ const phases = scene.twinkle ??= new Float32Array(9);
555
+ for (let phase = 0; phase < (options.dots?.length ? 9 : 0); phase++) {
556
+ const cycle = (time + phase * 0.45) % 4;
557
+ const x = cycle < 2 ? cycle / 2 : (4 - cycle) / 2;
558
+ let low = 0, high = 1;
559
+ for (let step = 0; step < 20; step++) {
560
+ const t2 = (low + high) / 2;
561
+ const at = 3 * (1 - t2) ** 2 * t2 * 0.42 + 3 * (1 - t2) * t2 * t2 * 0.58 + t2 ** 3;
562
+ if (at < x) low = t2;
563
+ else high = t2;
564
+ }
565
+ const t = (low + high) / 2;
566
+ phases[phase] = 1 - (3 * t * t - 2 * t ** 3);
567
+ }
568
+ ctx.fillStyle = scene.starColor;
569
+ for (const [i, dot] of (options.dots ?? []).entries()) {
570
+ ctx.globalAlpha = 0.05 + (dot.o - 0.05) * phases[i % 9];
571
+ ctx.beginPath();
572
+ ctx.arc(dot.x * sx, dot.y * sy, Math.max(1, dot.r * 3) / 2, 0, Math.PI * 2);
573
+ ctx.fill();
574
+ }
554
575
  ctx.globalAlpha = 1;
555
576
  }
556
577
  function schedule2() {
@@ -624,7 +645,7 @@ var thread = null;
624
645
  var url = null;
625
646
  var serial2 = 0;
626
647
  var unavailable2 = false;
627
- var frameRate2 = 30;
648
+ var frameRate2 = 60;
628
649
  var clients2 = /* @__PURE__ */ new Map();
629
650
  var visibilityChanged2 = () => {
630
651
  for (const client of clients2.values()) client.visibility();
@@ -687,6 +708,7 @@ function createNeonRenderer(root, svg, initial) {
687
708
  };
688
709
  const hideOriginal = () => {
689
710
  if (!renderer.ready) return;
711
+ root.setAttribute("data-ad-neon-rendered", "");
690
712
  svg.style.visibility = "hidden";
691
713
  const node = root.querySelector(".ad-neon-waves-comets");
692
714
  if (node !== comets) {
@@ -750,6 +772,7 @@ function createNeonRenderer(root, svg, initial) {
750
772
  intersection.disconnect();
751
773
  tokens.disconnect();
752
774
  svg.style.visibility = originalVisibility;
775
+ root.removeAttribute("data-ad-neon-rendered");
753
776
  if (comets) comets.style.visibility = cometVisibility;
754
777
  canvas.remove();
755
778
  clients2.delete(id);
@@ -795,6 +818,7 @@ function createNeonRenderer(root, svg, initial) {
795
818
  stops,
796
819
  shadows,
797
820
  color: style?.backgroundColor,
821
+ starColor: getComputedStyle(root.querySelector(".ad-neon-waves-star") ?? root).backgroundColor,
798
822
  options: { ...options, comets: comet ? options.comets : 0 }
799
823
  };
800
824
  const key = JSON.stringify(config);
@@ -841,8 +865,7 @@ var measurements = /* @__PURE__ */ new Set();
841
865
  var raf = null;
842
866
  var timer = null;
843
867
  var last = 0;
844
- var weakDevice = typeof navigator !== "undefined" && ((navigator.hardwareConcurrency || 8) <= 4 || (navigator.deviceMemory || 8) <= 4);
845
- var FRAME = 1e3 / (weakDevice ? 20 : 30);
868
+ var FRAME = 1e3 / 60;
846
869
  setBorderFrameRate(1e3 / FRAME);
847
870
  setNeonFrameRate(1e3 / FRAME);
848
871
  U.setMotionFrameRate = (fps) => {
@@ -946,7 +969,8 @@ function tick(now) {
946
969
  last = now;
947
970
  }
948
971
  if (hasVisibleWork() && !document.hidden) {
949
- timer = setTimeout(() => {
972
+ if (FRAME <= 1e3 / 60) schedule();
973
+ else timer = setTimeout(() => {
950
974
  timer = null;
951
975
  schedule();
952
976
  }, Math.max(0, FRAME - (performance.now() - now) - 4));
@@ -49,6 +49,7 @@ var PianoKeyboard = /* @__PURE__ */ memo(function PianoKeyboard2({
49
49
  const key = (midi, top, size, black) => {
50
50
  const props = {
51
51
  className: "ad-piano-key",
52
+ "data-midi": midi,
52
53
  "data-black": black || void 0,
53
54
  "data-active": midi === activeMidi || void 0,
54
55
  "data-hit": midi === activeMidi && hit || void 0,
@@ -1,4 +1,4 @@
1
- /* Styles owned by NeonWaves: the strands move by script; the stars twinkle on their own and the comets are moved by script. */
1
+ /* Worker canvas draws the decoration; these elements also provide the main-thread fallback. */
2
2
  .ad-neon-waves {
3
3
  position: relative;
4
4
  display: block;
@@ -22,11 +22,24 @@
22
22
  border-radius: 50%;
23
23
  background: var(--ad-pink);
24
24
  translate: -50% -50%;
25
+ }
26
+ .ad-neon-waves-stars,
27
+ .ad-neon-waves-stars-floor {
28
+ position: absolute;
29
+ inset: 0;
30
+ }
31
+ .ad-neon-waves-stars {
25
32
  animation: ad-neon-twinkle 4s ease-in-out infinite;
26
33
  }
34
+ .ad-neon-waves[data-ad-neon-rendered] :is(.ad-neon-waves-stars, .ad-neon-waves-stars-floor) {
35
+ display: none;
36
+ }
27
37
  @keyframes ad-neon-twinkle {
38
+ 0%, 100% {
39
+ opacity: 1;
40
+ }
28
41
  50% {
29
- opacity: 0.05;
42
+ opacity: 0;
30
43
  }
31
44
  }
32
45
  /* A comet is a short bright dash with a glow. Its glow is drawn once on its own layer; the