wenay-react2 1.0.37 → 1.0.38
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 +2 -0
- package/lib/common/src/components/Modal/ModalContextProvider.js +1 -1
- package/lib/common/src/components/Settings/SettingsDialog.d.ts +12 -2
- package/lib/common/src/components/Settings/SettingsDialog.js +401 -16
- package/lib/common/src/components/Toolbar/Toolbar.d.ts +21 -2
- package/lib/common/src/components/Toolbar/Toolbar.js +84 -14
- package/lib/common/src/grid/columnState/CardList.js +5 -2
- package/lib/common/src/grid/columnState/ColumnDots.js +8 -18
- package/lib/common/src/grid/columnState/columnState.d.ts +4 -0
- package/lib/common/src/grid/columnState/columnState.js +39 -14
- package/lib/common/src/hooks/useReplay.d.ts +54 -0
- package/lib/common/src/hooks/useReplay.js +222 -0
- package/lib/common/src/logs/logStyles.d.ts +13 -0
- package/lib/common/src/logs/logStyles.js +18 -0
- package/lib/common/src/logs/logs.js +5 -7
- package/lib/common/src/logs/logsContext.js +22 -26
- package/lib/common/src/menu/menu.d.ts +2 -1
- package/lib/common/src/menu/menu.js +28 -21
- package/lib/common/src/styles/tokens.d.ts +14 -1
- package/lib/common/src/styles/tokens.js +14 -1
- package/lib/common/src/utils/index.d.ts +1 -0
- package/lib/common/src/utils/index.js +1 -0
- package/lib/common/src/utils/searchHistory.d.ts +14 -0
- package/lib/common/src/utils/searchHistory.js +59 -0
- package/lib/style/menuRight.css +2 -2
- package/lib/style/style.css +563 -54
- package/lib/style/tokens.css +15 -3
- package/package.json +2 -2
|
@@ -15,7 +15,9 @@ const densities = { list: [
|
|
|
15
15
|
/** Icon with a text fallback: no glyph = the first letters of short/title
|
|
16
16
|
* (an "PR"-style pseudo-icon), so icon densities work for icon-less items. */
|
|
17
17
|
export function toolbarItemIcon(item) {
|
|
18
|
-
|
|
18
|
+
// No real icon -> a text pseudo-icon (first 3 letters). Rendered a touch lighter
|
|
19
|
+
// (lower weight + opacity) so this fallback does not read as a bold label.
|
|
20
|
+
return item.icon ?? _jsx("span", { style: { fontSize: 10, fontWeight: 600, letterSpacing: 0.5, textTransform: 'uppercase', opacity: 0.65 }, children: (item.short ?? item.title).slice(0, 3) });
|
|
19
21
|
}
|
|
20
22
|
function itemIconOnly(item) {
|
|
21
23
|
return toolbarItemIcon(item);
|
|
@@ -52,7 +54,9 @@ function itemContent(item, densityKey) {
|
|
|
52
54
|
/** Reserved visible-map key for the bar's settings (gear) button: not part of
|
|
53
55
|
* order (the gear always sits at the bar edge), but toggleable like an item. */
|
|
54
56
|
const SETTINGS_KEY = '__settings';
|
|
57
|
+
const RESET_KEY = '__reset';
|
|
55
58
|
export function createToolbar(opts) {
|
|
59
|
+
const resetDefaultVisible = () => opts.resetItem !== false && opts.resetItem?.defaultVisible === true;
|
|
56
60
|
const defConfig = () => ({
|
|
57
61
|
order: opts.def?.order?.slice() ?? opts.items.map(i => i.key),
|
|
58
62
|
visible: opts.def?.visible ? { ...opts.def.visible } : Object.fromEntries(opts.items.map(i => [i.key, i.defaultVisible != false])),
|
|
@@ -63,19 +67,36 @@ export function createToolbar(opts) {
|
|
|
63
67
|
// ext is fixed for the controller's lifetime, so hook call order inside
|
|
64
68
|
// useConfig/Bar/Settings never changes for a given toolbar instance
|
|
65
69
|
const ext = opts.source;
|
|
70
|
+
const sourceMode = ext ? (opts.sourceMode ?? 'orderVisible') : 'orderVisible';
|
|
66
71
|
// outside edits of the external config (grid drags...) must reach the
|
|
67
72
|
// toolbar's own subscribers too; module-lifetime subscription by design
|
|
68
73
|
ext?.onChange?.(() => emitChange(normalize()));
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
function sameOrder(a, b) {
|
|
75
|
+
return a.length == b.length && a.every((k, i) => k == b[i]);
|
|
76
|
+
}
|
|
77
|
+
function sourceKeySet(raw, known) {
|
|
78
|
+
return new Set((Array.isArray(raw?.order) ? raw.order : []).filter(k => known.has(k)));
|
|
79
|
+
}
|
|
80
|
+
function mergeSourceOrder(localOrder, rawSourceOrder, sourceKeys) {
|
|
81
|
+
if (!sourceKeys.size)
|
|
82
|
+
return localOrder;
|
|
83
|
+
const sourceOrder = rawSourceOrder.filter(k => sourceKeys.has(k));
|
|
84
|
+
let i = 0;
|
|
85
|
+
return localOrder.map(k => sourceKeys.has(k) ? (sourceOrder[i++] ?? k) : k);
|
|
86
|
+
}
|
|
71
87
|
/** The persisted state may be stale or partial (older app version, removed
|
|
72
88
|
* items, an unregistered density) - never crash, never drop user data that
|
|
73
89
|
* still applies: unknown keys are filtered out, missing items are appended
|
|
74
90
|
* (default-visible), fixed items are pinned back to their descriptor index. */
|
|
75
91
|
function normalize() {
|
|
76
|
-
const raw = rawList();
|
|
77
92
|
const known = new Set(opts.items.map(i => i.key));
|
|
78
|
-
const
|
|
93
|
+
const extRaw = ext?.getConfig();
|
|
94
|
+
const localRaw = { order: st.order, visible: st.visible };
|
|
95
|
+
const raw = ext && sourceMode == 'orderVisible' ? extRaw : localRaw;
|
|
96
|
+
const sourceKeys = ext && sourceMode == 'order' ? sourceKeySet(extRaw, known) : new Set();
|
|
97
|
+
const rawOrder = ext && sourceMode == 'order'
|
|
98
|
+
? mergeSourceOrder(Array.isArray(st.order) ? st.order : [], Array.isArray(extRaw?.order) ? extRaw.order : [], sourceKeys)
|
|
99
|
+
: Array.isArray(raw.order) ? raw.order : [];
|
|
79
100
|
const order = rawOrder.filter(k => known.has(k) && !opts.items.find(i => i.key == k)?.fixed);
|
|
80
101
|
for (const it of opts.items)
|
|
81
102
|
if (!it.fixed && order.indexOf(it.key) == -1)
|
|
@@ -88,23 +109,33 @@ export function createToolbar(opts) {
|
|
|
88
109
|
const visible = {};
|
|
89
110
|
for (const it of opts.items)
|
|
90
111
|
visible[it.key] = it.fixed ? true : (rawVisible[it.key] ?? it.defaultVisible != false);
|
|
91
|
-
// the gear
|
|
112
|
+
// the gear/reset flags are toolbar-local: an external source only owns items
|
|
92
113
|
const gearRaw = (ext ? st.visible : rawVisible) ?? {};
|
|
93
114
|
visible[SETTINGS_KEY] = typeof gearRaw[SETTINGS_KEY] == 'boolean' ? gearRaw[SETTINGS_KEY] : true;
|
|
115
|
+
if (opts.resetItem !== false)
|
|
116
|
+
visible[RESET_KEY] = typeof gearRaw[RESET_KEY] == 'boolean' ? gearRaw[RESET_KEY] : resetDefaultVisible();
|
|
94
117
|
const density = typeof st.density == 'string' && densities.list.some(d => d.key == st.density)
|
|
95
118
|
? st.density : (opts.def?.density ?? densities.list[0].key);
|
|
96
119
|
return { order, visible, density };
|
|
97
120
|
}
|
|
121
|
+
function metaVisible(next, key, def) {
|
|
122
|
+
return typeof next.visible[key] == 'boolean' ? next.visible[key] : def;
|
|
123
|
+
}
|
|
98
124
|
/** Every edit funnels through here: mutate the persisted object in place
|
|
99
125
|
* (identity is the updateBy/renderBy subscription key), announce, mark the
|
|
100
126
|
* cache dirty, emit outward. With an external source the items' order and
|
|
101
|
-
* visibility go THERE (its own change flow re-emits); density + gear
|
|
127
|
+
* visibility go THERE (its own change flow re-emits); density + gear/reset flags
|
|
102
128
|
* always stay in the toolbar's own store. */
|
|
103
129
|
function setConfig(next) {
|
|
104
|
-
if (ext) {
|
|
130
|
+
if (ext && sourceMode == 'orderVisible') {
|
|
105
131
|
const visible = { ...next.visible };
|
|
106
132
|
delete visible[SETTINGS_KEY];
|
|
107
|
-
|
|
133
|
+
delete visible[RESET_KEY];
|
|
134
|
+
st.visible = {
|
|
135
|
+
...st.visible,
|
|
136
|
+
[SETTINGS_KEY]: metaVisible(next, SETTINGS_KEY, true),
|
|
137
|
+
[RESET_KEY]: metaVisible(next, RESET_KEY, resetDefaultVisible()),
|
|
138
|
+
};
|
|
108
139
|
st.density = next.density;
|
|
109
140
|
renderBy(st);
|
|
110
141
|
memoryMarkDirty(opts.key);
|
|
@@ -113,6 +144,26 @@ export function createToolbar(opts) {
|
|
|
113
144
|
if (!ext.onChange)
|
|
114
145
|
emitChange(normalize());
|
|
115
146
|
}
|
|
147
|
+
else if (ext && sourceMode == 'order') {
|
|
148
|
+
const extRaw = ext.getConfig();
|
|
149
|
+
const known = new Set(opts.items.map(i => i.key));
|
|
150
|
+
const sourceKeys = sourceKeySet(extRaw, known);
|
|
151
|
+
const curSourceOrder = (Array.isArray(extRaw.order) ? extRaw.order : []).filter(k => sourceKeys.has(k));
|
|
152
|
+
const nextSourceOrder = next.order.filter(k => sourceKeys.has(k));
|
|
153
|
+
st.order = next.order.slice();
|
|
154
|
+
st.visible = { ...next.visible };
|
|
155
|
+
st.density = next.density;
|
|
156
|
+
renderBy(st);
|
|
157
|
+
memoryMarkDirty(opts.key);
|
|
158
|
+
const orderChanged = !sameOrder(curSourceOrder, nextSourceOrder);
|
|
159
|
+
if (orderChanged)
|
|
160
|
+
ext.setConfig({
|
|
161
|
+
order: nextSourceOrder,
|
|
162
|
+
visible: extRaw.visible && typeof extRaw.visible == 'object' ? { ...extRaw.visible } : {},
|
|
163
|
+
});
|
|
164
|
+
if (!orderChanged || !ext.onChange)
|
|
165
|
+
emitChange(normalize());
|
|
166
|
+
}
|
|
116
167
|
else {
|
|
117
168
|
st.order = next.order.slice();
|
|
118
169
|
st.visible = { ...next.visible };
|
|
@@ -149,9 +200,25 @@ export function createToolbar(opts) {
|
|
|
149
200
|
.map(it => ({ item: it, density: cfg.density, content: itemContent(it, cfg.density) }));
|
|
150
201
|
}
|
|
151
202
|
const reset = () => setConfig(defConfig());
|
|
203
|
+
function setOrder(order) {
|
|
204
|
+
setConfig({ ...getConfig(), order: order.slice() });
|
|
205
|
+
}
|
|
206
|
+
function show(key, on) {
|
|
207
|
+
const cfg = getConfig();
|
|
208
|
+
setConfig({ ...cfg, visible: { ...cfg.visible, [key]: on } });
|
|
209
|
+
}
|
|
210
|
+
function setDensity(density) {
|
|
211
|
+
setConfig({ ...getConfig(), density });
|
|
212
|
+
}
|
|
213
|
+
const showSettings = (on) => show(SETTINGS_KEY, on);
|
|
214
|
+
const showReset = (on) => show(RESET_KEY, on);
|
|
152
215
|
const settingsTitle = () => opts.settingsItem?.title ?? 'Toolbar settings';
|
|
153
216
|
const settingsIcon = () => opts.settingsItem?.icon ??
|
|
154
217
|
_jsx("svg", { width: '14', height: '14', viewBox: '0 0 16 16', "aria-hidden": 'true', children: _jsx("path", { d: 'M8 5.2 A2.8 2.8 0 1 0 8 10.8 A2.8 2.8 0 1 0 8 5.2 M8 1.2 L8.6 3.4 A4.8 4.8 0 0 1 10.6 4.2 L12.8 3.2 L14 5.6 L12.2 7 A4.8 4.8 0 0 1 12.2 9 L14 10.4 L12.8 12.8 L10.6 11.8 A4.8 4.8 0 0 1 8.6 12.6 L8 14.8 L7.4 12.6 A4.8 4.8 0 0 1 5.4 11.8 L3.2 12.8 L2 10.4 L3.8 9 A4.8 4.8 0 0 1 3.8 7 L2 5.6 L3.2 3.2 L5.4 4.2 A4.8 4.8 0 0 1 7.4 3.4 Z', fill: 'none', stroke: 'currentColor', strokeWidth: '1.2', strokeLinejoin: 'round' }) });
|
|
218
|
+
const resetOpts = () => opts.resetItem === false ? undefined : opts.resetItem;
|
|
219
|
+
const resetTitle = () => resetOpts()?.title ?? 'Reset toolbar';
|
|
220
|
+
const resetIcon = () => resetOpts()?.icon ??
|
|
221
|
+
_jsx("svg", { width: '14', height: '14', viewBox: '0 0 16 16', "aria-hidden": 'true', children: _jsx("path", { d: 'M3.2 5.5 A5.2 5.2 0 1 1 4.1 11.7 M3.2 5.5 H6.4 M3.2 5.5 V2.3', fill: 'none', stroke: 'currentColor', strokeWidth: '1.4', strokeLinecap: 'round', strokeLinejoin: 'round' }) });
|
|
155
222
|
function moveKey(cfg, key, to) {
|
|
156
223
|
const order = cfg.order.slice();
|
|
157
224
|
const from = order.indexOf(key);
|
|
@@ -166,20 +233,23 @@ export function createToolbar(opts) {
|
|
|
166
233
|
}
|
|
167
234
|
/** The live bar. settings=true adds a built-in gear opening Settings in a
|
|
168
235
|
* local popover - optional, some consumers mount Settings only in the
|
|
169
|
-
* global settings menu.
|
|
170
|
-
*
|
|
236
|
+
* global settings menu. reset permits rendering the reset button when its
|
|
237
|
+
* pseudo-control is enabled (hidden by default). popAlign: which bar edge
|
|
238
|
+
* the popover sticks to - 'right' (default, for bars in a top-right corner)
|
|
239
|
+
* or 'left'. */
|
|
171
240
|
function Bar(p = {}) {
|
|
172
241
|
useSubscribe();
|
|
173
242
|
updateBy(densities);
|
|
174
243
|
const cfg = normalize();
|
|
175
244
|
const [open, setOpen] = useState(false);
|
|
176
245
|
const byKey = new Map(opts.items.map(i => [i.key, i]));
|
|
246
|
+
const resetOn = opts.resetItem !== false && (p.reset ?? p.settings ?? false) && cfg.visible[RESET_KEY] != false;
|
|
177
247
|
return _jsxs("div", { className: p.className ?? 'wenayTb', children: [cfg.order.map(k => {
|
|
178
248
|
const it = byKey.get(k);
|
|
179
249
|
if (!it || cfg.visible[k] == false)
|
|
180
250
|
return null;
|
|
181
251
|
return _jsx("div", { className: 'wenayTbItem', title: cfg.density == 'icon' ? it.title : undefined, onClick: it.onClick, children: itemContent(it, cfg.density) }, k);
|
|
182
|
-
}), p.settings && cfg.visible[SETTINGS_KEY] != false && _jsxs(OutsideClickArea, { className: 'wenayTbGear', status: open, outsideClick: () => setOpen(false), children: [_jsx("div", { className: 'wenayTbItem', title: settingsTitle(), onClick: () => setOpen(v => !v), children: settingsIcon() }), open && _jsx("div", { className: p.popAlign == 'left' ? 'wenayTbPop wenayTbPopLeft' : 'wenayTbPop', children: _jsx(Settings, {}) })] })] });
|
|
252
|
+
}), resetOn && _jsx("div", { className: 'wenayTbItem', title: resetTitle(), onClick: () => reset(), children: resetIcon() }), p.settings && cfg.visible[SETTINGS_KEY] != false && _jsxs(OutsideClickArea, { className: 'wenayTbGear', status: open, outsideClick: () => setOpen(false), children: [_jsx("div", { className: 'wenayTbItem', title: settingsTitle(), onClick: () => setOpen(v => !v), children: settingsIcon() }), open && _jsx("div", { className: p.popAlign == 'left' ? 'wenayTbPop wenayTbPopLeft' : 'wenayTbPop', children: _jsx(Settings, {}) })] })] });
|
|
183
253
|
}
|
|
184
254
|
/** The pure editor over config: density segments + one row per item
|
|
185
255
|
* (visibility checkbox, icon preview, title, drag handle). Reorder rides the
|
|
@@ -237,11 +307,11 @@ export function createToolbar(opts) {
|
|
|
237
307
|
e.currentTarget.querySelector('.wenayTbHandle')?.focus();
|
|
238
308
|
r.props.onMouseDown(e);
|
|
239
309
|
}, onTouchStart: it.fixed ? undefined : r.props.onTouchStart, children: [_jsx("input", { type: 'checkbox', disabled: it.fixed, checked: cfg.visible[k] != false, onChange: () => setConfig({ ...cfg, visible: { ...cfg.visible, [k]: !(cfg.visible[k] != false) } }) }), _jsx("span", { className: 'wenayTbIcon', children: toolbarItemIcon(it) }), _jsx("span", { className: 'wenayTbRowTitle', children: it.title }), !it.fixed && _jsx("div", { className: 'wenayTbHandle', tabIndex: 0, title: 'Drag or arrow keys to reorder', onKeyDown: e => onHandleKey(k, e), children: "\u283F" })] }, k);
|
|
240
|
-
}) }), _jsxs("div", { className: 'wenayTbRow wenayTbRowMeta', children: [_jsx("input", { type: 'checkbox', checked: cfg.visible[SETTINGS_KEY] != false, onChange: () => setConfig({ ...cfg, visible: { ...cfg.visible, [SETTINGS_KEY]: !(cfg.visible[SETTINGS_KEY] != false) } }) }), _jsx("span", { className: 'wenayTbIcon', children: settingsIcon() }), _jsx("span", { className: 'wenayTbRowTitle', children: settingsTitle() })] })] });
|
|
310
|
+
}) }), _jsxs("div", { className: 'wenayTbRow wenayTbRowMeta', children: [_jsx("input", { type: 'checkbox', checked: cfg.visible[SETTINGS_KEY] != false, onChange: () => setConfig({ ...cfg, visible: { ...cfg.visible, [SETTINGS_KEY]: !(cfg.visible[SETTINGS_KEY] != false) } }) }), _jsx("span", { className: 'wenayTbIcon', children: settingsIcon() }), _jsx("span", { className: 'wenayTbRowTitle', children: settingsTitle() })] }), opts.resetItem !== false && _jsxs("div", { className: 'wenayTbRow wenayTbRowMeta', children: [_jsx("input", { type: 'checkbox', checked: cfg.visible[RESET_KEY] != false, onChange: () => setConfig({ ...cfg, visible: { ...cfg.visible, [RESET_KEY]: !(cfg.visible[RESET_KEY] != false) } }) }), _jsx("span", { className: 'wenayTbIcon', children: resetIcon() }), _jsx("span", { className: 'wenayTbRowTitle', children: resetTitle() }), _jsx("button", { type: 'button', className: 'wenayTbMetaAction', title: resetTitle(), onClick: () => reset(), children: resetIcon() })] })] });
|
|
241
311
|
}
|
|
242
312
|
return {
|
|
243
313
|
Bar,
|
|
244
314
|
Settings,
|
|
245
|
-
api: { useConfig, useItems, getConfig, setConfig, reset, onChange },
|
|
315
|
+
api: { useConfig, useItems, getConfig, setConfig, setOrder, show, setDensity, showSettings, showReset, reset, onChange },
|
|
246
316
|
};
|
|
247
317
|
}
|
|
@@ -10,6 +10,9 @@ function cmpValues(a, b) {
|
|
|
10
10
|
return 1;
|
|
11
11
|
return String(a).localeCompare(String(b));
|
|
12
12
|
}
|
|
13
|
+
function cx(parts) {
|
|
14
|
+
return parts.filter(Boolean).join(' ');
|
|
15
|
+
}
|
|
13
16
|
export function CardList(p) {
|
|
14
17
|
const cfg = p.state.api.useConfig();
|
|
15
18
|
const cols = p.state.columns;
|
|
@@ -20,9 +23,9 @@ export function CardList(p) {
|
|
|
20
23
|
const fieldKeys = keys.filter(k => k != titleKey && k != accentKey);
|
|
21
24
|
const value = (key, row) => p.renderValue?.(key, row) ?? String(row[key] ?? '');
|
|
22
25
|
const rows = [...p.data];
|
|
23
|
-
if (cfg.sort) {
|
|
26
|
+
if (cfg.sort) {
|
|
24
27
|
const { key, dir } = cfg.sort;
|
|
25
28
|
rows.sort((a, b) => cmpValues(a[key], b[key]) * (dir == 'asc' ? 1 : -1));
|
|
26
29
|
}
|
|
27
|
-
return _jsx("div", { className: p.className, style:
|
|
30
|
+
return _jsx("div", { className: cx(['wenayCardList', p.className]), style: p.style, children: rows.map((row, i) => (_jsxs("div", { className: 'wenayCardListItem', children: [_jsxs("div", { className: cx(['wenayCardListHeader', fieldKeys.length == 0 && 'wenayCardListHeader_compact']), children: [_jsx("b", { className: 'wenayCardListTitle', children: titleKey ? value(titleKey, row) : '' }), accentKey && _jsx("span", { className: 'wenayCardListAccent', children: value(accentKey, row) })] }), fieldKeys.map(k => (_jsxs("div", { className: 'wenayCardListField', children: [_jsx("span", { className: 'wenayCardListLabel', children: byKey.get(k)?.title ?? k }), _jsx("span", { className: 'wenayCardListValue', children: value(k, row) })] }, k)))] }, p.getId?.(row, i) ?? i))) });
|
|
28
31
|
}
|
|
@@ -18,6 +18,9 @@ import { useRef, useState } from 'react';
|
|
|
18
18
|
* vertical and clearly upward - a horizontal drag or a page scroll never is. */
|
|
19
19
|
const REMOVE_DY = 32;
|
|
20
20
|
const MOVE_SLOP = 4;
|
|
21
|
+
function cx(parts) {
|
|
22
|
+
return parts.filter(Boolean).join(' ');
|
|
23
|
+
}
|
|
21
24
|
export function ColumnDots(p) {
|
|
22
25
|
const cfg = p.state.api.useConfig();
|
|
23
26
|
const cols = p.state.columns;
|
|
@@ -62,14 +65,14 @@ export function ColumnDots(p) {
|
|
|
62
65
|
setDrag(null);
|
|
63
66
|
if (!g)
|
|
64
67
|
return;
|
|
65
|
-
if (!g.moved) {
|
|
68
|
+
if (!g.moved) {
|
|
66
69
|
setSelected(g.key);
|
|
67
70
|
return;
|
|
68
71
|
}
|
|
69
72
|
if (!st)
|
|
70
73
|
return;
|
|
71
74
|
const meta = byKey.get(g.key);
|
|
72
|
-
if (st.off) {
|
|
75
|
+
if (st.off) {
|
|
73
76
|
if (!meta?.fixed && visibleKeys.length > 1) {
|
|
74
77
|
p.state.api.show(g.key, false);
|
|
75
78
|
if (selected == g.key)
|
|
@@ -79,7 +82,6 @@ export function ColumnDots(p) {
|
|
|
79
82
|
}
|
|
80
83
|
const target = order[st.to];
|
|
81
84
|
if (target && target != g.key && cfg.visible[target] == false && !meta?.fixed) {
|
|
82
|
-
// the dot slides to another mark: that column takes this slot
|
|
83
85
|
p.state.api.setConfig({ ...cfg, visible: { ...cfg.visible, [g.key]: false, [target]: true } });
|
|
84
86
|
if (selected == g.key)
|
|
85
87
|
setSelected(target);
|
|
@@ -94,28 +96,16 @@ export function ColumnDots(p) {
|
|
|
94
96
|
setSelected(key);
|
|
95
97
|
}
|
|
96
98
|
const sortLabel = cfg.sort ? `${short(cfg.sort.key)} ${cfg.sort.dir == 'asc' ? '↑' : '↓'}` : 'off';
|
|
97
|
-
return _jsxs("div", { className: p.className, style:
|
|
99
|
+
return _jsxs("div", { className: cx(['wenayColDots', p.className]), style: p.style, children: [_jsxs("div", { className: 'wenayColDotsHead', children: [_jsxs("span", { className: 'wenayColDotsMeta', children: [visibleKeys.length, "/", max, " fields"] }), _jsx("span", { className: 'wenayColDotsSpacer' }), _jsxs("span", { className: 'wenayColDotsMeta', children: ["field: ", _jsx("b", { children: selected ? short(selected) : '—' })] }), _jsxs("button", { className: 'wenayColDotsSort', disabled: !selected, title: 'Sort by the selected field: asc -> desc -> off', onClick: () => selected && p.state.api.toggleSort(selected), children: ["\u21C5 sort: ", sortLabel] })] }), _jsxs("div", { ref: trackRef, className: 'wenayColDotsTrack', children: [_jsx("div", { className: 'wenayColDotsRail' }), order.map((k, i) => {
|
|
98
100
|
const vis = cfg.visible[k] != false;
|
|
99
101
|
const isSorted = cfg.sort?.key == k;
|
|
100
|
-
return _jsxs("div", { onPointerUp: () => tapMark(k),
|
|
102
|
+
return _jsxs("div", { onPointerUp: () => tapMark(k), className: cx(['wenayColDotsMark', vis ? 'wenayColDotsMark_on' : 'wenayColDotsMark_off']), style: { left: `${pct(i)}%`, cursor: !vis && visibleKeys.length < max ? 'pointer' : 'default' }, children: [isSorted && _jsx("div", { className: 'wenayColDotsSortMark', children: cfg.sort.dir == 'asc' ? '↑' : '↓' }), _jsx("div", { className: 'wenayColDotsMarkPin' }), _jsx("div", { className: 'wenayColDotsMarkLabel', children: short(k) })] }, 'm' + k);
|
|
101
103
|
}), order.map((k, i) => {
|
|
102
104
|
if (cfg.visible[k] == false)
|
|
103
105
|
return null;
|
|
104
106
|
const meta = byKey.get(k);
|
|
105
107
|
const d = drag?.key == k ? drag : null;
|
|
106
108
|
const removing = !!d?.off && !meta?.fixed && visibleKeys.length > 1;
|
|
107
|
-
return _jsx("div", { onPointerDown: e => downDot(k, e), onPointerMove: moveDot, onPointerUp: upDot, onPointerCancel: upDot, style: {
|
|
108
|
-
position: 'absolute', left: `${pct(i)}%`, top: 28, width: 44, height: 44,
|
|
109
|
-
margin: '-22px 0 0 -22px', display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
110
|
-
cursor: 'grab', zIndex: d ? 3 : 2,
|
|
111
|
-
transform: d ? `translate(${d.dx}px, ${removing ? d.dy : 0}px)` : undefined,
|
|
112
|
-
transition: d ? undefined : 'transform 0.15s ease',
|
|
113
|
-
opacity: removing ? 0.4 : 1,
|
|
114
|
-
}, children: _jsx("div", { style: {
|
|
115
|
-
width: 18, height: 18, borderRadius: 9,
|
|
116
|
-
background: selected == k ? '#0969da' : '#24292f',
|
|
117
|
-
border: meta?.fixed ? '2px solid #afb8c1' : selected == k ? '2px solid #b6d4fe' : '2px solid transparent',
|
|
118
|
-
boxShadow: d ? '0 3px 10px rgba(0,0,0,0.35)' : undefined,
|
|
119
|
-
} }) }, 'd' + k);
|
|
109
|
+
return _jsx("div", { onPointerDown: e => downDot(k, e), onPointerMove: moveDot, onPointerUp: upDot, onPointerCancel: upDot, className: cx(['wenayColDotsDotWrap', d && 'wenayColDotsDotWrap_dragging', removing && 'wenayColDotsDotWrap_removing']), style: { left: `${pct(i)}%`, transform: d ? `translate(${d.dx}px, ${removing ? d.dy : 0}px)` : undefined }, children: _jsx("div", { className: cx(['wenayColDotsDot', selected == k && 'wenayColDotsDot_selected', meta?.fixed && 'wenayColDotsDot_fixed']) }) }, 'd' + k);
|
|
120
110
|
})] })] });
|
|
121
111
|
}
|
|
@@ -78,6 +78,10 @@ export declare function createColumnState(opts: {
|
|
|
78
78
|
} | null;
|
|
79
79
|
isPresent: (key: string) => boolean;
|
|
80
80
|
setPresent: (keys: string[] | null) => void;
|
|
81
|
+
getPresentGate: () => {
|
|
82
|
+
[key: string]: true;
|
|
83
|
+
} | null;
|
|
84
|
+
setPresentGate: (keys: string[] | null) => void;
|
|
81
85
|
listSource: {
|
|
82
86
|
useConfig(): {
|
|
83
87
|
order: string[];
|
|
@@ -18,25 +18,50 @@ export function createColumnState(opts) {
|
|
|
18
18
|
});
|
|
19
19
|
const st = memoryGetOrCreate(opts.key, defConfig());
|
|
20
20
|
const [emitChange, onChange] = createListen();
|
|
21
|
-
/** Runtime-only, never persisted: which
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
/** Runtime-only, never persisted: actual = which keys the attached grid has;
|
|
22
|
+
* gate = optional app-level availability over a stable grid schema (standards,
|
|
23
|
+
* mode blocks). Consumers see actual AND gate as one presence map. */
|
|
24
|
+
const rt = {
|
|
25
|
+
present: null,
|
|
26
|
+
presentGate: null,
|
|
27
|
+
};
|
|
28
|
+
const keyMap = (keys) => keys ? Object.fromEntries(keys.map(k => [k, true])) : null;
|
|
29
|
+
const sameMap = (a, b) => JSON.stringify(a) == JSON.stringify(b);
|
|
30
|
+
function combinedPresent() {
|
|
31
|
+
if (!rt.present && !rt.presentGate)
|
|
32
|
+
return null;
|
|
33
|
+
const res = {};
|
|
34
|
+
for (const c of opts.columns) {
|
|
35
|
+
if ((!rt.present || rt.present[c.key]) && (!rt.presentGate || rt.presentGate[c.key]))
|
|
36
|
+
res[c.key] = true;
|
|
37
|
+
}
|
|
38
|
+
return res;
|
|
39
|
+
}
|
|
28
40
|
function setPresent(keys) {
|
|
29
|
-
const next =
|
|
30
|
-
if (
|
|
41
|
+
const next = keyMap(keys);
|
|
42
|
+
if (sameMap(next, rt.present))
|
|
31
43
|
return;
|
|
32
44
|
rt.present = next;
|
|
33
45
|
renderBy(rt);
|
|
34
46
|
}
|
|
35
|
-
|
|
36
|
-
|
|
47
|
+
function setPresentGate(keys) {
|
|
48
|
+
const next = keyMap(keys);
|
|
49
|
+
if (sameMap(next, rt.presentGate))
|
|
50
|
+
return;
|
|
51
|
+
rt.presentGate = next;
|
|
52
|
+
renderBy(rt);
|
|
53
|
+
applyToGrid();
|
|
54
|
+
}
|
|
55
|
+
const getPresent = combinedPresent;
|
|
56
|
+
const getPresentGate = () => rt.presentGate;
|
|
57
|
+
const isPresent = (key) => {
|
|
58
|
+
const p = combinedPresent();
|
|
59
|
+
return !p || p[key] == true;
|
|
60
|
+
};
|
|
61
|
+
const passesPresentGate = (key) => !rt.presentGate || rt.presentGate[key] == true;
|
|
37
62
|
function usePresent() {
|
|
38
63
|
updateBy(rt);
|
|
39
|
-
return
|
|
64
|
+
return combinedPresent();
|
|
40
65
|
}
|
|
41
66
|
/** The persisted state may be stale or partial (older app version, columns
|
|
42
67
|
* added/removed) - never crash, never drop user data that still applies:
|
|
@@ -162,7 +187,7 @@ export function createColumnState(opts) {
|
|
|
162
187
|
function toAgState(cfg) {
|
|
163
188
|
return cfg.order.map(k => ({
|
|
164
189
|
colId: k,
|
|
165
|
-
hide: cfg.visible[k] == false,
|
|
190
|
+
hide: cfg.visible[k] == false || !passesPresentGate(k),
|
|
166
191
|
width: cfg.width[k], // undefined = leave the grid's current width
|
|
167
192
|
sort: cfg.sort?.key == k ? cfg.sort.dir : null,
|
|
168
193
|
}));
|
|
@@ -278,7 +303,7 @@ export function createColumnState(opts) {
|
|
|
278
303
|
* (dots, cards, icon menus) render from these + the config. */
|
|
279
304
|
columns: opts.columns,
|
|
280
305
|
api: { getConfig, setConfig, useConfig, onChange, reset, show, move, setSort, toggleSort, visibleKeys,
|
|
281
|
-
getPresent, usePresent, isPresent, setPresent, listSource },
|
|
306
|
+
getPresent, usePresent, isPresent, setPresent, getPresentGate, setPresentGate, listSource },
|
|
282
307
|
grid: { attach, detach },
|
|
283
308
|
};
|
|
284
309
|
}
|
|
@@ -5,6 +5,8 @@ type StorePatch = Observe.StorePatch;
|
|
|
5
5
|
type ReplayEvent<Z extends any[]> = Replay.ReplayEvent<Z>;
|
|
6
6
|
type ReplayRemote<Z extends any[]> = Replay.ReplayRemote<Z>;
|
|
7
7
|
type StaleInfo = Replay.StaleInfo;
|
|
8
|
+
export type ReplayRouteEvent = Replay.ReplayRouteEvent;
|
|
9
|
+
export type ReplayRouteSwitchOptions = Replay.ReplayRouteSwitchOpts;
|
|
8
10
|
export type UseReplaySubscribeOptions = {
|
|
9
11
|
/** Start position for the first subscription: journal tail after this seq; omit = keyframe. */
|
|
10
12
|
since?: number;
|
|
@@ -62,6 +64,46 @@ export type ReplaySubscribeController = {
|
|
|
62
64
|
* cb goes through a ref — a new cb identity does not resubscribe.
|
|
63
65
|
*/
|
|
64
66
|
export declare function useReplaySubscribe<Z extends any[]>(remote: ReplayRemote<Z> | null | undefined, cb: (...event: Z) => void, options?: UseReplaySubscribeOptions): ReplaySubscribeController;
|
|
67
|
+
export type UseReplayRouteSubscribeOptions = {
|
|
68
|
+
/** Start position for the first route subscription. Omit = keyframe. */
|
|
69
|
+
since?: number;
|
|
70
|
+
/** Keep last delivered seq across disable/remount inside one mounted hook. Default true. */
|
|
71
|
+
keepSeq?: boolean;
|
|
72
|
+
/** false = close the active route subscription. Default true. */
|
|
73
|
+
enabled?: boolean;
|
|
74
|
+
/** Human-readable route label, e.g. "relay" / "direct". */
|
|
75
|
+
label?: string;
|
|
76
|
+
onSeq?: (seq: number) => void;
|
|
77
|
+
onError?: (e: unknown) => void;
|
|
78
|
+
/** Route lifecycle events from common2: switching, ready, error, closed. Goes through a ref. */
|
|
79
|
+
onRoute?: (ev: ReplayRouteEvent) => void;
|
|
80
|
+
/** Initial route lag policy. Route changes should use switchRoute(..., {policy}). */
|
|
81
|
+
policy?: 'queue' | 'frame';
|
|
82
|
+
/** Initial route frame condenser hint. Route changes should use switchRoute(..., {hint}). */
|
|
83
|
+
hint?: unknown;
|
|
84
|
+
};
|
|
85
|
+
export type ReplayRouteController<Z extends any[] = any[]> = {
|
|
86
|
+
/** Initial route or latest replacement catch-up has completed. False while replacement is catching up. */
|
|
87
|
+
readonly ready: boolean;
|
|
88
|
+
readonly error: unknown;
|
|
89
|
+
readonly route: ReplayRouteEvent | null;
|
|
90
|
+
readonly switching: boolean;
|
|
91
|
+
/** Last delivered seq (route hand-off reconnect point). Getter — reading it does not re-render. */
|
|
92
|
+
seq(): number;
|
|
93
|
+
/** Current route label. Getter — reading it does not re-render. */
|
|
94
|
+
label(): string | undefined;
|
|
95
|
+
/** Whether a route is currently active. Getter — reading it does not re-render. */
|
|
96
|
+
active(): boolean;
|
|
97
|
+
/** Switch to a replacement route: old route stays live until replacement catches up by seq. */
|
|
98
|
+
switchRoute(nextRemote: ReplayRemote<Z>, options?: ReplayRouteSwitchOptions): Promise<void>;
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Route-replaceable replay subscription. Use this when the app can promote/demote between
|
|
102
|
+
* transports (relay <-> direct) and must keep one logical replay fold with no gaps or dups.
|
|
103
|
+
* Changing the `remote` prop is still a fresh subscription boundary; no-gap hand-off is explicit
|
|
104
|
+
* through `controller.switchRoute(...)`.
|
|
105
|
+
*/
|
|
106
|
+
export declare function useReplayRouteSubscribe<Z extends any[]>(remote: ReplayRemote<Z> | null | undefined, cb: (...event: Z) => void, options?: UseReplayRouteSubscribeOptions): ReplayRouteController<Z>;
|
|
65
107
|
export type UseStoreReplaySyncOptions = UseReplaySubscribeOptions;
|
|
66
108
|
export type StoreReplaySyncController = ReplaySubscribeController;
|
|
67
109
|
/**
|
|
@@ -71,6 +113,18 @@ export type StoreReplaySyncController = ReplaySubscribeController;
|
|
|
71
113
|
* (useStoreNode/useStoreSelect).
|
|
72
114
|
*/
|
|
73
115
|
export declare function useStoreReplaySync<T extends object>(store: Observe.Store<T> | null | undefined, remote: ReplayRemote<[StorePatch]> | null | undefined, options?: UseStoreReplaySyncOptions): StoreReplaySyncController;
|
|
116
|
+
export type UseStoreReplayRouteSyncOptions = UseReplayRouteSubscribeOptions;
|
|
117
|
+
export type StoreReplayRouteSyncController = ReplayRouteController<[StorePatch]>;
|
|
118
|
+
/**
|
|
119
|
+
* Route-replaceable store replay sync. The supplied store remains the fold target while
|
|
120
|
+
* switchRoute() promotes/demotes the underlying replay route.
|
|
121
|
+
*/
|
|
122
|
+
export declare function useStoreReplayRouteSync<T extends object>(store: Observe.Store<T> | null | undefined, remote: ReplayRemote<[StorePatch]> | null | undefined, options?: UseStoreReplayRouteSyncOptions): StoreReplayRouteSyncController;
|
|
123
|
+
export type StoreReplayRouteMirrorController<T extends object> = StoreReplayRouteSyncController & {
|
|
124
|
+
readonly store: Observe.Store<T>;
|
|
125
|
+
};
|
|
126
|
+
/** Create a local mirror store and keep it synced through a route-replaceable replay remote. */
|
|
127
|
+
export declare function useStoreReplayRouteMirror<T extends object>(remote: ReplayRemote<[StorePatch]> | null | undefined, initial: T, options?: UseStoreReplayRouteSyncOptions): StoreReplayRouteMirrorController<T>;
|
|
74
128
|
export type StoreReplayMirrorController<T extends object> = StoreReplaySyncController & {
|
|
75
129
|
readonly store: Observe.Store<T>;
|
|
76
130
|
};
|