wenay-react2 1.0.27 → 1.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/lib/common/api.d.ts +4 -1
  2. package/lib/common/api.js +4 -1
  3. package/lib/common/src/components/Dnd/RNDFunc3.js +9 -17
  4. package/lib/common/src/components/Menu/RightMenu.d.ts +9 -9
  5. package/lib/common/src/components/Menu/RightMenu.js +68 -38
  6. package/lib/common/src/components/Menu/RightMenuStore.d.ts +11 -0
  7. package/lib/common/src/components/Menu/RightMenuStore.js +1 -0
  8. package/lib/common/src/components/Menu/index.d.ts +1 -0
  9. package/lib/common/src/components/Menu/index.js +1 -0
  10. package/lib/common/src/components/Settings/SettingsDialog.d.ts +22 -0
  11. package/lib/common/src/components/Settings/SettingsDialog.js +55 -0
  12. package/lib/common/src/components/Settings/index.d.ts +1 -0
  13. package/lib/common/src/components/Settings/index.js +1 -0
  14. package/lib/common/src/components/UiSlot/UiSlot.d.ts +24 -0
  15. package/lib/common/src/components/UiSlot/UiSlot.js +33 -0
  16. package/lib/common/src/components/UiSlot/index.d.ts +1 -0
  17. package/lib/common/src/components/UiSlot/index.js +1 -0
  18. package/lib/common/src/styles/tokens.d.ts +39 -0
  19. package/lib/common/src/styles/tokens.js +39 -0
  20. package/lib/common/src/utils/cache.d.ts +4 -0
  21. package/lib/common/src/utils/cache.js +52 -3
  22. package/lib/common/src/utils/callbackHub.d.ts +10 -0
  23. package/lib/common/src/utils/callbackHub.js +23 -0
  24. package/lib/common/src/utils/index.d.ts +1 -0
  25. package/lib/common/src/utils/index.js +1 -0
  26. package/lib/common/src/utils/mapMemory.d.ts +3 -0
  27. package/lib/common/src/utils/mapMemory.js +3 -0
  28. package/lib/style/style.css +121 -11
  29. package/lib/style/tokens.css +43 -0
  30. package/package.json +1 -1
@@ -25,7 +25,10 @@ export * from "./src/components/ParametersEngine";
25
25
  export * from "./src/components/Input";
26
26
  export * from "./src/components/Modal";
27
27
  export * from "./src/components/Menu";
28
- export * from "./src/menu/menu";
28
+ export * from "./src/components/Settings";
29
+ export * from "./src/components/UiSlot";
30
+ export { MenuBase, TimeNum, MenuElement as LegacyMenuElement } from "./src/menu/menu";
31
+ export type { tMenuReact, tMenuReactStrictly } from "./src/menu/menu";
29
32
  export * from "./src/menu/menuMouse";
30
33
  export * from "./src/menu/menuR";
31
34
  export * from "./src/logs/logs";
package/lib/common/api.js CHANGED
@@ -37,8 +37,11 @@ export * from "./src/components/Input";
37
37
  export * from "./src/components/Modal";
38
38
  // 11. Menu - depends on hooks, Dnd, and Modal
39
39
  export * from "./src/components/Menu";
40
+ // 11b. Settings dialog + section registry; UI slot with configurable placement
41
+ export * from "./src/components/Settings";
42
+ export * from "./src/components/UiSlot";
40
43
  // 12. MENU - depends on components
41
- export * from "./src/menu/menu";
44
+ export { MenuBase, TimeNum, MenuElement as LegacyMenuElement } from "./src/menu/menu";
42
45
  export * from "./src/menu/menuMouse";
43
46
  export * from "./src/menu/menuR";
44
47
  // 13. LOGS - depend on utils/mapMemory, menu, and components/ParametersEngine
@@ -21,9 +21,11 @@ export const DivRnd3 = (a) => {
21
21
  * Provides dragging and resizing, an optional header, and a close button.
22
22
  */
23
23
  export function DivRndBase3({ children, keyForSave: ks, position, size, overflow = true, zIndex = 9, onUpdate, disableDragging, className, header, moveOnlyHeader, limit, onCLickClose, sizeByWindow = true }) {
24
- if (onCLickClose && !limit) {
25
- limit = { y: { min: 0 } };
26
- }
24
+ // NOTE: no implicit limit for onCLickClose windows. `limit` is parent-relative,
25
+ // so {y:{min:0}} pinned windows to their DOM parent's top: a window opened from a
26
+ // centered wrapper (ModalProvider / ModalWrapper at top:50%) could not be dragged
27
+ // above mid-screen. Keeping the window on screen is already handled by the
28
+ // viewport clamp below (header rect vs viewport in useLayoutEffect).
27
29
  const positionDef = { x: 0, y: 0, ...(position ?? {}) };
28
30
  const sizeDef = { height: 0, width: 0, ...(size ?? {}) };
29
31
  // If there is a key, store position and size data in the map
@@ -199,7 +201,7 @@ export function DivRndBase3({ children, keyForSave: ks, position, size, overflow
199
201
  setHeight(window.innerHeight);
200
202
  }
201
203
  }, [x, y, width, height, sizeByWindow]);
202
- const headerD = (_jsx("div", { ref: headerRef, onTouchStart: (e) => {
204
+ const headerD = (_jsx("div", { ref: headerRef, className: "wenayWndHeader", onTouchStart: (e) => {
203
205
  const t = e.changedTouches[0];
204
206
  if (t) {
205
207
  lastT.current = {
@@ -215,14 +217,7 @@ export function DivRndBase3({ children, keyForSave: ks, position, size, overflow
215
217
  y: y - e.clientY
216
218
  };
217
219
  setA(true);
218
- }, style: {
219
- userSelect: "none",
220
- cursor: "grabbing"
221
- }, children: header ?? (_jsx("div", { style: {
222
- height: 20,
223
- width: "100%",
224
- backgroundImage: "repeating-linear-gradient(139deg, hsla(0,0%,100%,.1), hsla(0,0%,100%,.1) 15px, transparent 0, transparent 30px)"
225
- } })) }));
220
+ }, children: header ?? _jsx("div", { className: "wenayWndHeaderDef" }) }));
226
221
  return (_jsx(Rnd, { disableDragging: true, style: {
227
222
  zIndex: zIndexX * 2 + zIndex
228
223
  }, className: className, onResizeStop: (e, dir, elementRef, delta, { x: nx, y: ny }) => {
@@ -237,7 +232,7 @@ export function DivRndBase3({ children, keyForSave: ks, position, size, overflow
237
232
  }, position: { x, y }, size: { width, height }, default: {
238
233
  ...position,
239
234
  ...size
240
- }, children: _jsxs("div", { style: {
235
+ }, children: _jsxs("div", { className: "wenayWnd", style: {
241
236
  width: "100%",
242
237
  height: "100%",
243
238
  display: "flex",
@@ -256,10 +251,7 @@ export function DivRndBase3({ children, keyForSave: ks, position, size, overflow
256
251
  }, children: [moveOnlyHeader || header ? headerD : null, _jsxs("div", { className: "maxSize", style: { overflow: overflow ? "auto" : undefined }, children: [(a || b) && (_jsx("div", { className: "maxSize", style: {
257
252
  position: "absolute",
258
253
  zIndex: zIndexX * 2 + zIndex + 1
259
- } })), typeof children === "function" ? children(update) : children] }), onCLickClose && (_jsx("div", { className: "wenayCloseBtn", title: "Close", style: {
260
- position: "absolute",
261
- right: -12,
262
- top: -12,
254
+ } })), typeof children === "function" ? children(update) : children] }), onCLickClose && (_jsx("div", { className: "wenayCloseBtn wenayWndClose", title: "Close", style: {
263
255
  zIndex: zIndexX * 2 + zIndex + 1
264
256
  }, onClick: onCLickClose, 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" }) }) }, "323"))] }) }));
265
257
  }
@@ -1,22 +1,22 @@
1
1
  import React, { JSX } from 'react';
2
- type MenuElement = {
2
+ import { type MenuRightPosition, type MenuRightPosition2 } from "./RightMenuStore";
3
+ export type MenuElement = {
3
4
  label: string;
4
5
  subMenuContent: () => JSX.Element;
5
6
  };
6
- type DropdownMenuProps = {
7
+ export type DropdownMenuProps = {
7
8
  elements: MenuElement[];
8
9
  style?: React.CSSProperties;
9
- position?: 'left' | 'right';
10
- position2?: 'top' | 'bottom';
10
+ position?: MenuRightPosition;
11
+ position2?: MenuRightPosition2;
12
+ keyForSave?: string;
11
13
  };
12
- export declare function DropdownMenu({ elements, style, position: initialPosition, position2: initialPos2 }: DropdownMenuProps): import("react/jsx-runtime").JSX.Element;
14
+ export type MenuRightRenderProps = Omit<DropdownMenuProps, 'elements'>;
15
+ export declare function DropdownMenu({ elements, style, position: initialPosition, position2: initialPos2, keyForSave }: DropdownMenuProps): import("react/jsx-runtime").JSX.Element;
13
16
  export declare function MenuRightApi(): {
14
17
  set(array: MenuElement[]): void;
15
18
  delete(array: MenuElement[]): void;
16
19
  get(): MenuElement[];
17
- Render({ style }: {
18
- style?: React.CSSProperties;
19
- }): import("react/jsx-runtime").JSX.Element;
20
+ Render(props?: MenuRightRenderProps): import("react/jsx-runtime").JSX.Element;
20
21
  };
21
22
  export declare function DropdownMenuTest(): import("react/jsx-runtime").JSX.Element;
22
- export {};
@@ -5,7 +5,22 @@ import { useDraggable } from "../../hooks/useDraggable";
5
5
  import { DivOutsideClick } from "../../hooks/useOutside";
6
6
  import { DraggableOutlineDiv } from "../Dnd/DraggableOutlineDiv";
7
7
  import { GetModalFuncJSX } from "../Modal/Modal";
8
- export function DropdownMenu({ elements, style, position: initialPosition = 'right', position2: initialPos2 = 'top' }) {
8
+ import { mapRightMenu } from "./RightMenuStore";
9
+ export function DropdownMenu({ elements, style, position: initialPosition = 'right', position2: initialPos2 = 'top', keyForSave }) {
10
+ const [initialState] = useState(() => {
11
+ const fallback = {
12
+ position: initialPosition,
13
+ position2: initialPos2,
14
+ offset: { x: 0, y: 0 }
15
+ };
16
+ if (!keyForSave)
17
+ return fallback;
18
+ const saved = mapRightMenu.get(keyForSave);
19
+ if (saved)
20
+ return saved;
21
+ mapRightMenu.set(keyForSave, fallback);
22
+ return fallback;
23
+ });
9
24
  const [isOpen, setIsOpen] = useState(false);
10
25
  const [isFixed, setIsFixed] = useState(false);
11
26
  const [select, setSelect] = useState(null);
@@ -13,41 +28,52 @@ export function DropdownMenu({ elements, style, position: initialPosition = 'rig
13
28
  // Get modal JSX functions
14
29
  const jsx = useMemo(GetModalFuncJSX, []);
15
30
  const jsxRender = useMemo(() => _jsx(jsx.Render, {}), [jsx]);
16
- const [position, setPosition] = useState(initialPosition);
17
- const [isTop, setIsTop] = useState(initialPos2 === 'top');
18
- const positionLast = useRef({ x: 0, y: 0 });
31
+ const [position, setPosition] = useState(initialState.position);
32
+ const [isTop, setIsTop] = useState(initialState.position2 === 'top');
33
+ const positionLast = useRef({ ...initialState.offset });
34
+ const containerRef = useRef(null);
19
35
  const handleDragEnd = useCallback((finalPosition) => {
20
- // Handle horizontal offset
21
- if (position === 'left') {
22
- positionLast.current.x += finalPosition.x;
23
- if (positionLast.current.x > window.innerWidth * 0.6) {
24
- positionLast.current.x = window.innerWidth - positionLast.current.x;
25
- setPosition('right');
26
- }
27
- }
28
- else if (position === 'right') {
29
- positionLast.current.x -= finalPosition.x;
30
- if (positionLast.current.x > window.innerWidth * 0.6) {
31
- positionLast.current.x = window.innerWidth - positionLast.current.x;
32
- setPosition('left');
33
- }
34
- }
35
- // Handle vertical offset
36
- if (isTop) {
37
- positionLast.current.y += finalPosition.y;
38
- if (positionLast.current.y > document.documentElement.clientHeight * 0.6) {
39
- positionLast.current.y = document.documentElement.clientHeight - positionLast.current.y;
40
- setIsTop(false);
41
- }
42
- }
43
- else {
44
- positionLast.current.y -= finalPosition.y;
45
- if (positionLast.current.y > document.documentElement.clientHeight * 0.6) {
46
- positionLast.current.y = document.documentElement.clientHeight - positionLast.current.y;
47
- setIsTop(true);
48
- }
36
+ // Re-anchor from the computed dropped rect. useDraggable resets its delta
37
+ // before React necessarily paints, so measuring the element position here
38
+ // can read the pre-drop corner and cause a sudden relocation.
39
+ const el = containerRef.current;
40
+ if (!el)
41
+ return;
42
+ const parent = el.offsetParent;
43
+ // fixed positioning -> offsetParent is null -> viewport is the box
44
+ const bounds = parent?.getBoundingClientRect()
45
+ ?? new DOMRect(0, 0, window.innerWidth, document.documentElement.clientHeight);
46
+ const measured = el.getBoundingClientRect();
47
+ const width = measured.width;
48
+ const height = measured.height;
49
+ const baseLeft = position === 'left'
50
+ ? bounds.left + positionLast.current.x
51
+ : bounds.right - positionLast.current.x - width;
52
+ const baseTop = isTop
53
+ ? bounds.top + positionLast.current.y
54
+ : bounds.bottom - positionLast.current.y - height;
55
+ const left = baseLeft + finalPosition.x;
56
+ const top = baseTop + finalPosition.y;
57
+ const right = left + width;
58
+ const bottom = top + height;
59
+ const toLeft = left + width / 2 < bounds.left + bounds.width / 2;
60
+ const toTop = top + height / 2 < bounds.top + bounds.height / 2;
61
+ const nextPosition = toLeft ? 'left' : 'right';
62
+ const nextOffset = {
63
+ x: Math.max(0, toLeft ? left - bounds.left : bounds.right - right),
64
+ y: Math.max(0, toTop ? top - bounds.top : bounds.bottom - bottom)
65
+ };
66
+ positionLast.current = nextOffset;
67
+ setPosition(nextPosition);
68
+ setIsTop(toTop);
69
+ if (keyForSave) {
70
+ mapRightMenu.set(keyForSave, {
71
+ position: nextPosition,
72
+ position2: toTop ? 'top' : 'bottom',
73
+ offset: { ...nextOffset }
74
+ });
49
75
  }
50
- }, [position, isTop]);
76
+ }, [isTop, keyForSave, position]);
51
77
  const { position: pos, dragProps } = useDraggable(0, 0, 50, handleDragEnd, () => { });
52
78
  // Click and hover handlers
53
79
  const handleClickOutside = useCallback(() => {
@@ -89,7 +115,11 @@ export function DropdownMenu({ elements, style, position: initialPosition = 'rig
89
115
  [position]: 0,
90
116
  right: position === 'left' ? 'auto' : 0,
91
117
  flexDirection: position === 'left' ? 'row' : 'row-reverse'
92
- }, children: [_jsx("div", { className: "dropdown-content", onMouseEnter: handleContentMouseEnter, onMouseLeave: handleContentMouseLeave, children: elements.map((item, index) => (_jsx("div", { className: `menu-item ${select === index ? 'force-hover' : ''}`, onMouseEnter: () => handleSelect(item, index), onClick: () => handleSelect(item, index), children: item.label }, item.label))) }), _jsx("div", { onMouseEnter: handleSubmenuMouseEnter, onMouseLeave: handleSubmenuMouseLeave, children: jsx.JSX ? (_jsx("div", { className: "submenu", style: { width: '40vw', minHeight: '70vh' }, children: jsxRender })) : null })] }));
118
+ }, children: [_jsx("div", { className: "dropdown-content", onMouseEnter: handleContentMouseEnter, onMouseLeave: handleContentMouseLeave, children: elements.map((item, index) => (_jsx("div", { className: `menu-item ${select === index ? 'force-hover' : ''}`, onMouseEnter: () => handleSelect(item, index), onClick: () => handleSelect(item, index), children: item.label }, item.label))) }), _jsx("div", { onMouseEnter: handleSubmenuMouseEnter, onMouseLeave: handleSubmenuMouseLeave, children: jsx.JSX ? (
119
+ // clamp to the viewport: with hard 40vw/70vh the submenu ran
120
+ // off screen when the menu sat near a bottom/side edge
121
+ _jsx("div", { className: "submenu", style: { width: '40vw', minHeight: '70vh',
122
+ maxWidth: '90vw', maxHeight: '85vh', overflowY: 'auto' }, children: jsxRender })) : null })] }));
93
123
  // Calculate offsets
94
124
  const computedX = position === 'left'
95
125
  ? positionLast.current.x + pos.x
@@ -123,7 +153,7 @@ export function DropdownMenu({ elements, style, position: initialPosition = 'rig
123
153
  }
124
154
  return computedStyle;
125
155
  }, [style, position, isTop, computedX, computedY]);
126
- return (_jsxs(DivOutsideClick, { outsideClick: handleClickOutside, className: `menu-container ${isFixed ? 'activeM' : ''}`, style: containerStyle, onMouseEnter: () => !isFixed && setIsOpen(true), onMouseLeave: () => !isFixed && setIsOpen(false), children: [_jsx("div", { ...dragProps, className: "menu-button", onClick: handleToggle, children: "\u2630" }), dop] }));
156
+ return (_jsxs(DivOutsideClick, { ref: containerRef, outsideClick: handleClickOutside, className: `menu-container ${isFixed ? 'activeM' : ''}`, style: containerStyle, onMouseEnter: () => !isFixed && setIsOpen(true), onMouseLeave: () => !isFixed && setIsOpen(false), children: [_jsx("div", { ...dragProps, className: "menu-button", onClick: handleToggle, children: "\u2630" }), dop] }));
127
157
  }
128
158
  export function MenuRightApi() {
129
159
  const elements = [];
@@ -147,7 +177,7 @@ export function MenuRightApi() {
147
177
  get() {
148
178
  return elements;
149
179
  },
150
- Render({ style }) {
180
+ Render(props = {}) {
151
181
  const [el, setEl] = useState([...elements]);
152
182
  useEffect(() => {
153
183
  render = setEl;
@@ -155,7 +185,7 @@ export function MenuRightApi() {
155
185
  render = null;
156
186
  };
157
187
  }, []);
158
- return _jsx(DropdownMenu, { elements: el, style: style, position: "left" });
188
+ return _jsx(DropdownMenu, { ...props, elements: el });
159
189
  }
160
190
  };
161
191
  }
@@ -0,0 +1,11 @@
1
+ export type MenuRightPosition = 'left' | 'right';
2
+ export type MenuRightPosition2 = 'top' | 'bottom';
3
+ export type MenuRightSavedState = {
4
+ position: MenuRightPosition;
5
+ position2: MenuRightPosition2;
6
+ offset: {
7
+ x: number;
8
+ y: number;
9
+ };
10
+ };
11
+ export declare const mapRightMenu: Map<string, MenuRightSavedState>;
@@ -0,0 +1 @@
1
+ export const mapRightMenu = new Map();
@@ -1,2 +1,3 @@
1
1
  export { default as StickerMenu } from './StickerMenu';
2
2
  export * from './RightMenu';
3
+ export * from './RightMenuStore';
@@ -1,2 +1,3 @@
1
1
  export { default as StickerMenu } from './StickerMenu';
2
2
  export * from './RightMenu';
3
+ export * from './RightMenuStore';
@@ -0,0 +1,22 @@
1
+ import React from "react";
2
+ export type tSettingsSection = {
3
+ key: string;
4
+ name: string;
5
+ render: () => React.ReactNode;
6
+ };
7
+ /** Register an external section. Re-register with the same key replaces the previous one.
8
+ * The returned function removes exactly this registration (a no-op if it was replaced). */
9
+ export declare function registerSettingsSection(s: tSettingsSection): () => void;
10
+ /** Current external sections (static props sections are not included). */
11
+ export declare function getSettingsSections(): readonly tSettingsSection[];
12
+ /** Centered settings dialog: sections column on the left, active section content on the right.
13
+ * Sections = props.sections (first) + everything from registerSettingsSection.
14
+ * Look is themed via --dlg-* CSS variables (dark defaults), same contract as --wnd-*. */
15
+ export declare function SettingsDialog(props: {
16
+ trigger: React.ReactNode;
17
+ sections?: tSettingsSection[];
18
+ defaultSection?: string;
19
+ /** Section buttons: apps pass their own .chip / .chipActive; defaults are minimal library styles */
20
+ sectionClassName?: string;
21
+ sectionActiveClassName?: string;
22
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,55 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useState } from "react";
3
+ import { createPortal } from "react-dom";
4
+ import { renderBy, updateBy } from "../../../updateBy";
5
+ // Module singleton (closure + updateBy subscription, no React context): any module
6
+ // registers a section on mount and removes it on unmount; the dialog re-renders on changes.
7
+ const registry = { list: [] };
8
+ /** Register an external section. Re-register with the same key replaces the previous one.
9
+ * The returned function removes exactly this registration (a no-op if it was replaced). */
10
+ export function registerSettingsSection(s) {
11
+ const i = registry.list.findIndex(e => e.key == s.key);
12
+ if (i == -1)
13
+ registry.list.push(s);
14
+ else
15
+ registry.list.splice(i, 1, s);
16
+ renderBy(registry);
17
+ return () => {
18
+ const j = registry.list.indexOf(s);
19
+ if (j != -1) {
20
+ registry.list.splice(j, 1);
21
+ renderBy(registry);
22
+ }
23
+ };
24
+ }
25
+ /** Current external sections (static props sections are not included). */
26
+ export function getSettingsSections() {
27
+ return registry.list;
28
+ }
29
+ /** Centered settings dialog: sections column on the left, active section content on the right.
30
+ * Sections = props.sections (first) + everything from registerSettingsSection.
31
+ * Look is themed via --dlg-* CSS variables (dark defaults), same contract as --wnd-*. */
32
+ export function SettingsDialog(props) {
33
+ const [open, setOpen] = useState(false);
34
+ const [active, setActive] = useState(props.defaultSection);
35
+ updateBy(registry);
36
+ useEffect(() => {
37
+ if (!open)
38
+ return;
39
+ const onKeyDown = (e) => {
40
+ if (e.key == 'Escape')
41
+ setOpen(false);
42
+ };
43
+ document.addEventListener('keydown', onKeyDown);
44
+ return () => document.removeEventListener('keydown', onKeyDown);
45
+ }, [open]);
46
+ const sections = [...(props.sections ?? []), ...registry.list];
47
+ // fall back to the first section when active was never set or its section unmounted
48
+ const current = sections.find(s => s.key == active) ?? sections[0];
49
+ const base = props.sectionClassName ?? "wenayDlgSection";
50
+ const activeCls = props.sectionActiveClassName ?? "wenayDlgSectionActive";
51
+ return _jsxs(_Fragment, { children: [_jsx("span", { onClick: () => setOpen(true), style: { display: "inline-block", cursor: "pointer" }, children: props.trigger }), open && createPortal(_jsx("div", { className: "wenayDlgScrim", onClick: e => {
52
+ if (e.target == e.currentTarget)
53
+ setOpen(false);
54
+ }, children: _jsxs("div", { className: "wenayDlg", children: [_jsx("div", { className: "wenayDlgNav", children: sections.map(s => (_jsx("div", { className: s.key == current?.key ? `${base} ${activeCls}` : base, onClick: () => setActive(s.key), children: s.name }, s.key))) }), _jsx("div", { className: "wenayDlgContent", children: current?.render() }), _jsx("div", { className: "wenayCloseBtn wenayDlgClose", title: "Close", onClick: () => setOpen(false), 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" }) }) })] }) }), document.body)] });
55
+ }
@@ -0,0 +1 @@
1
+ export * from './SettingsDialog';
@@ -0,0 +1 @@
1
+ export * from './SettingsDialog';
@@ -0,0 +1,24 @@
1
+ import React from "react";
2
+ /** One UI block shown in exactly one of several mount points; the point is a persisted setting.
3
+ * Persistence rides the existing staticProps -> Cash mechanics: the app calls Cash.load()/save()
4
+ * the same way it does for window state (ExRNDMap3 etc.).
5
+ * Mount points render <Slot place="..."> themselves and decide nothing: the slot compares
6
+ * the persisted place with its own and renders children only on a match. */
7
+ export declare function createUiSlot<Places extends string>(opts: {
8
+ key: string;
9
+ places: {
10
+ [P in Places]: string;
11
+ };
12
+ def: NoInfer<Places>;
13
+ }): {
14
+ Slot: (p: {
15
+ place: Places;
16
+ children: React.ReactNode;
17
+ }) => React.JSX.Element | null;
18
+ PlacementSetting: (p?: {
19
+ className?: string;
20
+ activeClassName?: string;
21
+ }) => import("react/jsx-runtime").JSX.Element;
22
+ getPlace: () => Places;
23
+ setPlace: (p: Places) => void;
24
+ };
@@ -0,0 +1,33 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { renderBy, updateBy } from "../../../updateBy";
3
+ import { staticGetAdd } from "../../utils/mapMemory";
4
+ /** One UI block shown in exactly one of several mount points; the point is a persisted setting.
5
+ * Persistence rides the existing staticProps -> Cash mechanics: the app calls Cash.load()/save()
6
+ * the same way it does for window state (ExRNDMap3 etc.).
7
+ * Mount points render <Slot place="..."> themselves and decide nothing: the slot compares
8
+ * the persisted place with its own and renders children only on a match. */
9
+ export function createUiSlot(opts) {
10
+ const st = staticGetAdd(opts.key, { place: opts.def });
11
+ // a stored place may no longer exist after an app update - fall back to def
12
+ const isPlace = (p) => typeof p == "string" && p in opts.places;
13
+ const getPlace = () => isPlace(st.place) ? st.place : opts.def;
14
+ const setPlace = (p) => {
15
+ if (st.place == p)
16
+ return;
17
+ st.place = p;
18
+ renderBy(st);
19
+ };
20
+ function Slot(p) {
21
+ updateBy(st);
22
+ return getPlace() == p.place ? _jsx(_Fragment, { children: p.children }) : null;
23
+ }
24
+ /** Segmented row over opts.places; apps pass their own .chip / .chipActive classes */
25
+ function PlacementSetting(p = {}) {
26
+ updateBy(st);
27
+ const cur = getPlace();
28
+ const base = p.className ?? "wenaySegBtn";
29
+ const activeCls = p.activeClassName ?? "wenaySegBtnActive";
30
+ return _jsx("div", { style: { display: "inline-flex", gap: 4, flexWrap: "wrap" }, children: Object.keys(opts.places).map(key => (_jsx("div", { className: key == cur ? `${base} ${activeCls}` : base, onClick: () => setPlace(key), children: opts.places[key] }, key))) });
31
+ }
32
+ return { Slot, PlacementSetting, getPlace, setPlace };
33
+ }
@@ -0,0 +1 @@
1
+ export * from './UiSlot';
@@ -0,0 +1 @@
1
+ export * from './UiSlot';
@@ -17,6 +17,45 @@ export declare const tokens: {
17
17
  readonly itemColor: "#fff";
18
18
  readonly itemHoverColor: "#101010";
19
19
  readonly itemHoverBgColor: "#fff";
20
+ readonly shadow: "0 0 20px 14px rgba(34, 60, 80, 0.2)";
21
+ };
22
+ /** DivRnd3 window chrome (--wnd-*). Defaults = legacy look; apps re-skin via :root[data-theme].
23
+ * --wnd-header-height/color/font-size/letter-spacing/transform are intentionally undeclared
24
+ * (inherit/auto) until a theme sets them. */
25
+ readonly wnd: {
26
+ readonly bg: "transparent";
27
+ readonly border: "none";
28
+ readonly radius: "0";
29
+ readonly shadow: "none";
30
+ readonly headerBg: "transparent";
31
+ readonly headerStripes: "repeating-linear-gradient(139deg, hsla(0, 0%, 100%, 0.1), hsla(0, 0%, 100%, 0.1) 15px, transparent 0, transparent 30px)";
32
+ readonly headerBorder: "none";
33
+ readonly headerPadding: "0";
34
+ readonly closeSize: "28px";
35
+ readonly closeTop: "-12px";
36
+ readonly closeRight: "-12px";
37
+ /** var(--color-bg-light) in CSS */
38
+ readonly closeBg: "#17202e";
39
+ /** var(--color-border-common) in CSS */
40
+ readonly closeBorder: "1px solid rgb(50, 62, 71)";
41
+ readonly closeShadow: "0 2px 6px rgba(0, 0, 0, 0.35)";
42
+ /** var(--color-text-base) in CSS */
43
+ readonly closeColor: "#c4c4c4";
44
+ readonly closeHover: "#fff";
45
+ readonly closeHoverBg: "#e5484d";
46
+ };
47
+ /** SettingsDialog chrome (--dlg-*). Dark defaults; apps re-skin via :root[data-theme]. */
48
+ readonly dlg: {
49
+ readonly scrim: "rgba(0, 0, 0, 0.5)";
50
+ /** var(--color-bg-dark) in CSS */
51
+ readonly bg: "#131821";
52
+ /** var(--color-border-common) in CSS */
53
+ readonly border: "1px solid rgb(50, 62, 71)";
54
+ readonly radius: "10px";
55
+ readonly shadow: "0 12px 40px rgba(0, 0, 0, 0.5)";
56
+ /** var(--color-bg-light) in CSS */
57
+ readonly navBg: "#17202e";
58
+ readonly navWidth: "168px";
20
59
  };
21
60
  readonly font: {
22
61
  readonly family: "Roboto";
@@ -19,6 +19,45 @@ export const tokens = {
19
19
  itemColor: '#fff',
20
20
  itemHoverColor: '#101010',
21
21
  itemHoverBgColor: '#fff',
22
+ shadow: '0 0 20px 14px rgba(34, 60, 80, 0.2)',
23
+ },
24
+ /** DivRnd3 window chrome (--wnd-*). Defaults = legacy look; apps re-skin via :root[data-theme].
25
+ * --wnd-header-height/color/font-size/letter-spacing/transform are intentionally undeclared
26
+ * (inherit/auto) until a theme sets them. */
27
+ wnd: {
28
+ bg: 'transparent',
29
+ border: 'none',
30
+ radius: '0',
31
+ shadow: 'none',
32
+ headerBg: 'transparent',
33
+ headerStripes: 'repeating-linear-gradient(139deg, hsla(0, 0%, 100%, 0.1), hsla(0, 0%, 100%, 0.1) 15px, transparent 0, transparent 30px)',
34
+ headerBorder: 'none',
35
+ headerPadding: '0',
36
+ closeSize: '28px',
37
+ closeTop: '-12px',
38
+ closeRight: '-12px',
39
+ /** var(--color-bg-light) in CSS */
40
+ closeBg: '#17202e',
41
+ /** var(--color-border-common) in CSS */
42
+ closeBorder: '1px solid rgb(50, 62, 71)',
43
+ closeShadow: '0 2px 6px rgba(0, 0, 0, 0.35)',
44
+ /** var(--color-text-base) in CSS */
45
+ closeColor: '#c4c4c4',
46
+ closeHover: '#fff',
47
+ closeHoverBg: '#e5484d',
48
+ },
49
+ /** SettingsDialog chrome (--dlg-*). Dark defaults; apps re-skin via :root[data-theme]. */
50
+ dlg: {
51
+ scrim: 'rgba(0, 0, 0, 0.5)',
52
+ /** var(--color-bg-dark) in CSS */
53
+ bg: '#131821',
54
+ /** var(--color-border-common) in CSS */
55
+ border: '1px solid rgb(50, 62, 71)',
56
+ radius: '10px',
57
+ shadow: '0 12px 40px rgba(0, 0, 0, 0.5)',
58
+ /** var(--color-bg-light) in CSS */
59
+ navBg: '#17202e',
60
+ navWidth: '168px',
22
61
  },
23
62
  font: {
24
63
  family: 'Roboto',
@@ -20,12 +20,16 @@ export declare const ObjectStringToDate: (obj: unknown) => void;
20
20
  export declare function CacheFuncMapBase(arr: [k: string, v: Map<string, unknown>][], Save: IServerSaveBasePromise): {
21
21
  load(): Promise<void>;
22
22
  save(): Promise<void>;
23
+ saveDebounced(delay?: number): void;
24
+ flush(): Promise<void>;
23
25
  clear(): Promise<void>;
24
26
  getArr: [k: string, v: Map<string, unknown>][];
25
27
  };
26
28
  export declare function CacheFuncMap(arr: [k: string, v: Map<string, unknown>][]): {
27
29
  load(): Promise<void>;
28
30
  save(): Promise<void>;
31
+ saveDebounced(delay?: number): void;
32
+ flush(): Promise<void>;
29
33
  clear(): Promise<void>;
30
34
  getArr: [k: string, v: Map<string, unknown>][];
31
35
  };
@@ -99,6 +99,43 @@ function isDate(_date) {
99
99
  return ISO_DATE_RE.test(_date);
100
100
  }
101
101
  export function CacheFuncMapBase(arr, Save) {
102
+ const savedPayloadByKey = new Map();
103
+ let saveTimer = null;
104
+ let runningSave = null;
105
+ const getPayloads = () => arr.map(([key, map]) => [key, JSON.stringify([...map.entries()])]);
106
+ const rememberCurrentPayloads = () => {
107
+ savedPayloadByKey.clear();
108
+ for (const [key, payload] of getPayloads())
109
+ savedPayloadByKey.set(key, payload);
110
+ };
111
+ const cancelDebouncedSave = () => {
112
+ if (saveTimer === null)
113
+ return;
114
+ clearTimeout(saveTimer);
115
+ saveTimer = null;
116
+ };
117
+ const saveChangedPayloads = async () => {
118
+ for (const [key, payload] of getPayloads()) {
119
+ if (savedPayloadByKey.get(key) === payload)
120
+ continue;
121
+ if (await Save.set(key, JSON.parse(payload))) {
122
+ savedPayloadByKey.set(key, payload);
123
+ }
124
+ }
125
+ };
126
+ const trackSave = (savePromise) => {
127
+ const trackedPromise = savePromise.catch(() => undefined).finally(() => {
128
+ if (runningSave === trackedPromise)
129
+ runningSave = null;
130
+ });
131
+ runningSave = trackedPromise;
132
+ return savePromise;
133
+ };
134
+ const queueSave = () => {
135
+ if (runningSave === null)
136
+ return trackSave(saveChangedPayloads());
137
+ return trackSave(runningSave.then(saveChangedPayloads, saveChangedPayloads));
138
+ };
102
139
  return {
103
140
  async load() {
104
141
  for (let [k, v] of arr) {
@@ -108,13 +145,25 @@ export function CacheFuncMapBase(arr, Save) {
108
145
  ObjectStringToDate(t);
109
146
  addDataToMap(t, v);
110
147
  }
148
+ rememberCurrentPayloads();
111
149
  },
112
150
  async save() {
113
- for (let [k, v] of arr) {
114
- await Save.set(k, [...v.entries()]);
115
- }
151
+ await queueSave();
152
+ },
153
+ saveDebounced(delay = 800) {
154
+ cancelDebouncedSave();
155
+ saveTimer = setTimeout(() => {
156
+ saveTimer = null;
157
+ void queueSave();
158
+ }, delay);
159
+ },
160
+ async flush() {
161
+ cancelDebouncedSave();
162
+ await queueSave();
116
163
  },
117
164
  async clear() {
165
+ cancelDebouncedSave();
166
+ savedPayloadByKey.clear();
118
167
  for (let [k, v] of arr) {
119
168
  await Save.delete(k);
120
169
  }
@@ -0,0 +1,10 @@
1
+ /** Multiplexer for a single-slot callback API (onX(cb | null)): many subscribers share
2
+ * one slot instead of silently overwriting each other.
3
+ * bind(emit) is called lazily, ONCE, on the first on() - not at creation time, because
4
+ * the slot may still be taken by app initialization. */
5
+ export declare function createCallbackHub<Args extends any[]>(bind: (emit: (...a: Args) => void) => void): {
6
+ /** Subscribe; the returned function unsubscribes without touching other subscribers.
7
+ * The callback is registered BEFORE bind runs, so it also catches synchronous emits. */
8
+ on(cb: (...a: Args) => void): () => void;
9
+ count: () => number;
10
+ };
@@ -0,0 +1,23 @@
1
+ /** Multiplexer for a single-slot callback API (onX(cb | null)): many subscribers share
2
+ * one slot instead of silently overwriting each other.
3
+ * bind(emit) is called lazily, ONCE, on the first on() - not at creation time, because
4
+ * the slot may still be taken by app initialization. */
5
+ export function createCallbackHub(bind) {
6
+ const subs = new Set();
7
+ let bound = false;
8
+ // copy before iterating: an unsubscribe from inside a callback must not skip the rest
9
+ const emit = (...a) => { [...subs].forEach(cb => cb(...a)); };
10
+ return {
11
+ /** Subscribe; the returned function unsubscribes without touching other subscribers.
12
+ * The callback is registered BEFORE bind runs, so it also catches synchronous emits. */
13
+ on(cb) {
14
+ subs.add(cb);
15
+ if (!bound) {
16
+ bound = true;
17
+ bind(emit);
18
+ }
19
+ return () => { subs.delete(cb); };
20
+ },
21
+ count: () => subs.size,
22
+ };
23
+ }
@@ -1,6 +1,7 @@
1
1
  export * from './applyTransactionAsyncUpdate';
2
2
  export * from './arrayPromise';
3
3
  export * from './cache';
4
+ export * from './callbackHub';
4
5
  export * from './inputAutoStep';
5
6
  export * from './mapMemory';
6
7
  export * from './pageVisibilityContext';
@@ -1,6 +1,7 @@
1
1
  export * from './applyTransactionAsyncUpdate';
2
2
  export * from './arrayPromise';
3
3
  export * from './cache';
4
+ export * from './callbackHub';
4
5
  export * from './inputAutoStep';
5
6
  export * from './mapMemory';
6
7
  export * from './pageVisibilityContext';
@@ -10,6 +10,8 @@ export declare function staticGetById<T extends object>(key: any, def: T, id: st
10
10
  export declare const Cash: {
11
11
  load(): Promise<void>;
12
12
  save(): Promise<void>;
13
+ saveDebounced(delay?: number): void;
14
+ flush(): Promise<void>;
13
15
  clear(): Promise<void>;
14
16
  getArr: [k: string, v: Map<string, unknown>][];
15
17
  };
@@ -28,5 +30,6 @@ export declare const MemoryMap: {
28
30
  height?: number | string;
29
31
  width?: number | string;
30
32
  }>;
33
+ rightMenu: Map<string, import("../components/Menu/RightMenuStore").MenuRightSavedState>;
31
34
  other: Map<string, object>;
32
35
  };
@@ -1,6 +1,7 @@
1
1
  import { deepClone } from "wenay-common2";
2
2
  import { ExRNDMap3 } from "../components/Dnd/RNDFunc3";
3
3
  import { mapResiReact } from "../components/Dnd/Resizable";
4
+ import { mapRightMenu } from "../components/Menu/RightMenuStore";
4
5
  import { CacheFuncMap } from "./cache";
5
6
  const staticProps = new Map();
6
7
  export function staticSet(key, data) {
@@ -63,10 +64,12 @@ export function staticGetById(key, def, id) {
63
64
  export const Cash = CacheFuncMap([
64
65
  ["mapResiReact", mapResiReact],
65
66
  ["ExRNDMap3", ExRNDMap3],
67
+ ["mapRightMenu", mapRightMenu],
66
68
  ["staticProps", staticProps]
67
69
  ]);
68
70
  export const MemoryMap = {
69
71
  rnd: ExRNDMap3,
70
72
  resize: mapResiReact,
73
+ rightMenu: mapRightMenu,
71
74
  other: staticProps
72
75
  };
@@ -136,11 +136,11 @@ input:invalid {
136
136
 
137
137
  /* Menu */
138
138
  .MenuR {
139
- box-shadow: 0 0 20px 14px rgba(34, 60, 80, 0.2);
139
+ box-shadow: var(--menu-shadow);
140
140
  margin-top: 4px;
141
141
  min-width: 80px;
142
142
  width: max-content;
143
- background: rgba(12, 12, 12, 0.91);
143
+ background: var(--menu-bg-color);
144
144
  }
145
145
 
146
146
  .maxSize {
@@ -319,28 +319,138 @@ body {
319
319
  margin-left: -1px;
320
320
  }
321
321
 
322
+ /* DivRnd3 window chrome. All visuals go through --wnd-* tokens (tokens.css);
323
+ defaults reproduce the legacy look, apps re-skin via :root[data-theme] token overrides. */
324
+ .wenayWnd {
325
+ background: var(--wnd-bg);
326
+ border: var(--wnd-border);
327
+ border-radius: var(--wnd-radius);
328
+ box-shadow: var(--wnd-shadow);
329
+ }
330
+ /* Header drag zone. Color/font tokens are undeclared by default: an unresolved var()
331
+ makes the property fall back to its inherited value, i.e. exactly the legacy look. */
332
+ .wenayWndHeader {
333
+ user-select: none;
334
+ cursor: grabbing;
335
+ height: var(--wnd-header-height, auto);
336
+ padding: var(--wnd-header-padding);
337
+ background: var(--wnd-header-bg);
338
+ border-bottom: var(--wnd-header-border);
339
+ border-top-left-radius: var(--wnd-radius);
340
+ border-top-right-radius: var(--wnd-radius);
341
+ color: var(--wnd-header-color, inherit);
342
+ font-size: var(--wnd-header-font-size, inherit);
343
+ letter-spacing: var(--wnd-header-letter-spacing, inherit);
344
+ text-transform: var(--wnd-header-transform, inherit);
345
+ }
346
+ /* Default header content when no header element is passed (striped drag strip) */
347
+ .wenayWndHeaderDef {
348
+ height: var(--wnd-header-height, 20px);
349
+ width: 100%;
350
+ background: var(--wnd-header-stripes);
351
+ }
352
+
322
353
  /* Window close button (DivRnd3 and others) */
323
354
  .wenayCloseBtn {
324
- width: 28px;
325
- height: 28px;
355
+ width: var(--wnd-close-size, 28px);
356
+ height: var(--wnd-close-size, 28px);
357
+ /* keep the clickable zone >= 24x24 whatever the theme sets */
358
+ min-width: 24px;
359
+ min-height: 24px;
326
360
  display: flex;
327
361
  align-items: center;
328
362
  justify-content: center;
329
363
  border-radius: 50%;
330
- background: var(--color-bg-light);
331
- border: 1px solid var(--color-border-common);
332
- color: var(--color-text-base);
364
+ background: var(--wnd-close-bg, var(--color-bg-light));
365
+ border: var(--wnd-close-border, 1px solid var(--color-border-common));
366
+ color: var(--wnd-close-color, var(--color-text-base));
333
367
  cursor: pointer;
334
368
  user-select: none;
335
- box-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
369
+ box-shadow: var(--wnd-close-shadow, 0 2px 6px rgba(0, 0, 0, 0.35));
336
370
  transition: background 0.15s, color 0.15s, transform 0.15s;
337
371
  }
338
372
  .wenayCloseBtn:hover {
339
- background: #e5484d;
340
- border-color: #e5484d;
341
- color: #fff;
373
+ background: var(--wnd-close-hover-bg, #e5484d);
374
+ border-color: var(--wnd-close-hover-bg, #e5484d);
375
+ color: var(--wnd-close-hover, #fff);
342
376
  transform: scale(1.08);
343
377
  }
344
378
  .wenayCloseBtn:active {
345
379
  transform: scale(0.95);
346
380
  }
381
+ /* Positioning of the close button inside DivRnd3. Kept separate from .wenayCloseBtn,
382
+ which may be used outside windows and must not become position:absolute there. */
383
+ .wenayWndClose {
384
+ position: absolute;
385
+ top: var(--wnd-close-top, -12px);
386
+ right: var(--wnd-close-right, -12px);
387
+ }
388
+
389
+ /* Settings dialog (SettingsDialog). Themed via --dlg-* (tokens.css), dark defaults. */
390
+ .wenayDlgScrim {
391
+ position: fixed;
392
+ inset: 0;
393
+ z-index: var(--wenay-z-modal, 9999);
394
+ background: var(--dlg-scrim, rgba(0, 0, 0, 0.5));
395
+ display: flex;
396
+ align-items: center;
397
+ justify-content: center;
398
+ }
399
+ .wenayDlg {
400
+ position: relative;
401
+ display: flex;
402
+ width: 640px;
403
+ height: 420px;
404
+ max-width: 92vw;
405
+ max-height: 82vh;
406
+ background: var(--dlg-bg, var(--color-bg-dark));
407
+ border: var(--dlg-border, 1px solid var(--color-border-common));
408
+ border-radius: var(--dlg-radius, 10px);
409
+ box-shadow: var(--dlg-shadow, 0 12px 40px rgba(0, 0, 0, 0.5));
410
+ color: var(--color-text-base);
411
+ overflow: hidden;
412
+ }
413
+ .wenayDlgNav {
414
+ width: var(--dlg-nav-width, 168px);
415
+ flex: none;
416
+ background: var(--dlg-nav-bg, var(--color-bg-light));
417
+ padding: 10px 8px;
418
+ display: flex;
419
+ flex-direction: column;
420
+ gap: 4px;
421
+ overflow-y: auto;
422
+ }
423
+ .wenayDlgContent {
424
+ position: relative;
425
+ flex: 1;
426
+ min-width: 0;
427
+ padding: 14px 40px 14px 16px;
428
+ overflow: auto;
429
+ }
430
+ /* Positioning of the close button in the dialog's top-right corner (over the content column).
431
+ A direct child of .wenayDlg, NOT of the scrollable content, so it does not scroll away. */
432
+ .wenayDlgClose {
433
+ position: absolute;
434
+ top: 8px;
435
+ right: 8px;
436
+ }
437
+ /* Minimal default section/segment buttons; apps override via sectionClassName /
438
+ sectionActiveClassName (SettingsDialog) and className / activeClassName (PlacementSetting) */
439
+ .wenayDlgSection,
440
+ .wenaySegBtn {
441
+ padding: 6px 10px;
442
+ border-radius: 6px;
443
+ cursor: pointer;
444
+ user-select: none;
445
+ text-align: left;
446
+ color: inherit;
447
+ }
448
+ .wenayDlgSection:hover,
449
+ .wenaySegBtn:hover {
450
+ background: rgba(255, 255, 255, 0.08);
451
+ }
452
+ .wenayDlgSectionActive,
453
+ .wenaySegBtnActive {
454
+ background: rgba(255, 255, 255, 0.16);
455
+ color: #fff;
456
+ }
@@ -29,6 +29,49 @@
29
29
  --menu-item-hover-color: #101010;
30
30
  --menu-item-hover-bg-color: var(--base-color-hover);
31
31
 
32
+ /* Menu shadow (was a literal in .MenuR) */
33
+ --menu-shadow: 0 0 20px 14px rgba(34, 60, 80, 0.2);
34
+
35
+ /* Window (DivRnd3) chrome. Defaults = legacy look, so apps without themes do not change.
36
+ Apps re-skin via :root[data-theme='...'] overrides, same contract as --color-* / --menu-*.
37
+ Intentionally NOT declared here (fall back to inherit/auto until a theme declares them):
38
+ --wnd-header-height, --wnd-header-color, --wnd-header-font-size,
39
+ --wnd-header-letter-spacing, --wnd-header-transform */
40
+ --wnd-bg: transparent;
41
+ --wnd-border: none;
42
+ --wnd-radius: 0;
43
+ --wnd-shadow: none;
44
+
45
+ /* Header drag zone: wrapper background (solid in themes) vs default striped strip.
46
+ Themes typically set --wnd-header-bg to a panel color and --wnd-header-stripes to none. */
47
+ --wnd-header-bg: transparent;
48
+ --wnd-header-stripes: repeating-linear-gradient(139deg, hsla(0, 0%, 100%, 0.1), hsla(0, 0%, 100%, 0.1) 15px, transparent 0, transparent 30px);
49
+ --wnd-header-border: none;
50
+ --wnd-header-padding: 0;
51
+
52
+ /* Close button. Legacy: floating circle at the window's top-right corner.
53
+ Themes move it into the header (top/right offsets) and strip the circle chrome.
54
+ Clickable zone is clamped to >= 24x24 in CSS regardless of --wnd-close-size. */
55
+ --wnd-close-size: 28px;
56
+ --wnd-close-top: -12px;
57
+ --wnd-close-right: -12px;
58
+ --wnd-close-bg: var(--color-bg-light);
59
+ --wnd-close-border: 1px solid var(--color-border-common);
60
+ --wnd-close-shadow: 0 2px 6px rgba(0, 0, 0, 0.35);
61
+ --wnd-close-color: var(--color-text-base);
62
+ --wnd-close-hover: #fff;
63
+ --wnd-close-hover-bg: #e5484d;
64
+
65
+ /* Settings dialog (SettingsDialog). Dark defaults close to current apps;
66
+ apps re-skin via :root[data-theme='...'] overrides, same contract as --wnd-*. */
67
+ --dlg-scrim: rgba(0, 0, 0, 0.5);
68
+ --dlg-bg: var(--color-bg-dark);
69
+ --dlg-border: 1px solid var(--color-border-common);
70
+ --dlg-radius: 10px;
71
+ --dlg-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
72
+ --dlg-nav-bg: var(--color-bg-light);
73
+ --dlg-nav-width: 168px;
74
+
32
75
  /* Layers */
33
76
  --wenay-z-modal: 9999;
34
77
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wenay-react2",
3
- "version": "1.0.27",
3
+ "version": "1.0.29",
4
4
  "description": "Common react",
5
5
  "strict": true,
6
6
  "main": "dist/index.js",