wenay-react2 1.0.39 → 1.0.41
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/doc/EXAMPLE_USAGE.md +915 -0
- package/doc/PROJECT_FUNCTIONALITY.md +401 -0
- package/doc/PROJECT_RULES.md +80 -0
- package/doc/WENAY_REACT2_RENAMES.md +177 -0
- package/doc/changes/v1.0.35.md +26 -0
- package/doc/changes/v1.0.37.md +13 -0
- package/doc/changes/v1.0.38.md +48 -0
- package/doc/changes/v1.0.39.md +35 -0
- package/doc/changes/v1.0.40.md +15 -0
- package/doc/changes/v1.0.41.md +13 -0
- package/doc/progress/README.md +14 -0
- package/doc/progress/hook-controller-opportunities.md +367 -0
- package/doc/progress/hook-extraction-audit.md +195 -0
- package/doc/progress/public-surface-normalization.md +368 -0
- package/doc/progress/style-system-normalization.md +121 -0
- package/doc/target/README.md +33 -0
- package/doc/target/my.md +109 -0
- package/doc/wenay-react2-1.0.8.tgz +0 -0
- package/doc/wenay-react2-rare.md +702 -0
- package/doc/wenay-react2.md +475 -0
- package/lib/common/src/components/Dnd/FloatingWindow.d.ts +29 -12
- package/lib/common/src/components/Dnd/FloatingWindow.js +100 -91
- package/lib/common/src/components/Menu/RightMenu.d.ts +28 -1
- package/lib/common/src/components/Menu/RightMenu.js +62 -35
- package/lib/common/src/components/Modal/LeftModal.js +5 -4
- package/lib/common/src/components/Modal/Modal.js +8 -7
- package/lib/common/src/components/ParamsEditor.d.ts +15 -0
- package/lib/common/src/components/ParamsEditor.js +45 -18
- package/lib/common/src/components/Settings/SettingsDialog.d.ts +68 -6
- package/lib/common/src/components/Settings/SettingsDialog.js +61 -12
- package/lib/common/src/components/Toolbar/Toolbar.js +64 -19
- package/lib/common/src/components/UiSlot/UiSlot.js +5 -4
- package/lib/common/src/grid/columnState/columnState.js +8 -6
- package/lib/common/src/hooks/useKeyboard.js +4 -3
- package/lib/common/src/logs/logs.d.ts +40 -133
- package/lib/common/src/logs/logs.js +96 -66
- package/lib/common/src/logs/logsContext.d.ts +26 -0
- package/lib/common/src/logs/logsContext.js +25 -20
- package/lib/common/src/logs/logsController.d.ts +88 -0
- package/lib/common/src/logs/logsController.js +54 -0
- package/lib/common/src/logs/miniLogs.d.ts +69 -3
- package/lib/common/src/logs/miniLogs.js +70 -15
- package/lib/common/src/menu/menu.d.ts +15 -3
- package/lib/common/src/menu/menu.js +81 -17
- package/lib/common/src/menu/menuMouse.d.ts +18 -0
- package/lib/common/src/menu/menuMouse.js +43 -1
- package/lib/common/src/styles/tokens.d.ts +7 -6
- package/lib/common/src/styles/tokens.js +8 -5
- package/lib/common/src/utils/memoryStore.d.ts +1 -10
- package/lib/common/src/utils/searchHistory.js +4 -3
- package/lib/style/menuRight.css +29 -23
- package/lib/style/style.css +15 -9
- package/lib/style/tokens.css +8 -4
- package/package.json +50 -49
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { deepCloneMutable, isDate, TF, timeLocalToStr_yyyymmdd, timeLocalToStr_yyyymmdd_hhmm, timeLocalToStr_yyyymmdd_hhmmss, timeLocalToStr_yyyymmdd_hhmmss_ms } from "wenay-common2";
|
|
4
4
|
import { setResizeableElement, removeResizeableElement } from "./MyResizeObserver";
|
|
5
5
|
import { ParamRow, ParamToggleLabel } from "./Parameters";
|
|
@@ -163,6 +163,44 @@ function InputListArray(set, values, range, rangeLabels) {
|
|
|
163
163
|
return _jsx("option", { value: String(a), label: rangeLabels?.at(i) ?? String(a), children: a }, String(a));
|
|
164
164
|
}) });
|
|
165
165
|
}
|
|
166
|
+
export function useParamsEditorController({ params, onChange, onExpand }) {
|
|
167
|
+
const [, setUpdate] = useState(0);
|
|
168
|
+
const cloned = useMemo(() => deepCloneMutable(params), [params]);
|
|
169
|
+
const paramsRef = useRef(cloned);
|
|
170
|
+
if (paramsRef.current !== cloned)
|
|
171
|
+
paramsRef.current = cloned;
|
|
172
|
+
const callbacks = useRef({ onChange, onExpand });
|
|
173
|
+
callbacks.current = { onChange, onExpand };
|
|
174
|
+
const timeoutId = useRef(null);
|
|
175
|
+
useEffect(() => () => {
|
|
176
|
+
if (timeoutId.current != null)
|
|
177
|
+
clearTimeout(timeoutId.current);
|
|
178
|
+
}, []);
|
|
179
|
+
const refresh = useCallback(() => {
|
|
180
|
+
setUpdate(e => e + 1);
|
|
181
|
+
}, []);
|
|
182
|
+
const notifyChange = useCallback(() => {
|
|
183
|
+
callbacks.current.onChange(paramsRef.current);
|
|
184
|
+
}, []);
|
|
185
|
+
const notifyChangeDelayed = useCallback(() => {
|
|
186
|
+
if (timeoutId.current != null)
|
|
187
|
+
clearTimeout(timeoutId.current);
|
|
188
|
+
timeoutId.current = setTimeout(() => {
|
|
189
|
+
notifyChange();
|
|
190
|
+
}, 200);
|
|
191
|
+
}, [notifyChange]);
|
|
192
|
+
const notifyExpand = useCallback(() => {
|
|
193
|
+
callbacks.current.onExpand?.(paramsRef.current);
|
|
194
|
+
}, []);
|
|
195
|
+
return {
|
|
196
|
+
paramsRef,
|
|
197
|
+
params: paramsRef.current,
|
|
198
|
+
refresh,
|
|
199
|
+
notifyChange,
|
|
200
|
+
notifyChangeDelayed,
|
|
201
|
+
notifyExpand,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
166
204
|
export function ParamsEditor(data) {
|
|
167
205
|
// callbacks live in a ref: useMemo below intentionally freezes the element until
|
|
168
206
|
// params identity changes, and would otherwise freeze a stale onChange/onExpand with it
|
|
@@ -174,26 +212,15 @@ export function ParamsEditor(data) {
|
|
|
174
212
|
return result;
|
|
175
213
|
}
|
|
176
214
|
function ParamsEditorBase(data) {
|
|
177
|
-
const
|
|
178
|
-
|
|
215
|
+
const controller = useParamsEditorController(data);
|
|
216
|
+
const myParams = controller.paramsRef;
|
|
217
|
+
function Refresh() { controller.refresh(); }
|
|
179
218
|
const styleColorDisable = "rgb(146,146,146)";
|
|
180
|
-
const p = useMemo(() => deepCloneMutable(data.params), [data.params]);
|
|
181
|
-
const myParams = useRef(p);
|
|
182
|
-
// sync during render: the effect+Refresh variant committed a frame of stale params first
|
|
183
|
-
if (myParams.current !== p)
|
|
184
|
-
myParams.current = p;
|
|
185
|
-
const timeoutId = useRef(null);
|
|
186
|
-
useEffect(() => () => { if (timeoutId.current != null)
|
|
187
|
-
clearTimeout(timeoutId.current); }, []);
|
|
188
219
|
function refreshIndicator() {
|
|
189
|
-
|
|
220
|
+
controller.notifyChange();
|
|
190
221
|
}
|
|
191
222
|
function refreshIndicatorDelayed() {
|
|
192
|
-
|
|
193
|
-
clearTimeout(timeoutId.current);
|
|
194
|
-
timeoutId.current = setTimeout(() => {
|
|
195
|
-
refreshIndicator();
|
|
196
|
-
}, 200);
|
|
223
|
+
controller.notifyChangeDelayed();
|
|
197
224
|
}
|
|
198
225
|
function Param(//<T extends boolean|number|string|const_Date>(
|
|
199
226
|
set, val, type, range, labels) {
|
|
@@ -218,7 +245,7 @@ function ParamsEditorBase(data) {
|
|
|
218
245
|
refreshIndicator(); // process without delay
|
|
219
246
|
}
|
|
220
247
|
function onExpandA(param, flag) {
|
|
221
|
-
|
|
248
|
+
controller.notifyExpand();
|
|
222
249
|
}
|
|
223
250
|
function ListParams(obj, parentEnabled = true, expandLevel = 0) {
|
|
224
251
|
return Object.entries(obj).map(([key, param]) => {
|
|
@@ -13,20 +13,82 @@ export type SettingsSection = {
|
|
|
13
13
|
/** Semantic aliases: commands, domain terms, translated labels, etc. */
|
|
14
14
|
keywords?: readonly string[];
|
|
15
15
|
};
|
|
16
|
+
type SettingsTreeNode = {
|
|
17
|
+
section: SettingsSection;
|
|
18
|
+
children: SettingsTreeNode[];
|
|
19
|
+
parent?: SettingsTreeNode;
|
|
20
|
+
depth: number;
|
|
21
|
+
};
|
|
22
|
+
type SettingsTree = {
|
|
23
|
+
roots: SettingsTreeNode[];
|
|
24
|
+
ordered: SettingsTreeNode[];
|
|
25
|
+
byKey: Map<string, SettingsTreeNode>;
|
|
26
|
+
};
|
|
27
|
+
type SettingsTreeFilter = {
|
|
28
|
+
visibleKeys: Set<string>;
|
|
29
|
+
matchedKeys: Set<string>;
|
|
30
|
+
expandedKeys: Set<string>;
|
|
31
|
+
};
|
|
32
|
+
type SettingsSearchTerm = {
|
|
33
|
+
variants: string[];
|
|
34
|
+
};
|
|
16
35
|
/** Register an external section. Re-register with the same key replaces the previous one.
|
|
17
36
|
* The returned function removes exactly this registration (a no-op if it was replaced). */
|
|
18
37
|
export declare function registerSettingsSection(s: SettingsSection): () => void;
|
|
19
38
|
/** Current external sections (static props sections are not included). */
|
|
20
39
|
export declare function getSettingsSections(): readonly SettingsSection[];
|
|
21
|
-
|
|
22
|
-
* Sections = props.sections (first) + everything from registerSettingsSection.
|
|
23
|
-
* Flat {key, name, render} sections remain valid; use children/parentKey for hierarchy.
|
|
24
|
-
* Look is themed via --dlg-* CSS variables (dark defaults), same contract as --wnd-*. */
|
|
25
|
-
export declare function SettingsDialog(props: {
|
|
40
|
+
export type SettingsDialogProps = {
|
|
26
41
|
trigger?: React.ReactNode;
|
|
27
42
|
sections?: SettingsSection[];
|
|
28
43
|
defaultSection?: string;
|
|
29
44
|
/** Section buttons: apps pass their own .chip / .chipActive; defaults are minimal library styles */
|
|
30
45
|
sectionClassName?: string;
|
|
31
46
|
sectionActiveClassName?: string;
|
|
32
|
-
}
|
|
47
|
+
};
|
|
48
|
+
export type SettingsDialogTreeToolState = "collapsed" | "expanded" | "branch";
|
|
49
|
+
export type SettingsDialogController = {
|
|
50
|
+
open: boolean;
|
|
51
|
+
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
52
|
+
active?: string;
|
|
53
|
+
setActive: React.Dispatch<React.SetStateAction<string | undefined>>;
|
|
54
|
+
search: string;
|
|
55
|
+
setSearch: React.Dispatch<React.SetStateAction<string>>;
|
|
56
|
+
expanded: Set<string>;
|
|
57
|
+
historyOpen: boolean;
|
|
58
|
+
setHistoryOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
|
59
|
+
navWidth: number;
|
|
60
|
+
navResizing: boolean;
|
|
61
|
+
searchInputRef: React.RefObject<HTMLInputElement | null>;
|
|
62
|
+
searchBoxRef: React.RefObject<HTMLDivElement | null>;
|
|
63
|
+
tree: SettingsTree;
|
|
64
|
+
filtered: SettingsTreeFilter;
|
|
65
|
+
current?: SettingsSection;
|
|
66
|
+
currentNode?: SettingsTreeNode;
|
|
67
|
+
firstVisibleNode?: SettingsTreeNode;
|
|
68
|
+
searchTerms: SettingsSearchTerm[];
|
|
69
|
+
searchHistory: readonly string[];
|
|
70
|
+
base: string;
|
|
71
|
+
activeCls: string;
|
|
72
|
+
treeToolsVisible: boolean;
|
|
73
|
+
treeToolState: SettingsDialogTreeToolState;
|
|
74
|
+
treeToolTitle: string;
|
|
75
|
+
openDialog(): void;
|
|
76
|
+
closeDialog(): void;
|
|
77
|
+
onTriggerKeyDown(e: React.KeyboardEvent<HTMLSpanElement>): void;
|
|
78
|
+
toggleExpanded(key: string): void;
|
|
79
|
+
commitSearch(value?: string): void;
|
|
80
|
+
useHistoryItem(value: string): void;
|
|
81
|
+
clearHistory(): void;
|
|
82
|
+
commitNavWidth(value: number): void;
|
|
83
|
+
beginNavResize(e: React.PointerEvent<HTMLDivElement>): void;
|
|
84
|
+
onNavResizeKeyDown(e: React.KeyboardEvent<HTMLDivElement>): void;
|
|
85
|
+
closeHistoryWhenSearchFocusLeaves(e: React.FocusEvent<HTMLDivElement>): void;
|
|
86
|
+
cycleTreeTool(): void;
|
|
87
|
+
};
|
|
88
|
+
export declare function useSettingsDialogController(props: SettingsDialogProps): SettingsDialogController;
|
|
89
|
+
/** Centered settings dialog with a searchable JetBrains-style settings tree on the left.
|
|
90
|
+
* Sections = props.sections (first) + everything from registerSettingsSection.
|
|
91
|
+
* Flat {key, name, render} sections remain valid; use children/parentKey for hierarchy.
|
|
92
|
+
* Look is themed via --dlg-* CSS variables (dark defaults), same contract as --wnd-*. */
|
|
93
|
+
export declare function SettingsDialog(props: SettingsDialogProps): import("react/jsx-runtime").JSX.Element;
|
|
94
|
+
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import React, { useEffect, useRef, useState } from "react";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
|
-
import {
|
|
4
|
+
import { createUpdateApi, renderBy } from "../../../updateBy";
|
|
5
5
|
import { OutsideClickArea } from "../../hooks/useOutside";
|
|
6
6
|
import { FloatingWindowBase } from "../Dnd/FloatingWindow";
|
|
7
7
|
import { createSearchHistory } from "../../utils/searchHistory";
|
|
@@ -12,6 +12,7 @@ const settingsDialogNav = { min: 160, def: 220, max: 360 };
|
|
|
12
12
|
// Module singleton (closure + updateBy subscription, no React context): any module
|
|
13
13
|
// registers a section on mount and removes it on unmount; the dialog re-renders on changes.
|
|
14
14
|
const registry = { list: [] };
|
|
15
|
+
const registryApi = createUpdateApi(registry);
|
|
15
16
|
const settingsSearchHistory = createSearchHistory({ key: "SettingsDialog.searchHistory", max: 8 });
|
|
16
17
|
const settingsDialogLayout = memoryGetOrCreate("SettingsDialog.layout", { navWidth: settingsDialogNav.def });
|
|
17
18
|
/** Register an external section. Re-register with the same key replaces the previous one.
|
|
@@ -22,12 +23,12 @@ export function registerSettingsSection(s) {
|
|
|
22
23
|
registry.list.push(s);
|
|
23
24
|
else
|
|
24
25
|
registry.list.splice(i, 1, s);
|
|
25
|
-
|
|
26
|
+
registryApi.render();
|
|
26
27
|
return () => {
|
|
27
28
|
const j = registry.list.indexOf(s);
|
|
28
29
|
if (j != -1) {
|
|
29
30
|
registry.list.splice(j, 1);
|
|
30
|
-
|
|
31
|
+
registryApi.render();
|
|
31
32
|
}
|
|
32
33
|
};
|
|
33
34
|
}
|
|
@@ -220,11 +221,7 @@ function filterSettingsTree(roots, terms) {
|
|
|
220
221
|
roots.forEach(visit);
|
|
221
222
|
return { visibleKeys, matchedKeys, expandedKeys };
|
|
222
223
|
}
|
|
223
|
-
|
|
224
|
-
* Sections = props.sections (first) + everything from registerSettingsSection.
|
|
225
|
-
* Flat {key, name, render} sections remain valid; use children/parentKey for hierarchy.
|
|
226
|
-
* Look is themed via --dlg-* CSS variables (dark defaults), same contract as --wnd-*. */
|
|
227
|
-
export function SettingsDialog(props) {
|
|
224
|
+
export function useSettingsDialogController(props) {
|
|
228
225
|
const [open, setOpen] = useState(false);
|
|
229
226
|
const [active, setActive] = useState(props.defaultSection);
|
|
230
227
|
const [search, setSearch] = useState("");
|
|
@@ -236,7 +233,7 @@ export function SettingsDialog(props) {
|
|
|
236
233
|
const searchBoxRef = useRef(null);
|
|
237
234
|
const navResizeRef = useRef(null);
|
|
238
235
|
const navWidthRef = useRef(navWidth);
|
|
239
|
-
|
|
236
|
+
registryApi.use();
|
|
240
237
|
const searchHistory = settingsSearchHistory.use();
|
|
241
238
|
const sections = [...(props.sections ?? []), ...registry.list];
|
|
242
239
|
const tree = buildSettingsTree(sections);
|
|
@@ -310,6 +307,9 @@ export function SettingsDialog(props) {
|
|
|
310
307
|
setHistoryOpen(false);
|
|
311
308
|
setOpen(true);
|
|
312
309
|
}
|
|
310
|
+
function closeDialog() {
|
|
311
|
+
setOpen(false);
|
|
312
|
+
}
|
|
313
313
|
function onTriggerKeyDown(e) {
|
|
314
314
|
if (e.key != "Enter" && e.key != " ")
|
|
315
315
|
return;
|
|
@@ -380,6 +380,11 @@ export function SettingsDialog(props) {
|
|
|
380
380
|
setHistoryOpen(false);
|
|
381
381
|
searchInputRef.current?.focus();
|
|
382
382
|
}
|
|
383
|
+
function clearHistory() {
|
|
384
|
+
settingsSearchHistory.clear();
|
|
385
|
+
setHistoryOpen(false);
|
|
386
|
+
searchInputRef.current?.focus();
|
|
387
|
+
}
|
|
383
388
|
function closeHistoryWhenSearchFocusLeaves(e) {
|
|
384
389
|
const next = e.relatedTarget;
|
|
385
390
|
if (next && e.currentTarget.contains(next))
|
|
@@ -398,6 +403,52 @@ export function SettingsDialog(props) {
|
|
|
398
403
|
else
|
|
399
404
|
expandAll();
|
|
400
405
|
}
|
|
406
|
+
return {
|
|
407
|
+
open,
|
|
408
|
+
setOpen,
|
|
409
|
+
active,
|
|
410
|
+
setActive,
|
|
411
|
+
search,
|
|
412
|
+
setSearch,
|
|
413
|
+
expanded,
|
|
414
|
+
historyOpen,
|
|
415
|
+
setHistoryOpen,
|
|
416
|
+
navWidth,
|
|
417
|
+
navResizing,
|
|
418
|
+
searchInputRef,
|
|
419
|
+
searchBoxRef,
|
|
420
|
+
tree,
|
|
421
|
+
filtered,
|
|
422
|
+
current,
|
|
423
|
+
currentNode,
|
|
424
|
+
firstVisibleNode,
|
|
425
|
+
searchTerms,
|
|
426
|
+
searchHistory,
|
|
427
|
+
base,
|
|
428
|
+
activeCls,
|
|
429
|
+
treeToolsVisible,
|
|
430
|
+
treeToolState,
|
|
431
|
+
treeToolTitle,
|
|
432
|
+
openDialog,
|
|
433
|
+
closeDialog,
|
|
434
|
+
onTriggerKeyDown,
|
|
435
|
+
toggleExpanded,
|
|
436
|
+
commitSearch,
|
|
437
|
+
useHistoryItem,
|
|
438
|
+
clearHistory,
|
|
439
|
+
commitNavWidth,
|
|
440
|
+
beginNavResize,
|
|
441
|
+
onNavResizeKeyDown,
|
|
442
|
+
closeHistoryWhenSearchFocusLeaves,
|
|
443
|
+
cycleTreeTool,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
/** Centered settings dialog with a searchable JetBrains-style settings tree on the left.
|
|
447
|
+
* Sections = props.sections (first) + everything from registerSettingsSection.
|
|
448
|
+
* Flat {key, name, render} sections remain valid; use children/parentKey for hierarchy.
|
|
449
|
+
* Look is themed via --dlg-* CSS variables (dark defaults), same contract as --wnd-*. */
|
|
450
|
+
export function SettingsDialog(props) {
|
|
451
|
+
const { open, setOpen, setActive, search, setSearch, expanded, historyOpen, setHistoryOpen, navWidth, navResizing, searchInputRef, searchBoxRef, tree, filtered, current, firstVisibleNode, searchTerms, searchHistory, base, activeCls, treeToolsVisible, treeToolState, treeToolTitle, openDialog, onTriggerKeyDown, toggleExpanded, commitSearch, useHistoryItem, clearHistory, commitNavWidth, beginNavResize, onNavResizeKeyDown, closeHistoryWhenSearchFocusLeaves, cycleTreeTool, } = useSettingsDialogController(props);
|
|
401
452
|
function renderTreeNode(node) {
|
|
402
453
|
if (!filtered.visibleKeys.has(node.section.key))
|
|
403
454
|
return null;
|
|
@@ -444,8 +495,6 @@ export function SettingsDialog(props) {
|
|
|
444
495
|
setHistoryOpen(searchHistory.length != 0);
|
|
445
496
|
searchInputRef.current?.focus();
|
|
446
497
|
}, children: _jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", "aria-hidden": "true", children: _jsx("path", { d: "M2 2 L10 10 M10 2 L2 10", stroke: "currentColor", strokeWidth: "1.6", strokeLinecap: "round" }) }) }), historyOpen && searchHistory.length != 0 && _jsxs("div", { className: "wenayDlgSearchHistory", role: "listbox", children: [searchHistory.map(item => _jsx("button", { type: "button", className: "wenayDlgSearchHistoryItem", onMouseDown: e => e.preventDefault(), onClick: () => useHistoryItem(item), children: item }, item)), _jsx("button", { type: "button", className: "wenayDlgSearchHistoryClear", onMouseDown: e => e.preventDefault(), onClick: () => {
|
|
447
|
-
|
|
448
|
-
setHistoryOpen(false);
|
|
449
|
-
searchInputRef.current?.focus();
|
|
498
|
+
clearHistory();
|
|
450
499
|
}, children: "Clear history" })] })] }), treeToolsVisible && _jsx("button", { className: classNames(["wenayDlgTreeTool", `wenayDlgTreeTool_${treeToolState}`]), type: "button", title: treeToolTitle, "aria-label": treeToolTitle, onClick: cycleTreeTool, children: _jsxs("span", { className: "wenayDlgTreeToolDots", "aria-hidden": "true", children: [_jsx("span", {}), _jsx("span", {}), _jsx("span", {})] }) })] }), _jsxs("div", { className: "wenayDlgTree", role: "tree", children: [tree.roots.map(renderTreeNode), firstVisibleNode == null && _jsx("div", { className: "wenayDlgNoResults", children: "No settings found" })] })] }), _jsx("div", { className: "wenayDlgDivider", role: "separator", "aria-orientation": "vertical", "aria-label": "Resize settings navigation", "aria-valuemin": settingsDialogNav.min, "aria-valuemax": settingsDialogNav.max, "aria-valuenow": navWidth, tabIndex: 0, title: "Drag to resize navigation; double-click to reset", onPointerDown: beginNavResize, onDoubleClick: () => commitNavWidth(settingsDialogNav.def), onKeyDown: onNavResizeKeyDown, children: _jsx("span", {}) }), _jsx("div", { className: "wenayDlgContent", children: current?.render() })] }) }) }) }), document.body)] });
|
|
451
500
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useState } from 'react';
|
|
2
|
+
import { useLayoutEffect, useRef, useState } from 'react';
|
|
3
3
|
import { listen as createListen } from 'wenay-common2';
|
|
4
|
-
import {
|
|
4
|
+
import { createUpdateApi } from '../../../updateBy';
|
|
5
5
|
import { memoryGetOrCreate, memoryMarkDirty } from '../../utils/memoryStore';
|
|
6
6
|
import { OutsideClickArea } from '../../hooks/useOutside';
|
|
7
7
|
import { useReorder } from '../../hooks/useReorder';
|
|
@@ -12,6 +12,7 @@ const densities = { list: [
|
|
|
12
12
|
{ key: 'icon', name: 'Icons', renderItem: itemIconOnly },
|
|
13
13
|
{ key: 'label', name: 'Icons + labels' },
|
|
14
14
|
] };
|
|
15
|
+
const densitiesApi = createUpdateApi(densities);
|
|
15
16
|
/** Icon with a text fallback: no glyph = the first letters of short/title
|
|
16
17
|
* (an "PR"-style pseudo-icon), so icon densities work for icon-less items. */
|
|
17
18
|
export function toolbarItemIcon(item) {
|
|
@@ -30,12 +31,12 @@ export function registerToolbarDensity(d) {
|
|
|
30
31
|
densities.list.push(d);
|
|
31
32
|
else
|
|
32
33
|
densities.list.splice(i, 1, d);
|
|
33
|
-
|
|
34
|
+
densitiesApi.render();
|
|
34
35
|
return function offToolbarDensity() {
|
|
35
36
|
const j = densities.list.indexOf(d);
|
|
36
37
|
if (j != -1) {
|
|
37
38
|
densities.list.splice(j, 1);
|
|
38
|
-
|
|
39
|
+
densitiesApi.render();
|
|
39
40
|
}
|
|
40
41
|
};
|
|
41
42
|
}
|
|
@@ -55,6 +56,49 @@ function itemContent(item, densityKey) {
|
|
|
55
56
|
* order (the gear always sits at the bar edge), but toggleable like an item. */
|
|
56
57
|
const SETTINGS_KEY = '__settings';
|
|
57
58
|
const RESET_KEY = '__reset';
|
|
59
|
+
function useToolbarFlip(layoutKey) {
|
|
60
|
+
const itemRefs = useRef(new Map());
|
|
61
|
+
const prevRects = useRef(new Map());
|
|
62
|
+
const rafs = useRef([]);
|
|
63
|
+
useLayoutEffect(() => {
|
|
64
|
+
const prev = prevRects.current;
|
|
65
|
+
const next = new Map();
|
|
66
|
+
rafs.current.forEach(cancelAnimationFrame);
|
|
67
|
+
rafs.current = [];
|
|
68
|
+
itemRefs.current.forEach((node, key) => {
|
|
69
|
+
const rect = node.getBoundingClientRect();
|
|
70
|
+
next.set(key, { left: rect.left, top: rect.top });
|
|
71
|
+
const old = prev.get(key);
|
|
72
|
+
if (old == null)
|
|
73
|
+
return;
|
|
74
|
+
const dx = old.left - rect.left;
|
|
75
|
+
const dy = old.top - rect.top;
|
|
76
|
+
if (Math.abs(dx) < 0.5 && Math.abs(dy) < 0.5)
|
|
77
|
+
return;
|
|
78
|
+
node.style.transition = 'none';
|
|
79
|
+
node.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
80
|
+
node.getBoundingClientRect();
|
|
81
|
+
const raf = requestAnimationFrame(() => {
|
|
82
|
+
node.style.transition = 'transform 180ms ease';
|
|
83
|
+
node.style.transform = '';
|
|
84
|
+
});
|
|
85
|
+
rafs.current.push(raf);
|
|
86
|
+
});
|
|
87
|
+
prevRects.current = next;
|
|
88
|
+
return () => {
|
|
89
|
+
rafs.current.forEach(cancelAnimationFrame);
|
|
90
|
+
rafs.current = [];
|
|
91
|
+
};
|
|
92
|
+
}, [layoutKey]);
|
|
93
|
+
return function bind(key) {
|
|
94
|
+
return (node) => {
|
|
95
|
+
if (node)
|
|
96
|
+
itemRefs.current.set(key, node);
|
|
97
|
+
else
|
|
98
|
+
itemRefs.current.delete(key);
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
}
|
|
58
102
|
export function createToolbar(opts) {
|
|
59
103
|
const resetDefaultVisible = () => opts.resetItem !== false && opts.resetItem?.defaultVisible === true;
|
|
60
104
|
const defConfig = () => ({
|
|
@@ -63,6 +107,7 @@ export function createToolbar(opts) {
|
|
|
63
107
|
density: opts.def?.density ?? densities.list[0].key,
|
|
64
108
|
});
|
|
65
109
|
const st = memoryGetOrCreate(opts.key, defConfig());
|
|
110
|
+
const stApi = createUpdateApi(st);
|
|
66
111
|
const [emitChange, onChange] = createListen();
|
|
67
112
|
// ext is fixed for the controller's lifetime, so hook call order inside
|
|
68
113
|
// useConfig/Bar/Settings never changes for a given toolbar instance
|
|
@@ -137,7 +182,7 @@ export function createToolbar(opts) {
|
|
|
137
182
|
[RESET_KEY]: metaVisible(next, RESET_KEY, resetDefaultVisible()),
|
|
138
183
|
};
|
|
139
184
|
st.density = next.density;
|
|
140
|
-
|
|
185
|
+
stApi.render();
|
|
141
186
|
memoryMarkDirty(opts.key);
|
|
142
187
|
ext.setConfig({ order: next.order.slice(), visible });
|
|
143
188
|
// the source's own onChange already re-emitted; emit only when it can't
|
|
@@ -153,7 +198,7 @@ export function createToolbar(opts) {
|
|
|
153
198
|
st.order = next.order.slice();
|
|
154
199
|
st.visible = { ...next.visible };
|
|
155
200
|
st.density = next.density;
|
|
156
|
-
|
|
201
|
+
stApi.render();
|
|
157
202
|
memoryMarkDirty(opts.key);
|
|
158
203
|
const orderChanged = !sameOrder(curSourceOrder, nextSourceOrder);
|
|
159
204
|
if (orderChanged)
|
|
@@ -168,7 +213,7 @@ export function createToolbar(opts) {
|
|
|
168
213
|
st.order = next.order.slice();
|
|
169
214
|
st.visible = { ...next.visible };
|
|
170
215
|
st.density = next.density;
|
|
171
|
-
|
|
216
|
+
stApi.render();
|
|
172
217
|
memoryMarkDirty(opts.key);
|
|
173
218
|
emitChange(normalize());
|
|
174
219
|
}
|
|
@@ -176,7 +221,7 @@ export function createToolbar(opts) {
|
|
|
176
221
|
const getConfig = () => normalize();
|
|
177
222
|
/** subscription to everything the toolbar renders from (hook) */
|
|
178
223
|
function useSubscribe() {
|
|
179
|
-
|
|
224
|
+
stApi.use();
|
|
180
225
|
ext?.useConfig(); // ext is per-instance constant - hook order is stable
|
|
181
226
|
}
|
|
182
227
|
function useConfig() {
|
|
@@ -191,7 +236,7 @@ export function createToolbar(opts) {
|
|
|
191
236
|
* someone else's nodes.) */
|
|
192
237
|
function useItems() {
|
|
193
238
|
useSubscribe();
|
|
194
|
-
|
|
239
|
+
densitiesApi.use();
|
|
195
240
|
const cfg = normalize();
|
|
196
241
|
const byKey = new Map(opts.items.map(i => [i.key, i]));
|
|
197
242
|
return cfg.order
|
|
@@ -238,18 +283,18 @@ export function createToolbar(opts) {
|
|
|
238
283
|
* the popover sticks to - 'right' (default, for bars in a top-right corner)
|
|
239
284
|
* or 'left'. */
|
|
240
285
|
function Bar(p = {}) {
|
|
241
|
-
|
|
242
|
-
updateBy(densities);
|
|
286
|
+
const items = useItems();
|
|
243
287
|
const cfg = normalize();
|
|
244
288
|
const [open, setOpen] = useState(false);
|
|
245
|
-
const byKey = new Map(opts.items.map(i => [i.key, i]));
|
|
246
289
|
const resetOn = opts.resetItem !== false && (p.reset ?? p.settings ?? false) && cfg.visible[RESET_KEY] != false;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
290
|
+
const settingsOn = !!p.settings && cfg.visible[SETTINGS_KEY] != false;
|
|
291
|
+
const layoutKey = [
|
|
292
|
+
...items.map(x => `${x.item.key}:${x.density}`),
|
|
293
|
+
resetOn ? `${RESET_KEY}:on` : '',
|
|
294
|
+
settingsOn ? `${SETTINGS_KEY}:on` : '',
|
|
295
|
+
].join('|');
|
|
296
|
+
const bindFlip = useToolbarFlip(layoutKey);
|
|
297
|
+
return _jsxs("div", { className: p.className ?? 'wenayTb', children: [items.map(x => _jsx("div", { ref: bindFlip(x.item.key), className: 'wenayTbItem', title: x.density == 'icon' ? x.item.title : undefined, onClick: x.item.onClick, children: x.content }, x.item.key)), resetOn && _jsx("div", { ref: bindFlip(RESET_KEY), className: 'wenayTbItem', title: resetTitle(), onClick: () => reset(), children: resetIcon() }), settingsOn && _jsxs(OutsideClickArea, { className: 'wenayTbGear', status: open, outsideClick: () => setOpen(false), children: [_jsx("div", { ref: bindFlip(SETTINGS_KEY), className: 'wenayTbItem', title: settingsTitle(), onClick: () => setOpen(v => !v), children: settingsIcon() }), open && _jsx("div", { className: p.popAlign == 'left' ? 'wenayTbPop wenayTbPopLeft' : 'wenayTbPop', children: _jsx(Settings, {}) })] })] });
|
|
253
298
|
}
|
|
254
299
|
/** The pure editor over config: density segments + one row per item
|
|
255
300
|
* (visibility checkbox, icon preview, title, drag handle). Reorder rides the
|
|
@@ -260,7 +305,7 @@ export function createToolbar(opts) {
|
|
|
260
305
|
* Plus arrow keys on the focused handle; fixed rows have neither. */
|
|
261
306
|
function Settings(p = {}) {
|
|
262
307
|
useSubscribe();
|
|
263
|
-
|
|
308
|
+
densitiesApi.use();
|
|
264
309
|
const cfg = normalize();
|
|
265
310
|
const base = p.className ?? 'wenaySegBtn';
|
|
266
311
|
const activeCls = p.activeClassName ?? 'wenaySegBtnActive';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { createUpdateApi } from "../../../updateBy";
|
|
3
3
|
import { memoryGetOrCreate, memoryMarkDirty } from "../../utils/memoryStore";
|
|
4
4
|
/** One UI block shown in exactly one of several mount points; the point is a persisted setting.
|
|
5
5
|
* Persistence rides the existing memoryProps -> memoryCache mechanics: memoryProps is observable,
|
|
@@ -9,6 +9,7 @@ import { memoryGetOrCreate, memoryMarkDirty } from "../../utils/memoryStore";
|
|
|
9
9
|
* the persisted place with its own and renders children only on a match. */
|
|
10
10
|
export function createUiSlot(opts) {
|
|
11
11
|
const st = memoryGetOrCreate(opts.key, { place: opts.def });
|
|
12
|
+
const stApi = createUpdateApi(st);
|
|
12
13
|
// a stored place may no longer exist after an app update - fall back to def
|
|
13
14
|
const isPlace = (p) => typeof p == "string" && p in opts.places;
|
|
14
15
|
const getPlace = () => isPlace(st.place) ? st.place : opts.def;
|
|
@@ -16,16 +17,16 @@ export function createUiSlot(opts) {
|
|
|
16
17
|
if (st.place == p)
|
|
17
18
|
return;
|
|
18
19
|
st.place = p;
|
|
19
|
-
|
|
20
|
+
stApi.render();
|
|
20
21
|
memoryMarkDirty(opts.key);
|
|
21
22
|
};
|
|
22
23
|
function Slot(p) {
|
|
23
|
-
|
|
24
|
+
stApi.use();
|
|
24
25
|
return getPlace() == p.place ? _jsx(_Fragment, { children: p.children }) : null;
|
|
25
26
|
}
|
|
26
27
|
/** Segmented row over opts.places; apps pass their own .chip / .chipActive classes */
|
|
27
28
|
function PlacementSetting(p = {}) {
|
|
28
|
-
|
|
29
|
+
stApi.use();
|
|
29
30
|
const cur = getPlace();
|
|
30
31
|
const base = p.className ?? "wenaySegBtn";
|
|
31
32
|
const activeCls = p.activeClassName ?? "wenaySegBtnActive";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { listen as createListen } from 'wenay-common2';
|
|
2
|
-
import {
|
|
2
|
+
import { createUpdateApi } from '../../../updateBy';
|
|
3
3
|
import { memoryGetOrCreate, memoryMarkDirty } from '../../utils/memoryStore';
|
|
4
4
|
const SCHEMA_V = 1;
|
|
5
5
|
/** Grid events that mean "the user changed the column layout / sort / filter". */
|
|
@@ -17,6 +17,7 @@ export function createColumnState(opts) {
|
|
|
17
17
|
groups: opts.def?.groups ? { ...opts.def.groups } : Object.fromEntries(groupKeys.map(g => [g, groupMembers(g)])),
|
|
18
18
|
});
|
|
19
19
|
const st = memoryGetOrCreate(opts.key, defConfig());
|
|
20
|
+
const stApi = createUpdateApi(st);
|
|
20
21
|
const [emitChange, onChange] = createListen();
|
|
21
22
|
/** Runtime-only, never persisted: actual = which keys the attached grid has;
|
|
22
23
|
* gate = optional app-level availability over a stable grid schema (standards,
|
|
@@ -25,6 +26,7 @@ export function createColumnState(opts) {
|
|
|
25
26
|
present: null,
|
|
26
27
|
presentGate: null,
|
|
27
28
|
};
|
|
29
|
+
const rtApi = createUpdateApi(rt);
|
|
28
30
|
const keyMap = (keys) => keys ? Object.fromEntries(keys.map(k => [k, true])) : null;
|
|
29
31
|
const sameMap = (a, b) => JSON.stringify(a) == JSON.stringify(b);
|
|
30
32
|
function combinedPresent() {
|
|
@@ -42,14 +44,14 @@ export function createColumnState(opts) {
|
|
|
42
44
|
if (sameMap(next, rt.present))
|
|
43
45
|
return;
|
|
44
46
|
rt.present = next;
|
|
45
|
-
|
|
47
|
+
rtApi.render();
|
|
46
48
|
}
|
|
47
49
|
function setPresentGate(keys) {
|
|
48
50
|
const next = keyMap(keys);
|
|
49
51
|
if (sameMap(next, rt.presentGate))
|
|
50
52
|
return;
|
|
51
53
|
rt.presentGate = next;
|
|
52
|
-
|
|
54
|
+
rtApi.render();
|
|
53
55
|
applyToGrid();
|
|
54
56
|
}
|
|
55
57
|
const getPresent = combinedPresent;
|
|
@@ -60,7 +62,7 @@ export function createColumnState(opts) {
|
|
|
60
62
|
};
|
|
61
63
|
const passesPresentGate = (key) => !rt.presentGate || rt.presentGate[key] == true;
|
|
62
64
|
function usePresent() {
|
|
63
|
-
|
|
65
|
+
rtApi.use();
|
|
64
66
|
return combinedPresent();
|
|
65
67
|
}
|
|
66
68
|
/** The persisted state may be stale or partial (older app version, columns
|
|
@@ -116,7 +118,7 @@ export function createColumnState(opts) {
|
|
|
116
118
|
st.sort = next.sort ? { ...next.sort } : null;
|
|
117
119
|
st.filter = { ...next.filter };
|
|
118
120
|
st.groups = Object.fromEntries(Object.entries(next.groups).map(([g, keys]) => [g, keys.slice()]));
|
|
119
|
-
|
|
121
|
+
stApi.render();
|
|
120
122
|
memoryMarkDirty(opts.key);
|
|
121
123
|
if (!fromGrid)
|
|
122
124
|
applyToGrid();
|
|
@@ -126,7 +128,7 @@ export function createColumnState(opts) {
|
|
|
126
128
|
const setConfig = (next) => commit(next, false);
|
|
127
129
|
const reset = () => commit(defConfig(), false);
|
|
128
130
|
function useConfig() {
|
|
129
|
-
|
|
131
|
+
stApi.use();
|
|
130
132
|
return normalize();
|
|
131
133
|
}
|
|
132
134
|
function show(key, on) {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { useEffect, useRef } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { createUpdateApi } from "../../updateBy";
|
|
3
3
|
import { listen as createListen } from "wenay-common2";
|
|
4
4
|
export const keyboardState = {
|
|
5
5
|
key: ""
|
|
6
6
|
};
|
|
7
|
+
const keyboardStateApi = createUpdateApi(keyboardState);
|
|
7
8
|
const [emitKeyDown, keyboardListen] = createListen();
|
|
8
9
|
export const keyboard = {
|
|
9
10
|
get key() { return keyboardState.key; },
|
|
@@ -11,7 +12,7 @@ export const keyboard = {
|
|
|
11
12
|
get() { return keyboardState.key; },
|
|
12
13
|
clear() {
|
|
13
14
|
keyboardState.key = "";
|
|
14
|
-
|
|
15
|
+
keyboardStateApi.render();
|
|
15
16
|
emitKeyDown("", undefined);
|
|
16
17
|
},
|
|
17
18
|
reset() {
|
|
@@ -38,7 +39,7 @@ export function useKeyboard(options = {}) {
|
|
|
38
39
|
if (!(event instanceof KeyboardEvent))
|
|
39
40
|
return;
|
|
40
41
|
keyboardState.key = event.key;
|
|
41
|
-
|
|
42
|
+
keyboardStateApi.render();
|
|
42
43
|
emitKeyDown(event.key, event);
|
|
43
44
|
onKeyDownRef.current?.(event.key, event);
|
|
44
45
|
};
|