@neo-ui-kit/react 1.2.3 → 1.3.2

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
@@ -25,13 +25,19 @@ import "@neo-ui-kit/react/styles.css";
25
25
  To test repository changes before publishing, run `npm run package` from the repository root and install the resulting archive in a separate application:
26
26
 
27
27
  ```bash
28
- npm install /path/to/adui/release/neo-ui-kit-react-1.2.3.tgz
28
+ npm install /path/to/adui/release/neo-ui-kit-react-1.3.2.tgz
29
29
  ```
30
30
 
31
31
  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-пакет).
32
32
 
33
33
  ## Theme
34
34
 
35
+ Decorations inside `data-ad-paused` or `data-ad-offscreen` boundaries retain their
36
+ phase while paused. After changing either attribute yourself, call `refreshMotion()`
37
+ (imported from `@neo-ui-kit/react`) after the DOM update.
38
+ This refreshes both CSS and scripted decorations on the next shared frame.
39
+ `usePauseOffscreen` handles its own notifications.
40
+
35
41
  Two colours build the whole palette (a primary scale, a secondary scale and tinted neutrals):
36
42
 
37
43
  ```tsx
@@ -178,10 +184,71 @@ import { Form, FormFields, useForm } from "@neo-ui-kit/react/forms";
178
184
  import { ThemeProvider, useMotion } from "@neo-ui-kit/react/core";
179
185
  ```
180
186
 
187
+ ## ThemePicker and declarative fields
188
+
189
+ `ThemePicker` renders compact built-in swatches, or a gallery when `options` are supplied. Each option has `value`, `label`, optional `image` (foreground), `background` (full-card backdrop), `color` and `description`. Foreground pictures keep the same contained size when selected; captions and their accent lines sit below the picture. The selected card has a check mark and glow. Arrow keys, Home and End select a theme; `disabled` blocks selection.
190
+
191
+ ```tsx
192
+ import { ThemePicker } from "@neo-ui-kit/react";
193
+
194
+ <ThemePicker value={theme} onValueChange={setTheme} options={[
195
+ { value: "dark", label: "Dark", image: "/themes/dark-icon.webp",
196
+ background: "/themes/dark-background.webp", color: "#ff244c" },
197
+ { value: "light", label: "Light", image: "/themes/light-icon.webp",
198
+ background: "/themes/light-background.webp", color: "#f3d38f" },
199
+ ]} />
200
+ ```
201
+
202
+ `FormFields` binds each `name` (including dotted paths) to the nearest `Form`. Supported kinds: `text`, `number`, `textarea`, `select`, `autocomplete`, `checkbox`, `switch`, `color`, `date`, `file`, `people`, `slider`, `rotary`, `tags`, `theme`, `toggle`, `segmented`, `tabs` and `custom`. `theme` uses `ThemePicker`; `toggle` uses `ToggleButton`; `segmented` uses `SegmentedControl`. Pass the control's options through `props`. Initial values and resets come from the form, so set them in `initialValues` rather than `props.defaultValue`.
203
+
204
+ For `kind: "custom"`, `render(field, form)` is required and returns your component. `field` supplies `value`, `onValueChange`, `onBlur`, `error` and `touched`; `form` is the typed `FormApi`. The renderer updates when any form value changes. It owns its label, value conversion and validation display. `span` and `showWhen` also work for custom fields; no registry entry is needed.
205
+
206
+ ```tsx
207
+ import { TextField, type FormFieldDefinition } from "@neo-ui-kit/react";
208
+ import { Form, FormFields, useForm } from "@neo-ui-kit/react/forms";
209
+
210
+ type Values = { theme: string; nickname: string };
211
+ const fields: FormFieldDefinition<Values>[] = [
212
+ { name: "theme", kind: "theme", label: "Theme" },
213
+ { name: "nickname", kind: "custom", render: (field, form) => (
214
+ <TextField label={`Nickname (${form.values.theme})`}
215
+ value={String(field.value ?? "")} onValueChange={field.onValueChange}
216
+ onBlur={field.onBlur} error={field.touched ? field.error : undefined} />
217
+ ) },
218
+ ];
219
+
220
+ function Preferences() {
221
+ const form = useForm<Values>({ initialValues: { theme: "ruby", nickname: "DJ" } });
222
+ return <Form form={form}><FormFields fields={fields} /></Form>;
223
+ }
224
+ ```
225
+
181
226
  ## RotaryKnob
182
227
 
183
228
  Canvas-rendered volume knob. Drag near the edge to rotate, drag from the center to move linearly, click the outer scale to jump. Wheel and arrow keys change the value, Shift gives a fine step, PageUp/PageDown change by 10, Home/End set 0/100, Escape cancels a drag, double-click resets. `onValueChange` fires while adjusting, `onValueCommit` after the gesture. `readOnly` turns it into a gauge.
184
229
 
230
+ ## 1.3.2 release notes
231
+
232
+ - Waveform updates only its moving layers and pauses invisible sparks and fully clipped sheen.
233
+ - RotaryKnob completes all canvas layers after StrictMode restarts, avoids duplicate pending renders of the same size, and waits for square canvas dimensions before drawing its feedback ring.
234
+ - Scripted decorations respect pause/offscreen boundaries. `refreshMotion()` batches boundary changes without checking ancestors on every frame.
235
+ - FormFields helper selectors avoid invalidating unrelated descendants while keeping the same layout.
236
+ - Playground code windows can expand to the full browser viewport. The editor and preview resize together without resetting the draft.
237
+
238
+ Standalone profiling on an RTX 3060, Electron 42.11.8, at 1920×1050 with CPU ×4
239
+ showed 36.9 → 58.9 FPS over 45 seconds for 20 cards combining a waveform, knob,
240
+ and animated border. This is a measured scenario, not a guarantee of 60 FPS for
241
+ every application. CPU ×16/×32 and software compositing still have substantial
242
+ slowdowns; changing rasterization alone did not remove them.
243
+
244
+ ## 1.3.1 release notes
245
+
246
+ - ThemePicker gallery cards accept separate backgrounds and keep selected/unselected pictures at the same size, with readable captions and theme-coloured accent lines.
247
+ - FormFields adds `theme`, `toggle`, `segmented`, `tabs` and a typed `custom` renderer. Existing kinds and registries remain supported.
248
+ - QuantumBackground adds `particleDepth`, `particleScale` and `particleSharpness` to control particle depth, size and crispness. Defaults preserve the previous appearance.
249
+ - The documentation includes interactive gallery and custom-field examples.
250
+ - Updated English/Ukrainian catalog descriptions and existing release checks for the expanded fields and particle controls. Version 1.3.0 was not published to npm.
251
+
185
252
  ## 1.2.3 release notes
186
253
 
187
254
  - `Avatar` imports only its host seal illustration, allowing bundlers to remove unused illustrations. Its appearance and public API are unchanged.
@@ -260,6 +327,8 @@ import "@neo-ui-kit/react/styles.css";
260
327
 
261
328
  Settings (`QuantumBackgroundSettings`): `field` (one of `quantumBackgroundFields`), `sensitivity`, `density`, `timeScale`, `paused`, `interactive`, nebula, connection, crawler, particle-trail, lens-flare, depth-of-field, camera-shake, bloom, trails, chromatic, anamorphic, god-ray, grain, vortex, pulse, bass-gate, onset, audio-camera, terrain-height and adaptive-quality values. Unset props use `quantumBackgroundDefaults`; only props that change are sent to the scene. `quantumBackgroundControls` lists each setting's group, label, range, step and the switch it depends on, for building your own settings panel next to the component. `particleCount` sets how many particles are drawn across both layers (0 to `quantumBackgroundMaxParticles`, 85 000); leave it unset to follow `density`. Adaptive quality may draw fewer, so turn `adaptiveQualityEnabled` off when the exact count matters. `particleBudget` is an upper cap on top of either. `fieldSize` is the share of the scene the field spans (0.78 by default, 1 reaches the edges). `fullscreen` makes the scene cover the whole viewport above the page, and the field then runs edge to edge (`quantumBackgroundFullscreenFieldSize`, 1.1) unless `fieldSize` is set (in the browser's top layer, so no ancestor's `overflow`, `transform` or `z-index` clips it, and the iframe keeps running); Escape calls `onFullscreenChange(false)`, so keep it controlled. `children` are laid over the scene, e.g. a close button while full screen. This differs from `ref.toggleFullscreen()`, which uses the browser's Fullscreen API and needs a click.
262
329
 
330
+ `particleDepth` (0–1, default 0) separates particles into depth: rare foreground lights are larger and move faster, while the secondary layer stays farther away. `particleScale` (0.5–3, default 1) changes their size independently of the field footprint. `particleSharpness` (0–1, default 0) blends toward a crisp white core with a compact coloured halo. These controls share the existing particle buffers and preserve the original appearance at their defaults. For a clear foreground over an image, try `particleDepth={1} particleScale={1.5} particleSharpness={1}` with `dofEnabled={false} bloom={0} trails={0}`.
331
+
263
332
  `threeUrl` points the scene at your own copy of three.js 0.182 instead of unpkg (for example `threeUrl="./vendor/three/"` with `build/three.module.js` and `examples/jsm/` served there); with it the scene makes no network requests at all. Only the scene's import map changes, so a strict CSP pins the same script hashes everywhere.
264
333
 
265
334
  `spectrum` feeds sound the scene cannot hear itself — a native audio engine, a WebRTC call, a server stream: band levels 0–1 from low to high frequencies, log-spaced across `spectrumRange` (default `[40, 12000]` Hz), any band count. Pass a new array whenever the levels change (10–60 times a second); bands, beats, reaction lines and flashes follow it exactly as they follow a playing song. A file or microphone started from the ref takes over while it plays; `spectrum={undefined}` returns the scene to silence.
@@ -1,3 +1,7 @@
1
+ import {
2
+ classes,
3
+ mark
4
+ } from "./chunk-BJSFN52W.js";
1
5
  import {
2
6
  canObserveIntersection,
3
7
  createResizeObserver,
@@ -32,6 +36,7 @@ var pendingTargets = /* @__PURE__ */ new Set();
32
36
  var active = [];
33
37
  var lastScan = -Infinity;
34
38
  var needsScan = false;
39
+ var boundariesChanged = false;
35
40
  var tickers = /* @__PURE__ */ new Set();
36
41
  U.subscribeTick = (listener) => {
37
42
  tickers.add(listener);
@@ -81,6 +86,16 @@ function scan(now, animations = pendingAnimations) {
81
86
  }
82
87
  function tick(now) {
83
88
  raf = null;
89
+ if (boundariesChanged) {
90
+ boundariesChanged = false;
91
+ for (const scope of preferenceScopes) {
92
+ const runnable = scope.enabled && !scope.root.closest?.("[data-ad-offscreen], [data-ad-paused]");
93
+ if (runnable === running.has(scope)) continue;
94
+ scope.previous = null;
95
+ if (runnable) running.add(scope);
96
+ else running.delete(scope);
97
+ }
98
+ }
84
99
  if (needsScan || now - lastScan > 400) scan(now);
85
100
  const deadline = performance.now() + 8;
86
101
  for (const item of measurements) {
@@ -128,6 +143,7 @@ function schedule() {
128
143
  }
129
144
  function refreshMotion() {
130
145
  needsScan = true;
146
+ boundariesChanged = true;
131
147
  schedule();
132
148
  }
133
149
  var wake = () => {
@@ -243,7 +259,7 @@ U.createMotion = (root = document) => {
243
259
  this.previous = null;
244
260
  const target = root.dataset ? root : root.host || document.documentElement;
245
261
  target.dataset.adMotion = enabled ? "on" : "off";
246
- if (this.enabled) running.add(this);
262
+ if (this.enabled && !root.closest?.("[data-ad-offscreen], [data-ad-paused]")) running.add(this);
247
263
  else running.delete(this);
248
264
  schedule();
249
265
  return this.enabled;
@@ -763,6 +779,83 @@ function useSmoothWheel(ref) {
763
779
  }, [ref, enabled]);
764
780
  }
765
781
 
782
+ // src/components/foundation/ThemeProvider/ThemeProvider.tsx
783
+ import { createContext, useContext, useMemo } from "react";
784
+ import { jsx } from "react/jsx-runtime";
785
+ var themes = {
786
+ ruby: ["#ff244c", "#ff7c97"],
787
+ light: ["#e0a43a", "#f3d38f"],
788
+ green: ["#10c99a", "#7cf3d0"],
789
+ violet: ["#9b5cff", "#d3a8ff"]
790
+ };
791
+ var ThemeColorSchemeContext = createContext("dark");
792
+ var ThemePaletteContext = createContext({
793
+ theme: "ruby",
794
+ primary: themes.ruby[0],
795
+ secondary: themes.ruby[1]
796
+ });
797
+ var useThemePalette = () => useContext(ThemePaletteContext);
798
+ var emptyTokens = {};
799
+ function hue(hex) {
800
+ const [r, g, b] = [1, 3, 5].map(
801
+ (i) => parseInt(hex.slice(i, i + 2), 16) / 255
802
+ );
803
+ const max = Math.max(r, g, b);
804
+ const delta = max - Math.min(r, g, b);
805
+ if (!delta) return 0;
806
+ let h = (r - g) / delta + 4;
807
+ if (max === r) h = (g - b) / delta % 6;
808
+ else if (max === g) h = (b - r) / delta + 2;
809
+ return (h * 60 + 360) % 360;
810
+ }
811
+ var ThemeProvider = ({
812
+ theme,
813
+ colorScheme,
814
+ primary,
815
+ secondary,
816
+ accent,
817
+ tokens = emptyTokens,
818
+ style,
819
+ children,
820
+ id,
821
+ className,
822
+ ...attributes
823
+ }) => {
824
+ const inheritedScheme = useContext(ThemeColorSchemeContext);
825
+ const inheritedPalette = useThemePalette();
826
+ const scheme = colorScheme ?? inheritedScheme;
827
+ const selected = theme ?? inheritedPalette.theme;
828
+ const main = primary ?? accent ?? (theme ? themes[theme][0] : inheritedPalette.primary);
829
+ const light = secondary ?? (theme ? themes[theme][1] : inheritedPalette.secondary);
830
+ const vars = useMemo(() => {
831
+ const next = { ...style };
832
+ if (theme !== void 0 || primary !== void 0 || accent !== void 0) next["--ad-primary"] = main;
833
+ if (theme !== void 0 || secondary !== void 0) next["--ad-secondary"] = light;
834
+ for (const [key, value] of Object.entries(tokens))
835
+ next[key.startsWith("--") ? key : `--ad-${key}`] = value;
836
+ const effectivePrimary2 = next["--ad-primary"];
837
+ if (typeof effectivePrimary2 === "string" && /^#[0-9a-f]{6}$/i.test(effectivePrimary2) && next["--ad-hue-shift"] === void 0)
838
+ next["--ad-hue-shift"] = `${Math.round(hue(effectivePrimary2) - hue(themes.ruby[0]))}deg`;
839
+ return next;
840
+ }, [style, main, light, primary, secondary, accent, theme, tokens]);
841
+ const effectivePrimary = String(vars["--ad-primary"] ?? main);
842
+ const effectiveSecondary = String(vars["--ad-secondary"] ?? light);
843
+ const palette = useMemo(() => ({ theme: selected, primary: effectivePrimary, secondary: effectiveSecondary }), [selected, effectivePrimary, effectiveSecondary]);
844
+ return /* @__PURE__ */ jsx(ThemeColorSchemeContext.Provider, { value: scheme, children: /* @__PURE__ */ jsx(ThemePaletteContext.Provider, { value: palette, children: /* @__PURE__ */ jsx(
845
+ "div",
846
+ {
847
+ ...mark("ThemeProvider", attributes),
848
+ id,
849
+ className: classes("ad-theme", className),
850
+ style: vars,
851
+ "data-ad-component": "ThemeProvider",
852
+ "data-ad-theme": selected,
853
+ "data-ad-color-scheme": scheme,
854
+ children
855
+ }
856
+ ) }) });
857
+ };
858
+
766
859
  export {
767
860
  refreshMotion,
768
861
  setMotionFrameRate,
@@ -773,5 +866,8 @@ export {
773
866
  useTick,
774
867
  usePauseOffscreen,
775
868
  useTabShape,
776
- useSmoothWheel
869
+ useSmoothWheel,
870
+ themes,
871
+ useThemePalette,
872
+ ThemeProvider
777
873
  };