wenay-react2 1.0.41 → 1.0.43

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 (63) hide show
  1. package/doc/EXAMPLE_USAGE.md +58 -2
  2. package/doc/PROJECT_FUNCTIONALITY.md +5 -1
  3. package/doc/changes/v1.0.41.md +23 -0
  4. package/doc/changes/v1.0.42.md +26 -0
  5. package/doc/changes/v1.0.43.md +10 -0
  6. package/doc/progress/architecture-fix-queue.md +74 -0
  7. package/doc/progress/style-system-normalization.md +5 -5
  8. package/doc/target/my.md +39 -67
  9. package/doc/wenay-react2-rare.md +118 -13
  10. package/doc/wenay-react2.md +78 -12
  11. package/lib/common/api.d.ts +1 -0
  12. package/lib/common/api.js +8 -4
  13. package/lib/common/src/components/Dnd/DragArea.d.ts +5 -0
  14. package/lib/common/src/components/Dnd/DragArea.js +5 -1
  15. package/lib/common/src/components/Dnd/FloatingWindow.d.ts +9 -5
  16. package/lib/common/src/components/Dnd/FloatingWindow.js +39 -97
  17. package/lib/common/src/components/Dnd/Resizable.d.ts +3 -7
  18. package/lib/common/src/components/Dnd/Resizable.js +4 -3
  19. package/lib/common/src/components/Menu/RightMenu.js +8 -4
  20. package/lib/common/src/components/Menu/RightMenuStore.d.ts +2 -2
  21. package/lib/common/src/components/Menu/RightMenuStore.js +5 -3
  22. package/lib/common/src/components/Modal/LeftModal.js +4 -2
  23. package/lib/common/src/components/Modal/ModalContextProvider.js +5 -16
  24. package/lib/common/src/components/MyResizeObserver.d.ts +24 -0
  25. package/lib/common/src/components/MyResizeObserver.js +49 -0
  26. package/lib/common/src/components/Overlay.d.ts +24 -0
  27. package/lib/common/src/components/Overlay.js +28 -0
  28. package/lib/common/src/components/Settings/SettingsDialog.js +21 -22
  29. package/lib/common/src/components/Toolbar/Toolbar.d.ts +1 -0
  30. package/lib/common/src/components/Toolbar/Toolbar.js +16 -23
  31. package/lib/common/src/grid/columnState/ColumnDots.js +34 -17
  32. package/lib/common/src/grid/columnState/ColumnsMenu.js +4 -13
  33. package/lib/common/src/grid/columnState/columnGrid.d.ts +103 -0
  34. package/lib/common/src/grid/columnState/columnGrid.js +231 -0
  35. package/lib/common/src/grid/columnState/columnState.js +10 -9
  36. package/lib/common/src/grid/columnState/index.js +3 -0
  37. package/lib/common/src/hooks/useDraggable.d.ts +8 -0
  38. package/lib/common/src/hooks/useDraggable.js +13 -4
  39. package/lib/common/src/hooks/useOutside.d.ts +4 -1
  40. package/lib/common/src/hooks/useOutside.js +2 -1
  41. package/lib/common/src/logs/logs.d.ts +96 -5
  42. package/lib/common/src/logs/logs.js +147 -108
  43. package/lib/common/src/logs/logsController.d.ts +3 -0
  44. package/lib/common/src/styles/tokens.d.ts +3 -0
  45. package/lib/common/src/styles/tokens.js +3 -0
  46. package/lib/common/src/utils/cache.d.ts +12 -0
  47. package/lib/common/src/utils/cache.js +0 -0
  48. package/lib/common/src/utils/fixedOrder.d.ts +15 -0
  49. package/lib/common/src/utils/fixedOrder.js +30 -0
  50. package/lib/common/src/utils/index.d.ts +2 -0
  51. package/lib/common/src/utils/index.js +2 -0
  52. package/lib/common/src/utils/memoryStore.d.ts +3 -6
  53. package/lib/common/src/utils/memoryStore.js +1 -3
  54. package/lib/common/src/utils/persistedMaps.d.ts +16 -0
  55. package/lib/common/src/utils/persistedMaps.js +5 -0
  56. package/lib/common/src/utils/searchHistory.js +7 -6
  57. package/lib/common/src/utils/structEqual.d.ts +12 -0
  58. package/lib/common/src/utils/structEqual.js +40 -0
  59. package/lib/common/updateBy.d.ts +3 -0
  60. package/lib/common/updateBy.js +57 -22
  61. package/lib/style/style.css +692 -621
  62. package/lib/style/tokens.css +11 -8
  63. package/package.json +2 -2
@@ -3,8 +3,11 @@ import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
3
3
  // with dots on the columns that are currently shown. Placing a dot IS showing
4
4
  // the column; the card view (CardList) rebuilds from the same config live.
5
5
  // Gestures (pointer events, mouse + touch):
6
- // drag a dot along the track -> the dot slides to another (empty) mark: that
7
- // column replaces the old one
6
+ // drag a dot along the track -> LIVE: every empty mark the dot crosses swaps
7
+ // the shown column immediately (the grid/cards
8
+ // follow the finger - that is how you SEARCH for
9
+ // a column on a phone); a small label above the
10
+ // finger names the currently shown column
8
11
  // swipe a dot UP (flick off) -> the dot tears off the track: column hidden
9
12
  // tap an empty mark -> a dot appears there: column shown
10
13
  // tap a dot (no move) -> select the field (highlight); the sort
@@ -38,7 +41,7 @@ export function ColumnDots(p) {
38
41
  function downDot(key, e) {
39
42
  e.preventDefault();
40
43
  e.currentTarget.setPointerCapture(e.pointerId);
41
- gestureRef.current = { key, startX: e.clientX, startY: e.clientY, moved: false };
44
+ gestureRef.current = { key, startX: e.clientX, startY: e.clientY, moved: false, shown: key, fixed: !!byKey.get(key)?.fixed };
42
45
  }
43
46
  function moveDot(e) {
44
47
  const g = gestureRef.current;
@@ -56,7 +59,17 @@ export function ColumnDots(p) {
56
59
  const from = order.indexOf(g.key);
57
60
  const off = -dy > REMOVE_DY && -dy > Math.abs(dx);
58
61
  const to = Math.max(0, Math.min(n - 1, Math.round(from + dx / step)));
59
- setDrag({ key: g.key, dx, dy, to, off });
62
+ // LIVE swap: crossing an EMPTY mark shows that column immediately (the grid
63
+ // follows the finger - column search by dragging). Occupied marks are passed
64
+ // over; a fixed dot never swaps (same rule the old on-drop commit had).
65
+ if (!off && !g.fixed) {
66
+ const target = order[to];
67
+ if (target && target != g.shown && cfg.visible[target] == false) {
68
+ p.state.api.setConfig({ ...cfg, visible: { ...cfg.visible, [g.shown]: false, [target]: true } });
69
+ g.shown = target;
70
+ }
71
+ }
72
+ setDrag({ key: g.key, dx, dy, to, off, shown: g.shown });
60
73
  }
61
74
  function upDot() {
62
75
  const g = gestureRef.current;
@@ -71,21 +84,19 @@ export function ColumnDots(p) {
71
84
  }
72
85
  if (!st)
73
86
  return;
74
- const meta = byKey.get(g.key);
87
+ // swaps already happened live during the move; the drop only settles
88
+ // selection and handles the tear-off of the CURRENTLY shown column
75
89
  if (st.off) {
90
+ const meta = byKey.get(g.shown);
76
91
  if (!meta?.fixed && visibleKeys.length > 1) {
77
- p.state.api.show(g.key, false);
78
- if (selected == g.key)
92
+ p.state.api.show(g.shown, false);
93
+ if (selected == g.key || selected == g.shown)
79
94
  setSelected(null);
80
95
  }
81
96
  return;
82
97
  }
83
- const target = order[st.to];
84
- if (target && target != g.key && cfg.visible[target] == false && !meta?.fixed) {
85
- p.state.api.setConfig({ ...cfg, visible: { ...cfg.visible, [g.key]: false, [target]: true } });
86
- if (selected == g.key)
87
- setSelected(target);
88
- }
98
+ if (g.shown != g.key && selected == g.key)
99
+ setSelected(g.shown);
89
100
  }
90
101
  function tapMark(key) {
91
102
  if (cfg.visible[key] != false)
@@ -101,11 +112,17 @@ export function ColumnDots(p) {
101
112
  const isSorted = cfg.sort?.key == k;
102
113
  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);
103
114
  }), order.map((k, i) => {
104
- if (cfg.visible[k] == false)
115
+ const d = drag?.key == k ? drag : null;
116
+ // the dragged dot stays mounted through live swaps (its column may be
117
+ // hidden mid-gesture; unmounting would kill the pointer capture)...
118
+ if (cfg.visible[k] == false && !d)
119
+ return null;
120
+ // ...and the live-shown column's resting dot is suppressed: its
121
+ // representation IS the dot under the finger
122
+ if (drag && !d && k == drag.shown && drag.shown != drag.key)
105
123
  return null;
106
124
  const meta = byKey.get(k);
107
- const d = drag?.key == k ? drag : null;
108
- const removing = !!d?.off && !meta?.fixed && visibleKeys.length > 1;
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);
125
+ const removing = !!d?.off && !byKey.get(d.shown)?.fixed && visibleKeys.length > 1;
126
+ return _jsxs("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: [d && !d.off && _jsx("div", { className: 'wenayColDotsDragLabel', children: short(d.shown) }), _jsx("div", { className: cx(['wenayColDotsDot', selected == k && 'wenayColDotsDot_selected', meta?.fixed && 'wenayColDotsDot_fixed']) })] }, 'd' + k);
110
127
  })] })] });
111
128
  }
@@ -22,6 +22,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
22
22
  // comes back to life when the column returns. No ag-grid, no storage here.
23
23
  import { useRef } from 'react';
24
24
  import { useReorder } from '../../hooks/useReorder';
25
+ import { movedOrderWithFixed } from '../../utils/fixedOrder';
25
26
  function colsMenuClass(parts) {
26
27
  return parts.filter(Boolean).join(' ');
27
28
  }
@@ -96,20 +97,10 @@ export function ColumnsMenu(p) {
96
97
  p.state.api.show(key, cfg.visible[key] == false);
97
98
  }
98
99
  /** Simulated drop with the same fixed pinning as the config's normalize():
99
- * the drag preview never disagrees with where the button actually lands. */
100
+ * the drag preview never disagrees with where the button actually lands -
101
+ * both sides now share utils/fixedOrder. */
100
102
  function movedOrder(order, key, to) {
101
- const next = order.slice();
102
- const from = next.indexOf(key);
103
- if (from == -1)
104
- return next;
105
- next.splice(from, 1);
106
- next.splice(Math.max(0, Math.min(next.length, to)), 0, key);
107
- const res = next.filter(k => !byKey.get(k)?.fixed);
108
- p.state.columns.forEach(function pinFixed(c, i) {
109
- if (c.fixed)
110
- res.splice(Math.min(i, res.length), 0, c.key);
111
- });
112
- return res;
103
+ return movedOrderWithFixed(order, key, to, p.state.columns);
113
104
  }
114
105
  return _jsx(MenuStrip, { items: items, tail: p.tail, onItem: onItem, onMove: p.reorder == false ? undefined : order => p.state.api.move(order), move: movedOrder, holdMs: p.holdMs, compact: p.compact, className: p.className, style: p.style });
115
106
  }
@@ -0,0 +1,103 @@
1
+ import React from 'react';
2
+ import type { ColDef, ColGroupDef, GridPreDestroyedEvent, GridReadyEvent } from 'ag-grid-community';
3
+ import type { AgGridReactProps } from 'ag-grid-react';
4
+ import { type AgGridTableProps } from '../agGrid4';
5
+ import { createToolbar, type ToolbarConfig, type ToolbarItem, type ToolbarSourceMode } from '../../components/Toolbar';
6
+ import { CardList } from './CardList';
7
+ import { ColumnDots } from './ColumnDots';
8
+ import { ColumnsMenu } from './ColumnsMenu';
9
+ import { type ColumnMeta, type ColumnsConfig, type ColumnStateController } from './columnState';
10
+ export type ColumnGridColumnDef<T extends object> = ColDef<T> | ColGroupDef<T>;
11
+ export type ColumnGridColumn = Partial<Omit<ColumnMeta, 'key'>> & {
12
+ key: string;
13
+ };
14
+ export type ColumnGridToolbarOptions = {
15
+ key?: string;
16
+ items?: ToolbarItem[];
17
+ def?: Partial<ToolbarConfig>;
18
+ settingsItem?: {
19
+ title?: string;
20
+ icon?: React.ReactNode;
21
+ };
22
+ resetItem?: false | {
23
+ title?: string;
24
+ icon?: React.ReactNode;
25
+ defaultVisible?: boolean;
26
+ };
27
+ sourceMode?: ToolbarSourceMode;
28
+ };
29
+ export type ColumnGridOptions<T extends object> = {
30
+ /** Shared persistence key for the column config. */
31
+ key: string;
32
+ /** ag-grid defs. Leaf defs become ColumnMeta automatically by colId/field/headerName. */
33
+ columnDefs?: readonly ColumnGridColumnDef<T>[];
34
+ /** Optional metadata overrides, keyed by colId/field. Missing fields are inferred from columnDefs. */
35
+ columns?: readonly ColumnGridColumn[];
36
+ def?: Partial<ColumnsConfig>;
37
+ /** Optional default data for View; per-render props still override it. */
38
+ data?: readonly T[];
39
+ /** Optional default row id getter for View/CardList and simple table wiring. */
40
+ getId?: (row: T, index: number) => string;
41
+ /** Optional sizeColumnsToFit when the visible column count changes. */
42
+ autoSizeOnColumnCountChange?: boolean;
43
+ saveMs?: number;
44
+ /** Built by default over the same columnState list source; pass false to skip it. */
45
+ toolbar?: false | ColumnGridToolbarOptions;
46
+ };
47
+ export type ColumnGridTableProps<T extends object> = Omit<AgGridTableProps<T>, 'columnDefs' | 'autoSizeColumns' | 'onGridReady' | 'onGridPreDestroyed'> & {
48
+ columnDefs?: AgGridReactProps<T>['columnDefs'];
49
+ /** Defaults to false because columnState restores/persists widths. */
50
+ autoSizeColumns?: boolean;
51
+ onGridReady?: (event: GridReadyEvent<T>) => void;
52
+ onGridPreDestroyed?: (event: GridPreDestroyedEvent<T>) => void;
53
+ /** Defaults from createColumnGrid(opts); fits once when visible column count changes. */
54
+ autoSizeOnColumnCountChange?: boolean;
55
+ };
56
+ export type ColumnGridMenuProps = Omit<React.ComponentProps<typeof ColumnsMenu>, 'state'>;
57
+ export type ColumnGridDotsProps = Omit<React.ComponentProps<typeof ColumnDots>, 'state'>;
58
+ export type ColumnGridCardsProps<T extends object> = Omit<React.ComponentProps<typeof CardList<T>>, 'state'>;
59
+ export type ColumnGridViewMode = 'table' | 'cards';
60
+ export type ColumnGridControls = false | 'auto' | 'toolbar' | 'menu' | 'dots';
61
+ export type ColumnGridToolbar = ReturnType<typeof createToolbar>;
62
+ export type ColumnGridToolbarBarProps = React.ComponentProps<ColumnGridToolbar['Bar']>;
63
+ export type ColumnGridToolbarSettingsProps = React.ComponentProps<ColumnGridToolbar['Settings']>;
64
+ export type ColumnGridViewProps<T extends object> = {
65
+ mode?: ColumnGridViewMode;
66
+ data?: readonly T[];
67
+ getId?: (row: T, index: number) => string;
68
+ controls?: ColumnGridControls;
69
+ table?: ColumnGridTableProps<T>;
70
+ cards?: Omit<ColumnGridCardsProps<T>, 'data'>;
71
+ menu?: ColumnGridMenuProps;
72
+ dots?: ColumnGridDotsProps;
73
+ toolbar?: ColumnGridToolbarBarProps;
74
+ className?: string;
75
+ style?: React.CSSProperties;
76
+ bodyClassName?: string;
77
+ bodyStyle?: React.CSSProperties;
78
+ tableHeight?: React.CSSProperties['height'];
79
+ };
80
+ export type ColumnGridController<T extends object> = {
81
+ state: ColumnStateController;
82
+ toolbar: ColumnGridToolbar | null;
83
+ columns: readonly ColumnMeta[];
84
+ columnDefs: AgGridReactProps<T>['columnDefs'];
85
+ api: ColumnStateController['api'] & {
86
+ tableProps: (props?: ColumnGridTableProps<T>) => AgGridTableProps<T>;
87
+ };
88
+ grid: ColumnStateController['grid'];
89
+ tableProps: (props?: ColumnGridTableProps<T>) => AgGridTableProps<T>;
90
+ Table: (props: ColumnGridTableProps<T>) => React.JSX.Element;
91
+ Menu: (props?: ColumnGridMenuProps) => React.JSX.Element;
92
+ Dots: (props?: ColumnGridDotsProps) => React.JSX.Element;
93
+ Cards: (props: ColumnGridCardsProps<T>) => React.JSX.Element;
94
+ Toolbar: (props?: ColumnGridToolbarBarProps) => React.JSX.Element | null;
95
+ Settings: (props?: ColumnGridToolbarSettingsProps) => React.JSX.Element | null;
96
+ View: (props: ColumnGridViewProps<T>) => React.JSX.Element;
97
+ /** Release factory-lifetime subscriptions; persisted config is untouched. */
98
+ dispose: () => void;
99
+ };
100
+ /** High-level column kit for the common case: one keyed config drives a grid,
101
+ * compact menu, toolbar settings, mobile dots, and card/table views. */
102
+ export declare function createColumnGrid<T extends object>(opts: ColumnGridOptions<T>): ColumnGridController<T>;
103
+ export declare function useColumnGrid<T extends object>(opts: ColumnGridOptions<T>): ColumnGridController<T>;
@@ -0,0 +1,231 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { AgGridTable } from '../agGrid4';
4
+ import { createToolbar } from '../../components/Toolbar';
5
+ import { CardList } from './CardList';
6
+ import { ColumnDots } from './ColumnDots';
7
+ import { ColumnsMenu } from './ColumnsMenu';
8
+ import { createColumnState } from './columnState';
9
+ function isGroupDef(def) {
10
+ return Array.isArray(def.children);
11
+ }
12
+ function textOrUndefined(v) {
13
+ if (v == null || v === '')
14
+ return undefined;
15
+ return String(v);
16
+ }
17
+ function titleFromKey(key) {
18
+ return key
19
+ .replace(/[_-]+/g, ' ')
20
+ .replace(/\b\w/g, s => s.toUpperCase());
21
+ }
22
+ function collectColumnMetas(defs, parentGroup) {
23
+ const res = [];
24
+ for (const def of defs ?? []) {
25
+ if (isGroupDef(def)) {
26
+ const group = textOrUndefined(def.groupId) ?? textOrUndefined(def.headerName) ?? parentGroup;
27
+ res.push(...collectColumnMetas(def.children, group));
28
+ continue;
29
+ }
30
+ const key = textOrUndefined(def.colId) ?? textOrUndefined(def.field);
31
+ if (!key)
32
+ continue;
33
+ res.push({
34
+ key,
35
+ title: textOrUndefined(def.headerName) ?? titleFromKey(key),
36
+ group: parentGroup,
37
+ });
38
+ }
39
+ return res;
40
+ }
41
+ function resolveColumns(opts) {
42
+ const auto = collectColumnMetas(opts.columnDefs);
43
+ const overrides = new Map((opts.columns ?? []).map(c => [c.key, c]));
44
+ const used = new Set();
45
+ const res = [];
46
+ for (const base of auto) {
47
+ if (used.has(base.key))
48
+ continue;
49
+ const patch = overrides.get(base.key);
50
+ used.add(base.key);
51
+ res.push({ ...base, ...patch, key: base.key, title: patch?.title ?? base.title });
52
+ }
53
+ for (const patch of opts.columns ?? []) {
54
+ if (used.has(patch.key))
55
+ continue;
56
+ used.add(patch.key);
57
+ res.push({ ...patch, key: patch.key, title: patch.title ?? titleFromKey(patch.key) });
58
+ }
59
+ return res;
60
+ }
61
+ function cx(parts) {
62
+ return parts.filter(Boolean).join(' ');
63
+ }
64
+ function mergeClass(a, b) {
65
+ return a ? b + ' ' + a : b;
66
+ }
67
+ /** High-level column kit for the common case: one keyed config drives a grid,
68
+ * compact menu, toolbar settings, mobile dots, and card/table views. */
69
+ export function createColumnGrid(opts) {
70
+ const columnDefs = (opts.columnDefs ?? []);
71
+ const state = createColumnState({
72
+ key: opts.key,
73
+ columns: resolveColumns(opts),
74
+ def: opts.def,
75
+ saveMs: opts.saveMs,
76
+ });
77
+ let gridApiForFit = null;
78
+ let fitOnCountChange = opts.autoSizeOnColumnCountChange === true;
79
+ let visibleCount = state.api.visibleKeys().length;
80
+ let fitRaf = 0;
81
+ function scheduleFit() {
82
+ if (!gridApiForFit)
83
+ return;
84
+ cancelAnimationFrame(fitRaf);
85
+ fitRaf = requestAnimationFrame(() => gridApiForFit?.sizeColumnsToFit());
86
+ }
87
+ const offConfigChange = state.api.onChange.on(() => {
88
+ const next = state.api.visibleKeys().length;
89
+ if (next == visibleCount)
90
+ return;
91
+ visibleCount = next;
92
+ if (fitOnCountChange)
93
+ scheduleFit();
94
+ });
95
+ const toolbar = opts.toolbar === false ? null : createToolbar({
96
+ key: opts.toolbar?.key ?? `${opts.key}.toolbar`,
97
+ items: opts.toolbar?.items ?? state.columns.map(c => ({
98
+ key: c.key,
99
+ title: c.title,
100
+ short: c.short,
101
+ icon: c.icon,
102
+ fixed: c.fixed,
103
+ defaultVisible: c.defaultVisible,
104
+ onClick: () => {
105
+ const present = state.api.getPresent();
106
+ if (c.fixed || (present && !present[c.key]))
107
+ return;
108
+ const cfg = state.api.getConfig();
109
+ state.api.show(c.key, cfg.visible[c.key] == false);
110
+ },
111
+ })),
112
+ def: opts.toolbar?.def,
113
+ settingsItem: opts.toolbar?.settingsItem,
114
+ resetItem: opts.toolbar?.resetItem,
115
+ source: state.api.listSource,
116
+ sourceMode: opts.toolbar?.sourceMode,
117
+ });
118
+ function tableProps(props = {}) {
119
+ const { onGridReady, onGridPreDestroyed, autoSizeColumns = false, autoSizeOnColumnCountChange = opts.autoSizeOnColumnCountChange === true, columnDefs: localDefs = columnDefs, ...rest } = props;
120
+ return {
121
+ ...rest,
122
+ columnDefs: localDefs,
123
+ autoSizeColumns,
124
+ onGridReady(event) {
125
+ gridApiForFit = event.api;
126
+ fitOnCountChange = autoSizeOnColumnCountChange;
127
+ state.grid.attach(event.api);
128
+ // re-seed: the column count may have changed while no grid was attached,
129
+ // and the first post-remount onChange must not mis-skip the auto-fit
130
+ visibleCount = state.api.visibleKeys().length;
131
+ if (fitOnCountChange)
132
+ scheduleFit();
133
+ onGridReady?.(event);
134
+ },
135
+ onGridPreDestroyed(event) {
136
+ if (gridApiForFit === event.api)
137
+ gridApiForFit = null;
138
+ cancelAnimationFrame(fitRaf);
139
+ fitRaf = 0;
140
+ state.grid.detach();
141
+ onGridPreDestroyed?.(event);
142
+ },
143
+ };
144
+ }
145
+ function Table(props) {
146
+ return _jsx(AgGridTable, { ...tableProps(props) });
147
+ }
148
+ function Menu(props = {}) {
149
+ return _jsx(ColumnsMenu, { ...props, state: state });
150
+ }
151
+ function Dots(props = {}) {
152
+ return _jsx(ColumnDots, { ...props, max: props.max ?? state.columns.length, className: mergeClass(props.className, 'wenayColumnGridDots'), state: state });
153
+ }
154
+ function Cards(props) {
155
+ return _jsx(CardList, { ...props, state: state });
156
+ }
157
+ function Toolbar(props = {}) {
158
+ if (!toolbar)
159
+ return null;
160
+ const Bar = toolbar.Bar;
161
+ return _jsx(Bar, { ...props });
162
+ }
163
+ function Settings(props = {}) {
164
+ if (!toolbar)
165
+ return null;
166
+ const ToolbarSettings = toolbar.Settings;
167
+ return _jsx(ToolbarSettings, { ...props });
168
+ }
169
+ function controlKind(mode, controls) {
170
+ if (controls === false)
171
+ return null;
172
+ if (!controls || controls == 'auto')
173
+ return 'dots';
174
+ return controls;
175
+ }
176
+ function renderControls(kind, p) {
177
+ if (!kind)
178
+ return null;
179
+ if (kind == 'dots')
180
+ return _jsx(Dots, { ...p.dots });
181
+ if (kind == 'toolbar')
182
+ return _jsx(Toolbar, { settings: true, popAlign: "left", ...p.toolbar });
183
+ return _jsx(Menu, { compact: true, ...p.menu });
184
+ }
185
+ function View(p) {
186
+ const mode = p.mode ?? 'table';
187
+ const kind = controlKind(mode, p.controls);
188
+ const controls = renderControls(kind, p);
189
+ const data = p.data ?? opts.data ?? [];
190
+ const getId = p.getId ?? opts.getId;
191
+ const bodyStyle = mode == 'table'
192
+ ? { height: p.tableHeight ?? 240, minHeight: 0, ...p.bodyStyle }
193
+ : { ...p.bodyStyle };
194
+ const tableGetRowId = p.table?.getRowId ?? (getId
195
+ ? ((params) => getId(params.data, 0))
196
+ : undefined);
197
+ return _jsxs("div", { className: cx(['wenayColumnGrid', `wenayColumnGrid_${mode}`, kind && `wenayColumnGrid_controls_${kind}`, p.className]), style: p.style, children: [_jsx("div", { className: cx(['wenayColumnGridBody', p.bodyClassName]), style: bodyStyle, children: mode == 'cards'
198
+ ? _jsx(Cards, { ...p.cards, data: data, getId: p.cards?.getId ?? getId })
199
+ : _jsx(Table, { ...p.table, rowData: (p.table?.rowData ?? data), getRowId: tableGetRowId }) }), controls && _jsx("div", { className: cx(['wenayColumnGridControls', kind && `wenayColumnGridControls_${kind}`]), children: controls })] });
200
+ }
201
+ /** Release factory-lifetime subscriptions (config onChange, toolbar source, pending fit).
202
+ * Persisted config stays; this is listener lifetime only (HMR, repeated factories). */
203
+ function dispose() {
204
+ offConfigChange();
205
+ toolbar?.api.dispose();
206
+ cancelAnimationFrame(fitRaf);
207
+ fitRaf = 0;
208
+ gridApiForFit = null;
209
+ }
210
+ return {
211
+ state,
212
+ toolbar,
213
+ columns: state.columns,
214
+ columnDefs,
215
+ api: { ...state.api, tableProps },
216
+ grid: state.grid,
217
+ tableProps,
218
+ Table,
219
+ Menu,
220
+ Dots,
221
+ Cards,
222
+ Toolbar,
223
+ Settings,
224
+ View,
225
+ dispose,
226
+ };
227
+ }
228
+ export function useColumnGrid(opts) {
229
+ const [grid] = useState(() => createColumnGrid(opts));
230
+ return grid;
231
+ }
@@ -1,6 +1,8 @@
1
1
  import { listen as createListen } from 'wenay-common2';
2
2
  import { createUpdateApi } from '../../../updateBy';
3
3
  import { memoryGetOrCreate, memoryMarkDirty } from '../../utils/memoryStore';
4
+ import { pinFixedOrder } from '../../utils/fixedOrder';
5
+ import { structEqual } from '../../utils/structEqual';
4
6
  const SCHEMA_V = 1;
5
7
  /** Grid events that mean "the user changed the column layout / sort / filter". */
6
8
  const GRID_EVENTS = ['columnMoved', 'columnResized', 'columnVisible', 'sortChanged', 'filterChanged'];
@@ -74,14 +76,11 @@ export function createColumnState(opts) {
74
76
  const known = new Set(opts.columns.map(c => c.key));
75
77
  const byKey = new Map(opts.columns.map(c => [c.key, c]));
76
78
  const rawOrder = Array.isArray(st.order) ? st.order : [];
77
- const order = rawOrder.filter(k => known.has(k) && !byKey.get(k)?.fixed);
79
+ const prelim = rawOrder.filter(k => known.has(k) && !byKey.get(k)?.fixed);
78
80
  for (const c of opts.columns)
79
- if (!c.fixed && order.indexOf(c.key) == -1)
80
- order.push(c.key);
81
- opts.columns.forEach(function pinFixed(c, i) {
82
- if (c.fixed)
83
- order.splice(Math.min(i, order.length), 0, c.key);
84
- });
81
+ if (!c.fixed && prelim.indexOf(c.key) == -1)
82
+ prelim.push(c.key);
83
+ const order = pinFixedOrder(prelim, opts.columns);
85
84
  const rawVisible = st.visible && typeof st.visible == 'object' ? st.visible : {};
86
85
  const visible = {};
87
86
  for (const c of opts.columns)
@@ -245,13 +244,15 @@ export function createColumnState(opts) {
245
244
  order.push(k);
246
245
  const filter = (gridApi.getFilterModel() ?? {});
247
246
  const next = { ...cfg, order, visible, width, sort, filter };
248
- if (JSON.stringify(next) == JSON.stringify(cfg))
247
+ // structural compare: the grid returns filter models with its own key order,
248
+ // which must not count as a change (stringify used to false-positive here)
249
+ if (structEqual(next, cfg))
249
250
  return;
250
251
  commit(next, true);
251
252
  // normalize() may have corrected the grid's move (a fixed column dragged
252
253
  // away from its pinned index): push the corrected order back so the grid
253
254
  // and the config never disagree
254
- if (JSON.stringify(normalize().order) != JSON.stringify(next.order))
255
+ if (!structEqual(normalize().order, next.order))
255
256
  applyToGrid();
256
257
  }
257
258
  function onGridEvent(e) {
@@ -1,3 +1,6 @@
1
+ // Grid-less barrel: columnState/ColumnsMenu/ColumnDots/CardList must stay importable without
2
+ // pulling ag-grid runtime (mobile/card consumers). createColumnGrid lives in './columnGrid'
3
+ // (it imports AgGridTable + createToolbar) and is exported from the root api, not from here.
1
4
  export * from './columnState';
2
5
  export * from './ColumnsMenu';
3
6
  export * from './ColumnDots';
@@ -8,6 +8,14 @@ export type UseDraggableOptions = {
8
8
  holdMs?: number;
9
9
  onDragEnd?: DragEndCallback;
10
10
  onDragStart?: DragStartCallback;
11
+ /** Imperative per-move-tick callback: fires with the current delta on every
12
+ * mousemove/touchmove of an active drag - NOT on setPosition/resetPosition/cancelDrag
13
+ * and not on release. Goes through a ref, an inline closure does not resubscribe. */
14
+ onMove?: (position: Position) => void;
15
+ /** When false, position lives only in positionRef/onMove and the hook does NOT
16
+ * re-render per move tick (imperative consumers, e.g. the DragBox adapter).
17
+ * isDragging start/end re-renders remain. Default true. */
18
+ trackState?: boolean;
11
19
  };
12
20
  export interface UseDraggableReturn {
13
21
  readonly position: Position;
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useMemo, useRef, useState } from "react";
2
2
  export function useDraggableApi(options = {}) {
3
- const { initialPosition = { x: 0, y: 0 }, holdMs = 500, onDragEnd, onDragStart } = options;
3
+ const { initialPosition = { x: 0, y: 0 }, holdMs = 500, onDragEnd, onDragStart, onMove, trackState = true } = options;
4
4
  const [position, setPositionState] = useState(initialPosition);
5
5
  const positionRef = useRef(position);
6
6
  const holdMsRef = useRef(holdMs);
@@ -11,17 +11,22 @@ export function useDraggableApi(options = {}) {
11
11
  const draggingRef = useRef(false);
12
12
  const onDragEndRef = useRef(onDragEnd);
13
13
  const onDragStartRef = useRef(onDragStart);
14
+ const onMoveRef = useRef(onMove);
15
+ const trackStateRef = useRef(trackState);
14
16
  const holdTimerMouse = useRef(null);
15
17
  const holdTimerTouch = useRef(null);
16
18
  const setPos = (p) => {
17
19
  positionRef.current = p;
18
- setPositionState(p);
20
+ if (trackStateRef.current)
21
+ setPositionState(p);
19
22
  };
20
23
  holdMsRef.current = holdMs;
24
+ trackStateRef.current = trackState;
21
25
  draggingRef.current = draggingMouse || draggingTouch;
22
26
  useEffect(() => {
23
27
  onDragEndRef.current = onDragEnd;
24
28
  onDragStartRef.current = onDragStart;
29
+ onMoveRef.current = onMove;
25
30
  });
26
31
  const cancelMouseHold = useMemo(() => function cancelMouseHold() {
27
32
  if (holdTimerMouse.current != null) {
@@ -77,7 +82,9 @@ export function useDraggableApi(options = {}) {
77
82
  if (!draggingMouse)
78
83
  return;
79
84
  const handleMouseMove = (e) => {
80
- setPos({ x: e.clientX - offsetMouse.current.x, y: e.clientY - offsetMouse.current.y });
85
+ const p = { x: e.clientX - offsetMouse.current.x, y: e.clientY - offsetMouse.current.y };
86
+ setPos(p);
87
+ onMoveRef.current?.(p);
81
88
  };
82
89
  const handleMouseUp = () => {
83
90
  document.removeEventListener("mousemove", handleMouseMove);
@@ -104,7 +111,9 @@ export function useDraggableApi(options = {}) {
104
111
  const theTouch = Array.from(e.changedTouches).find((t) => t.identifier === offsetTouch.current?.id);
105
112
  if (!theTouch)
106
113
  return;
107
- setPos({ x: theTouch.clientX - offsetTouch.current.x, y: theTouch.clientY - offsetTouch.current.y });
114
+ const p = { x: theTouch.clientX - offsetTouch.current.x, y: theTouch.clientY - offsetTouch.current.y };
115
+ setPos(p);
116
+ onMoveRef.current?.(p);
108
117
  };
109
118
  const handleTouchEnd = (e) => {
110
119
  if (!offsetTouch.current)
@@ -37,6 +37,9 @@ type ButtonBaseProps = {
37
37
  };
38
38
  type ButtonProps = ButtonBaseProps & {
39
39
  statusDef?: boolean;
40
+ /** Persist the open/closed status under this key (module-lifetime). Same naming as FloatingWindow/Resizable/RightMenu. */
41
+ keyForSave?: string;
42
+ /** @deprecated alias of {@link keyForSave}; kept for compatibility, `keyForSave` wins when both are set. */
40
43
  keySave?: string;
41
44
  outClick?: boolean | (() => void);
42
45
  zIndex?: number;
@@ -46,7 +49,7 @@ export declare const OutsideClickArea: React.ForwardRefExoticComponent<React.HTM
46
49
  status?: boolean;
47
50
  zIndex?: number;
48
51
  } & React.RefAttributes<HTMLDivElement>>;
49
- export declare function Button({ keySave, statusDef, outClick, ...data }: ButtonProps): import("react/jsx-runtime").JSX.Element;
52
+ export declare function Button({ keyForSave, keySave, statusDef, outClick, ...data }: ButtonProps): import("react/jsx-runtime").JSX.Element;
50
53
  export declare function HoverButton(props: ButtonBaseProps): import("react/jsx-runtime").JSX.Element;
51
54
  export declare const OutsideButton: typeof Button;
52
55
  export declare function AbsoluteButton(props: Parameters<typeof Button>[0]): import("react/jsx-runtime").JSX.Element;
@@ -73,7 +73,8 @@ function ButtonBase({ children, button, style = {}, className = "", state: [a, s
73
73
  return _jsxs("div", { style: { position: "relative", width: "min-content", ...style }, className: className, children: [_jsx("div", { onClick: () => setA(!a), children: typeof button == "function" ? button(a) : button }), a && (typeof children == "function" ? children({ onClose: () => setA(!a) }) : children)] });
74
74
  }
75
75
  const saveStatus = {};
76
- export function Button({ keySave, statusDef, outClick, ...data }) {
76
+ export function Button({ keyForSave, keySave, statusDef, outClick, ...data }) {
77
+ keySave = keyForSave ?? keySave;
77
78
  if (keySave && saveStatus[keySave] != null)
78
79
  statusDef = saveStatus[keySave];
79
80
  const [status, setStatusRaw] = useState(statusDef ?? false);