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.
@@ -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
- return item.icon ?? _jsx("span", { style: { fontSize: 10, fontWeight: 700, letterSpacing: 0.5, textTransform: 'uppercase' }, children: (item.short ?? item.title).slice(0, 3) });
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
- /** raw order/visible as stored - own store or the external source */
70
- const rawList = () => ext ? ext.getConfig() : { order: st.order, visible: st.visible };
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 rawOrder = Array.isArray(raw.order) ? raw.order : [];
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 flag is toolbar-local: an external source only owns items
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 flag
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
- st.visible = { ...st.visible, [SETTINGS_KEY]: next.visible[SETTINGS_KEY] != false };
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. popAlign: which bar edge the popover sticks to -
170
- * 'right' (default, for bars in a top-right corner) or 'left'. */
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) { // sticky sort: applies even when its column is hidden
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: { display: 'grid', gap: 8, ...p.style }, children: rows.map((row, i) => (_jsxs("div", { style: { border: '1px solid #d0d7de', borderRadius: 8, padding: '8px 10px', background: '#fff' }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 8, marginBottom: fieldKeys.length ? 6 : 0 }, children: [_jsx("b", { style: { fontSize: 14, flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: titleKey ? value(titleKey, row) : '' }), accentKey && _jsx("span", { style: { fontSize: 11, padding: '1px 8px', borderRadius: 10, background: '#ddf4ff', color: '#0969da', whiteSpace: 'nowrap' }, children: value(accentKey, row) })] }), fieldKeys.map(k => (_jsxs("div", { style: { display: 'flex', gap: 10, fontSize: 12, lineHeight: 1.7 }, children: [_jsx("span", { style: { color: '#57606a', minWidth: 72 }, children: byKey.get(k)?.title ?? k }), _jsx("span", { style: { flex: 1, textAlign: 'right', overflow: 'hidden', textOverflow: 'ellipsis' }, children: value(k, row) })] }, k)))] }, p.getId?.(row, i) ?? i))) });
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) { // tap = select the field
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) { // swipe up = tear the dot off (fixed and the last dot stay)
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: { userSelect: 'none', ...p.style }, children: [_jsxs("div", { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 6, fontSize: 12 }, children: [_jsxs("span", { style: { color: '#57606a' }, children: [visibleKeys.length, "/", max, " fields"] }), _jsx("span", { style: { flex: 1 } }), _jsxs("span", { style: { color: '#57606a' }, children: ["field: ", _jsx("b", { children: selected ? short(selected) : '—' })] }), _jsxs("button", { disabled: !selected, title: 'Sort by the selected field: asc -> desc -> off', onClick: () => selected && p.state.api.toggleSort(selected), style: { border: '1px solid #6e7781', borderRadius: 6, padding: '2px 8px', fontSize: 12, cursor: selected ? 'pointer' : 'default', opacity: selected ? 1 : 0.5, background: '#fff' }, children: ["\u21C5 sort: ", sortLabel] })] }), _jsxs("div", { ref: trackRef, style: { position: 'relative', height: 56, margin: '0 14px', touchAction: 'none' }, children: [_jsx("div", { style: { position: 'absolute', left: -8, right: -8, top: 27, height: 2, background: '#d0d7de', borderRadius: 1 } }), order.map((k, i) => {
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), style: { position: 'absolute', left: `${pct(i)}%`, top: 0, width: 44, height: 56, marginLeft: -22, cursor: !vis && visibleKeys.length < max ? 'pointer' : 'default' }, children: [isSorted && _jsx("div", { style: { position: 'absolute', left: 0, right: 0, top: 0, textAlign: 'center', fontSize: 11, color: '#0969da', fontWeight: 700 }, children: cfg.sort.dir == 'asc' ? '↑' : '↓' }), _jsx("div", { style: { position: 'absolute', left: '50%', top: 24, width: 8, height: 8, marginLeft: -4, borderRadius: 4, background: vis ? 'transparent' : '#afb8c1' } }), _jsx("div", { style: { position: 'absolute', left: 0, right: 0, top: 40, textAlign: 'center', fontSize: 10, color: vis ? '#24292f' : '#8c959f', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }, children: short(k) })] }, 'm' + 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 column keys the attached grid
22
- * actually HAS right now. null = unknown / no grid = treat all as present.
23
- * A column removed from the live grid (dynamic defs, "drop empty columns"
24
- * standards) keeps its config entry and its menu button - the button just
25
- * goes inert. The grid adapter maintains this; grid-less consumers may
26
- * call setPresent themselves. */
27
- const rt = { present: null };
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 = keys ? Object.fromEntries(keys.map(k => [k, true])) : null;
30
- if (JSON.stringify(next) == JSON.stringify(rt.present))
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
- const getPresent = () => rt.present;
36
- const isPresent = (key) => !rt.present || rt.present[key] == true;
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 rt.present;
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
  };