wenay-react2 1.0.46 → 1.0.47

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
@@ -2,7 +2,8 @@
2
2
 
3
3
  Documentation index only.
4
4
 
5
- - Brief API guide: [doc/wenay-react2.md](doc/wenay-react2.md)
5
+ - Brief API guide: [doc/wenay-react2.md](doc/wenay-react2.md)
6
+ - React Native/headless entrypoint: [doc/native.md](doc/native.md)
6
7
  - Detailed / rare API guide: [doc/wenay-react2-rare.md](doc/wenay-react2-rare.md)
7
8
  - Project functionality map: [doc/PROJECT_FUNCTIONALITY.md](doc/PROJECT_FUNCTIONALITY.md)
8
9
  - Example usage standards: [doc/EXAMPLE_USAGE.md](doc/EXAMPLE_USAGE.md)
@@ -16,4 +17,5 @@ Feature-specific docs may also live next to their code, for example agGrid4 docs
16
17
 
17
18
  ## Shipped examples
18
19
 
19
- - [doc/examples/stand.tsx](doc/examples/stand.tsx) — весь интерактивный стенд (`wenay-react2/demo/stand`); Active = канон, Archive = regression/compat.`r`n- [doc/examples/peer-call-media.tsx](doc/examples/peer-call-media.tsx) — Peer calls, presence, camera and microphone relay with server-owned ACL.
20
+ - [doc/examples/stand.tsx](doc/examples/stand.tsx) — весь интерактивный стенд (`wenay-react2/demo/stand`); Active = канон, Archive = regression/compat.
21
+ - [doc/examples/peer-call-media.tsx](doc/examples/peer-call-media.tsx) — Peer calls, presence, camera and microphone relay with server-owned ACL.
@@ -0,0 +1,9 @@
1
+ # 1.0.47
2
+
3
+ - Added the platform-neutral `wenay-react2/native` entrypoint.
4
+ - Added `createNativeColumnState`: an AsyncStorage-compatible controller for order, visibility, width, sorting, filters and groups without DOM, CSS, React DOM or ag-grid dependencies.
5
+ - Added `createNativeColumnDots`: a renderer-independent gesture model with live replacement, reorder, visibility, sorting and tear-off behavior.
6
+ - Added focused hydration, persistence, gesture and dependency-boundary tests.
7
+ - Fixed the shipped examples list formatting in README.
8
+
9
+ Verification: Jest, TypeScript build, package archive inspection and runtime import of `wenay-react2/native` from the packed artifact.
package/doc/native.md ADDED
@@ -0,0 +1,37 @@
1
+ # React Native entrypoint
2
+
3
+ Import the renderer-neutral API from `wenay-react2/native`. It has no React DOM,
4
+ CSS, browser-global or ag-grid dependency.
5
+
6
+ ```ts
7
+ import AsyncStorage from '@react-native-async-storage/async-storage'
8
+ import {createNativeColumnDots, createNativeColumnState} from 'wenay-react2/native'
9
+
10
+ const columns = createNativeColumnState({
11
+ key: 'super-admin.columns',
12
+ columns: [
13
+ {key: 'server', title: 'Server', fixed: true},
14
+ {key: 'status', title: 'Status'},
15
+ {key: 'panic', title: 'Panic'},
16
+ ],
17
+ storage: AsyncStorage,
18
+ })
19
+ await columns.ready
20
+
21
+ const dots = createNativeColumnDots({state: columns, max: 8})
22
+ dots.begin('status', 40, 0)
23
+ dots.move(120, 0, {start: 0, length: 240})
24
+ dots.end()
25
+ ```
26
+
27
+ The persisted `v/order/visible/width/sort/filter/groups` shape matches the web
28
+ `ColumnsConfig`. Writes are serialized and local edits made while hydration is
29
+ pending win over stale storage.
30
+
31
+ `createNativeColumnDots` is a view-independent interaction model. Connect its
32
+ `begin/move/end` coordinates to PanResponder or Gesture Handler. Set `removeDirection` to `up`, `down` or `either` (the renderer-neutral default). It supports
33
+ live field replacement along the slider, explicit reorder and toggle, sticky
34
+ `asc -> desc -> off` sorting, and upward tear-off.
35
+
36
+ Call `dots.dispose()` and `columns.dispose()` with the owning screen. Use
37
+ `columns.api.flush()` when the latest AsyncStorage write must be awaited.
@@ -0,0 +1,41 @@
1
+ import type { NativeColumnStateController } from './columnState.js';
2
+ export type NativeColumnDotsGeometry = {
3
+ start: number;
4
+ length: number;
5
+ };
6
+ export type NativeColumnDotsSnapshot = {
7
+ selected: string | null;
8
+ visible: string[];
9
+ sort: {
10
+ key: string;
11
+ dir: 'asc' | 'desc';
12
+ } | null;
13
+ drag: null | {
14
+ origin: string;
15
+ shown: string;
16
+ index: number;
17
+ removing: boolean;
18
+ };
19
+ };
20
+ /** Coordinate-driven interaction model for PanResponder, Gesture Handler or another native view. */
21
+ export declare function createNativeColumnDots(opts: {
22
+ state: NativeColumnStateController;
23
+ max?: number;
24
+ moveSlop?: number;
25
+ removeDistance?: number;
26
+ /** Native coordinate systems and designs differ. Default 'either' is renderer-neutral. */
27
+ removeDirection?: 'up' | 'down' | 'either';
28
+ }): {
29
+ getSnapshot: () => NativeColumnDotsSnapshot;
30
+ subscribe: (listener: (snapshot: NativeColumnDotsSnapshot) => void) => () => void;
31
+ select: (key: string | null) => void;
32
+ toggle: (key: string) => void;
33
+ reorder: (key: string, to: number) => void;
34
+ toggleSelectedSort: () => void;
35
+ begin: (key: string, primary: number, cross: number) => boolean;
36
+ move: (primary: number, cross: number, geometry: NativeColumnDotsGeometry) => void;
37
+ end: () => void;
38
+ cancel: () => void;
39
+ dispose: () => void;
40
+ };
41
+ export type NativeColumnDotsController = ReturnType<typeof createNativeColumnDots>;
@@ -0,0 +1,104 @@
1
+ /** Coordinate-driven interaction model for PanResponder, Gesture Handler or another native view. */
2
+ export function createNativeColumnDots(opts) {
3
+ const listeners = new Set();
4
+ const byKey = new Map(opts.state.columns.map(column => [column.key, column]));
5
+ let selected = null;
6
+ let drag = null;
7
+ function getSnapshot() {
8
+ const config = opts.state.api.getConfig();
9
+ return {
10
+ selected,
11
+ visible: opts.state.api.visibleKeys(),
12
+ sort: config.sort,
13
+ drag: drag ? { origin: drag.origin, shown: drag.shown, index: drag.index, removing: drag.removing } : null,
14
+ };
15
+ }
16
+ function emit() {
17
+ const snapshot = getSnapshot();
18
+ for (const listener of listeners)
19
+ listener(snapshot);
20
+ }
21
+ const stopState = opts.state.api.subscribe(emit);
22
+ const subscribe = (listener) => {
23
+ listeners.add(listener);
24
+ return function unsubscribe() { listeners.delete(listener); };
25
+ };
26
+ function select(key) {
27
+ selected = key && byKey.has(key) ? key : null;
28
+ emit();
29
+ }
30
+ function toggle(key) {
31
+ const config = opts.state.api.getConfig();
32
+ if (!byKey.has(key) || byKey.get(key)?.fixed)
33
+ return;
34
+ const isVisible = config.visible[key] != false;
35
+ if (isVisible && opts.state.api.visibleKeys().length <= 1)
36
+ return;
37
+ if (!isVisible && opts.state.api.visibleKeys().length >= (opts.max ?? 8))
38
+ return;
39
+ selected = !isVisible ? key : selected == key ? null : selected;
40
+ opts.state.api.show(key, !isVisible);
41
+ }
42
+ const reorder = (key, to) => opts.state.api.moveKey(key, to);
43
+ const toggleSelectedSort = () => { if (selected)
44
+ opts.state.api.toggleSort(selected); };
45
+ function begin(key, primary, cross) {
46
+ const config = opts.state.api.getConfig();
47
+ if (!byKey.has(key) || config.visible[key] == false)
48
+ return false;
49
+ drag = { origin: key, shown: key, startPrimary: primary, startCross: cross, moved: false,
50
+ index: config.order.indexOf(key), removing: false };
51
+ emit();
52
+ return true;
53
+ }
54
+ function move(primary, cross, geometry) {
55
+ if (!drag)
56
+ return;
57
+ const config = opts.state.api.getConfig();
58
+ const dx = primary - drag.startPrimary;
59
+ const dy = cross - drag.startCross;
60
+ if (!drag.moved && Math.hypot(dx, dy) < (opts.moveSlop ?? 4))
61
+ return;
62
+ drag.moved = true;
63
+ const last = Math.max(0, config.order.length - 1);
64
+ const relative = geometry.length > 0 ? (primary - geometry.start) / geometry.length : 0;
65
+ drag.index = Math.max(0, Math.min(last, Math.round(relative * last)));
66
+ const removeDelta = opts.removeDirection == 'up' ? -dy
67
+ : opts.removeDirection == 'down' ? dy : Math.abs(dy);
68
+ drag.removing = removeDelta > (opts.removeDistance ?? 32) && removeDelta > Math.abs(dx);
69
+ if (!drag.removing && !byKey.get(drag.origin)?.fixed) {
70
+ const target = config.order[drag.index];
71
+ if (target && target != drag.shown && config.visible[target] == false) {
72
+ opts.state.api.setConfig({ ...config, visible: { ...config.visible, [drag.shown]: false, [target]: true } });
73
+ drag.shown = target;
74
+ }
75
+ }
76
+ emit();
77
+ }
78
+ function end() {
79
+ const current = drag;
80
+ drag = null;
81
+ if (!current)
82
+ return;
83
+ if (!current.moved)
84
+ selected = current.origin;
85
+ else if (current.removing && !byKey.get(current.shown)?.fixed && opts.state.api.visibleKeys().length > 1) {
86
+ opts.state.api.show(current.shown, false);
87
+ if (selected == current.origin || selected == current.shown)
88
+ selected = null;
89
+ }
90
+ else if (current.shown != current.origin && selected == current.origin)
91
+ selected = current.shown;
92
+ emit();
93
+ }
94
+ function cancel() {
95
+ drag = null;
96
+ emit();
97
+ }
98
+ function dispose() {
99
+ stopState();
100
+ listeners.clear();
101
+ drag = null;
102
+ }
103
+ return { getSnapshot, subscribe, select, toggle, reorder, toggleSelectedSort, begin, move, end, cancel, dispose };
104
+ }
@@ -0,0 +1,69 @@
1
+ export type NativeColumnMeta = {
2
+ key: string;
3
+ title: string;
4
+ short?: string;
5
+ icon?: unknown;
6
+ group?: string;
7
+ fixed?: boolean;
8
+ defaultVisible?: boolean;
9
+ cardRole?: 'title' | 'accent';
10
+ };
11
+ export type NativeColumnsSort = {
12
+ key: string;
13
+ dir: 'asc' | 'desc';
14
+ };
15
+ export type NativeColumnsConfig = {
16
+ v: number;
17
+ order: string[];
18
+ visible: {
19
+ [key: string]: boolean;
20
+ };
21
+ width: {
22
+ [key: string]: number;
23
+ };
24
+ sort: NativeColumnsSort | null;
25
+ filter: {
26
+ [key: string]: unknown;
27
+ };
28
+ groups: {
29
+ [group: string]: string[];
30
+ };
31
+ };
32
+ export type NativeColumnStorage = {
33
+ getItem(key: string): Promise<string | null>;
34
+ setItem(key: string, value: string): Promise<unknown>;
35
+ removeItem?(key: string): Promise<unknown>;
36
+ };
37
+ export type NativeColumnStateError = {
38
+ phase: 'read' | 'parse' | 'write';
39
+ error: unknown;
40
+ };
41
+ /** Headless, platform-neutral column controller. AsyncStorage satisfies storage directly. */
42
+ export declare function createNativeColumnState(opts: {
43
+ key: string;
44
+ columns: readonly NativeColumnMeta[];
45
+ def?: Partial<NativeColumnsConfig>;
46
+ storage?: NativeColumnStorage;
47
+ saveMs?: number;
48
+ onError?: (event: NativeColumnStateError) => void;
49
+ }): {
50
+ columns: readonly NativeColumnMeta[];
51
+ ready: Promise<NativeColumnsConfig>;
52
+ api: {
53
+ getConfig: () => NativeColumnsConfig;
54
+ setConfig: (next: NativeColumnsConfig) => void;
55
+ subscribe: (listener: (config: NativeColumnsConfig) => void) => () => void;
56
+ show: (key: string, visible: boolean) => void;
57
+ move: (order: string[]) => void;
58
+ moveKey: (key: string, to: number) => void;
59
+ setSort: (sort: NativeColumnsSort | null) => void;
60
+ toggleSort: (key: string) => void;
61
+ setFilter: (key: string, value: unknown) => void;
62
+ setGroup: (group: string, keys: string[]) => void;
63
+ visibleKeys: () => string[];
64
+ reset: () => void;
65
+ flush: () => Promise<void>;
66
+ };
67
+ dispose: () => void;
68
+ };
69
+ export type NativeColumnStateController = ReturnType<typeof createNativeColumnState>;
@@ -0,0 +1,209 @@
1
+ const SCHEMA_V = 1;
2
+ function copy(config) {
3
+ return {
4
+ ...config,
5
+ order: config.order.slice(),
6
+ visible: { ...config.visible },
7
+ width: { ...config.width },
8
+ sort: config.sort ? { ...config.sort } : null,
9
+ filter: { ...config.filter },
10
+ groups: Object.fromEntries(Object.entries(config.groups).map(([group, keys]) => [group, keys.slice()])),
11
+ };
12
+ }
13
+ function pinFixed(order, columns) {
14
+ const fixed = new Set(columns.filter(column => column.fixed).map(column => column.key));
15
+ const result = order.filter(key => !fixed.has(key));
16
+ columns.forEach(function pin(column, index) {
17
+ if (column.fixed)
18
+ result.splice(Math.min(index, result.length), 0, column.key);
19
+ });
20
+ return result;
21
+ }
22
+ /** Headless, platform-neutral column controller. AsyncStorage satisfies storage directly. */
23
+ export function createNativeColumnState(opts) {
24
+ const columns = opts.columns.slice();
25
+ const byKey = new Map(columns.map(column => [column.key, column]));
26
+ const known = new Set(byKey.keys());
27
+ const groupKeys = [...new Set(columns.map(column => column.group).filter((group) => !!group))];
28
+ const listeners = new Set();
29
+ let revision = 0;
30
+ let hydrated = !opts.storage;
31
+ let disposed = false;
32
+ let timer;
33
+ let writes = Promise.resolve();
34
+ const members = (group) => columns.filter(column => column.group == group).map(column => column.key);
35
+ function defaults() {
36
+ return {
37
+ v: SCHEMA_V,
38
+ order: opts.def?.order?.slice() ?? columns.map(column => column.key),
39
+ visible: opts.def?.visible ? { ...opts.def.visible } : Object.fromEntries(columns.map(column => [column.key, column.defaultVisible != false])),
40
+ width: opts.def?.width ? { ...opts.def.width } : {},
41
+ sort: opts.def?.sort ? { ...opts.def.sort } : null,
42
+ filter: opts.def?.filter ? { ...opts.def.filter } : {},
43
+ groups: opts.def?.groups ? { ...opts.def.groups } : Object.fromEntries(groupKeys.map(group => [group, members(group)])),
44
+ };
45
+ }
46
+ function normalize(value) {
47
+ const base = defaults();
48
+ const order = (Array.isArray(value?.order) ? value.order : base.order)
49
+ .filter(key => known.has(key) && !byKey.get(key)?.fixed);
50
+ for (const column of columns)
51
+ if (!column.fixed && !order.includes(column.key))
52
+ order.push(column.key);
53
+ const rawVisible = value?.visible && typeof value.visible == 'object' ? value.visible : base.visible;
54
+ const visible = {};
55
+ for (const column of columns)
56
+ visible[column.key] = column.fixed ? true : (rawVisible[column.key] ?? column.defaultVisible != false);
57
+ const width = {};
58
+ for (const [key, item] of Object.entries(value?.width ?? {}))
59
+ if (known.has(key) && typeof item == 'number' && isFinite(item) && item > 0)
60
+ width[key] = item;
61
+ const sort = value?.sort && known.has(value.sort.key) && (value.sort.dir == 'asc' || value.sort.dir == 'desc')
62
+ ? { key: value.sort.key, dir: value.sort.dir } : null;
63
+ const filter = {};
64
+ for (const [key, item] of Object.entries(value?.filter ?? {}))
65
+ if (known.has(key))
66
+ filter[key] = item;
67
+ const groups = {};
68
+ for (const group of groupKeys) {
69
+ const allowed = members(group);
70
+ const raw = value?.groups?.[group];
71
+ groups[group] = Array.isArray(raw) ? raw.filter(key => allowed.includes(key)) : allowed;
72
+ }
73
+ return { v: SCHEMA_V, order: pinFixed(order, columns), visible, width, sort, filter, groups };
74
+ }
75
+ let config = normalize(defaults());
76
+ const emit = () => {
77
+ const snapshot = copy(config);
78
+ for (const listener of listeners)
79
+ listener(snapshot);
80
+ };
81
+ const report = (phase, error) => opts.onError?.({ phase, error });
82
+ function enqueue(snapshot) {
83
+ if (!opts.storage || disposed)
84
+ return;
85
+ writes = writes.then(async function save() {
86
+ try {
87
+ await opts.storage.setItem(opts.key, JSON.stringify(snapshot));
88
+ }
89
+ catch (error) {
90
+ report('write', error);
91
+ }
92
+ });
93
+ }
94
+ function schedule() {
95
+ if (!opts.storage || !hydrated || disposed)
96
+ return;
97
+ clearTimeout(timer);
98
+ timer = setTimeout(function saveLater() {
99
+ timer = undefined;
100
+ enqueue(copy(config));
101
+ }, opts.saveMs ?? 100);
102
+ }
103
+ function commit(next) {
104
+ if (disposed)
105
+ return;
106
+ config = normalize(next);
107
+ revision++;
108
+ emit();
109
+ schedule();
110
+ }
111
+ const ready = (async function hydrate() {
112
+ if (!opts.storage)
113
+ return copy(config);
114
+ const before = revision;
115
+ try {
116
+ const raw = await opts.storage.getItem(opts.key);
117
+ if (raw != null && revision == before) {
118
+ try {
119
+ config = normalize(JSON.parse(raw));
120
+ emit();
121
+ }
122
+ catch (error) {
123
+ report('parse', error);
124
+ }
125
+ }
126
+ }
127
+ catch (error) {
128
+ report('read', error);
129
+ }
130
+ finally {
131
+ hydrated = true;
132
+ if (revision != before)
133
+ schedule();
134
+ }
135
+ return copy(config);
136
+ })();
137
+ const getConfig = () => copy(config);
138
+ const subscribe = (listener) => {
139
+ listeners.add(listener);
140
+ return function unsubscribe() { listeners.delete(listener); };
141
+ };
142
+ function show(key, visible) {
143
+ if (!known.has(key) || byKey.get(key)?.fixed)
144
+ return;
145
+ commit({ ...config, visible: { ...config.visible, [key]: visible } });
146
+ }
147
+ function move(order) { commit({ ...config, order }); }
148
+ function moveKey(key, to) {
149
+ if (!known.has(key) || byKey.get(key)?.fixed)
150
+ return;
151
+ const order = config.order.slice();
152
+ const from = order.indexOf(key);
153
+ if (from == -1)
154
+ return;
155
+ order.splice(from, 1);
156
+ order.splice(Math.max(0, Math.min(order.length, to)), 0, key);
157
+ move(order);
158
+ }
159
+ function setSort(sort) { commit({ ...config, sort }); }
160
+ function toggleSort(key) {
161
+ if (!known.has(key))
162
+ return;
163
+ const current = config.sort;
164
+ setSort(current?.key != key ? { key, dir: 'asc' } : current.dir == 'asc' ? { key, dir: 'desc' } : null);
165
+ }
166
+ function setFilter(key, value) {
167
+ if (!known.has(key))
168
+ return;
169
+ const filter = { ...config.filter };
170
+ if (value == null)
171
+ delete filter[key];
172
+ else
173
+ filter[key] = value;
174
+ commit({ ...config, filter });
175
+ }
176
+ function setGroup(group, keys) {
177
+ if (!groupKeys.includes(group))
178
+ return;
179
+ commit({ ...config, groups: { ...config.groups, [group]: keys } });
180
+ }
181
+ function visibleKeys() {
182
+ return config.order.filter(key => {
183
+ if (config.visible[key] == false)
184
+ return false;
185
+ const group = byKey.get(key)?.group;
186
+ return !group || config.groups[group]?.includes(key);
187
+ });
188
+ }
189
+ function reset() { commit(defaults()); }
190
+ async function flush() {
191
+ clearTimeout(timer);
192
+ timer = undefined;
193
+ await ready;
194
+ enqueue(copy(config));
195
+ await writes;
196
+ }
197
+ function dispose() {
198
+ disposed = true;
199
+ clearTimeout(timer);
200
+ listeners.clear();
201
+ }
202
+ return {
203
+ columns: columns,
204
+ ready,
205
+ api: { getConfig, setConfig: (next) => commit(next), subscribe, show, move, moveKey,
206
+ setSort, toggleSort, setFilter, setGroup, visibleKeys, reset, flush },
207
+ dispose,
208
+ };
209
+ }
@@ -0,0 +1,3 @@
1
+ /** DOM/CSS/ag-grid/react-dom-free React Native entrypoint. */
2
+ export * from './columnState.js';
3
+ export * from './columnDots.js';
@@ -0,0 +1,3 @@
1
+ /** DOM/CSS/ag-grid/react-dom-free React Native entrypoint. */
2
+ export * from './columnState.js';
3
+ export * from './columnDots.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wenay-react2",
3
- "version": "1.0.46",
3
+ "version": "1.0.47",
4
4
  "description": "Common react",
5
5
  "strict": true,
6
6
  "main": "dist/index.js",
@@ -45,6 +45,7 @@
45
45
  },
46
46
  "exports": {
47
47
  ".": "./lib/index.js",
48
+ "./native": "./lib/native/index.js",
48
49
  "./package.json": "./package.json",
49
50
  "./demo/peer-media": "./lib/common/demo/peerMedia.js",
50
51
  "./demo/stand": "./lib/common/testUseReact/qa.js"