@visns-studio/visns-components 6.5.4 → 6.6.0
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/package.json +1 -1
- package/src/components/DataGrid.jsx +71 -48
- package/src/components/auth/Login.jsx +5 -5
- package/src/components/generic/GenericAuth.jsx +57 -11
- package/src/components/generic/GenericIndex.jsx +35 -3
- package/src/components/generic/GenericMain.jsx +8 -0
- package/src/components/styles/DataGrid.module.scss +11 -11
- package/src/components/styles/Field.module.scss +5 -5
- package/src/components/styles/GenericDetail.module.scss +6 -3
- package/src/components/styles/GenericIndex.module.scss +9 -2
- package/src/components/styles/GenericMain.module.scss +57 -4
- package/src/components/styles/Navigation.module.scss +11 -7
- package/src/components/styles/_controls.scss +4 -4
- package/src/components/styles/density.css +85 -0
- package/src/components/styles/global-datagrid.css +50 -167
- package/src/components/utils/useDensity.js +225 -0
- package/src/index.js +4 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { useMemo, useState, useEffect, useSyncExternalStore } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Display density, in JS.
|
|
5
|
+
*
|
|
6
|
+
* Two independent switches decide how roomy the UI is, and both live on the
|
|
7
|
+
* document rather than in React state, because both are set by whoever owns
|
|
8
|
+
* the page rather than by the component reading them:
|
|
9
|
+
*
|
|
10
|
+
* - `data-density` on `<html>` — the app-wide opt-in, written by GenericAuth
|
|
11
|
+
* from its `density` prop (see `styles/density.css` for the CSS half).
|
|
12
|
+
* - `tablet-mode` on `<body>` — the per-view opt-in, written by GenericIndex
|
|
13
|
+
* when a view config sets `"tabletMode": true`.
|
|
14
|
+
*
|
|
15
|
+
* They compose: a tablet view inside a large-density app is the roomiest of
|
|
16
|
+
* the four combinations. CSS reads both selectors directly; anything that has
|
|
17
|
+
* to lay out in JS — the datagrid measures and sizes rows in px — reads them
|
|
18
|
+
* through `useDensity()` so there is one table of numbers instead of a
|
|
19
|
+
* `isTabletMode ? 52 : 32` at every call site.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The px geometry the datagrid cannot express in CSS.
|
|
24
|
+
*
|
|
25
|
+
* `default.desktop` / `default.tablet` are the values the grid has always
|
|
26
|
+
* used and must not drift — `tests/density.test.mjs` pins them.
|
|
27
|
+
*
|
|
28
|
+
* rowHeight fixed row height, or null to let the grid measure rows.
|
|
29
|
+
* minRowHeight floor applied to measured rows.
|
|
30
|
+
* rowUnit the height one row actually occupies, used to work out how
|
|
31
|
+
* many rows fit a page.
|
|
32
|
+
* columnMinWidth floor for a column with no explicit `minWidth`.
|
|
33
|
+
* actionColUnit width one action icon needs inside an action column.
|
|
34
|
+
* checkboxColWidth fixed width of the row-select checkbox column.
|
|
35
|
+
*/
|
|
36
|
+
export const DENSITY_GRID = {
|
|
37
|
+
default: {
|
|
38
|
+
desktop: {
|
|
39
|
+
rowHeight: null,
|
|
40
|
+
minRowHeight: 32,
|
|
41
|
+
rowUnit: 32,
|
|
42
|
+
columnMinWidth: 80,
|
|
43
|
+
actionColUnit: 34,
|
|
44
|
+
checkboxColWidth: 60,
|
|
45
|
+
},
|
|
46
|
+
tablet: {
|
|
47
|
+
rowHeight: 52,
|
|
48
|
+
minRowHeight: 52,
|
|
49
|
+
rowUnit: 52,
|
|
50
|
+
columnMinWidth: 80,
|
|
51
|
+
actionColUnit: 44,
|
|
52
|
+
checkboxColWidth: 60,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
large: {
|
|
56
|
+
desktop: {
|
|
57
|
+
rowHeight: null,
|
|
58
|
+
minRowHeight: 40,
|
|
59
|
+
rowUnit: 40,
|
|
60
|
+
columnMinWidth: 96,
|
|
61
|
+
actionColUnit: 44,
|
|
62
|
+
checkboxColWidth: 72,
|
|
63
|
+
},
|
|
64
|
+
tablet: {
|
|
65
|
+
rowHeight: 60,
|
|
66
|
+
minRowHeight: 60,
|
|
67
|
+
rowUnit: 60,
|
|
68
|
+
columnMinWidth: 96,
|
|
69
|
+
actionColUnit: 52,
|
|
70
|
+
checkboxColWidth: 80,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const DEFAULT_DENSITY = 'default';
|
|
76
|
+
|
|
77
|
+
const SERVER_SNAPSHOT = `${DEFAULT_DENSITY}|false`;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Read both switches off the document as one string. A string (not an object)
|
|
81
|
+
* so `useSyncExternalStore` can compare snapshots by value — returning a fresh
|
|
82
|
+
* object from `getSnapshot` makes React re-render forever.
|
|
83
|
+
*/
|
|
84
|
+
const readSnapshot = () => {
|
|
85
|
+
if (typeof document === 'undefined') {
|
|
86
|
+
return SERVER_SNAPSHOT;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const density =
|
|
90
|
+
document.documentElement?.getAttribute('data-density') ||
|
|
91
|
+
DEFAULT_DENSITY;
|
|
92
|
+
const isTablet = Boolean(
|
|
93
|
+
document.body && document.body.classList.contains('tablet-mode')
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
return `${density}|${isTablet}`;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const getServerSnapshot = () => SERVER_SNAPSHOT;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Notify on either switch changing. Two observers rather than one on the
|
|
103
|
+
* document, because we only care about one attribute on each of two nodes and
|
|
104
|
+
* a subtree observer would fire on every class change anywhere in the app.
|
|
105
|
+
*/
|
|
106
|
+
const subscribe = (onChange) => {
|
|
107
|
+
if (
|
|
108
|
+
typeof document === 'undefined' ||
|
|
109
|
+
typeof MutationObserver === 'undefined'
|
|
110
|
+
) {
|
|
111
|
+
return () => {};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const observers = [];
|
|
115
|
+
|
|
116
|
+
if (document.documentElement) {
|
|
117
|
+
const rootObserver = new MutationObserver(onChange);
|
|
118
|
+
rootObserver.observe(document.documentElement, {
|
|
119
|
+
attributes: true,
|
|
120
|
+
attributeFilter: ['data-density'],
|
|
121
|
+
});
|
|
122
|
+
observers.push(rootObserver);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (document.body) {
|
|
126
|
+
const bodyObserver = new MutationObserver(onChange);
|
|
127
|
+
bodyObserver.observe(document.body, {
|
|
128
|
+
attributes: true,
|
|
129
|
+
attributeFilter: ['class'],
|
|
130
|
+
});
|
|
131
|
+
observers.push(bodyObserver);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return () => observers.forEach((observer) => observer.disconnect());
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Resolve a snapshot string into the shape callers want.
|
|
139
|
+
*
|
|
140
|
+
* @param {string} snapshot `"<density>|<isTabletMode>"`
|
|
141
|
+
* @returns {{density: string, isLarge: boolean, isTabletMode: boolean, grid: object}}
|
|
142
|
+
*/
|
|
143
|
+
export const parseDensitySnapshot = (snapshot) => {
|
|
144
|
+
const [rawDensity, rawTablet] = String(snapshot).split('|');
|
|
145
|
+
// An unrecognised attribute value styles as nothing in CSS, so it must
|
|
146
|
+
// measure as nothing here too.
|
|
147
|
+
const density = DENSITY_GRID[rawDensity] ? rawDensity : DEFAULT_DENSITY;
|
|
148
|
+
const isTabletMode = rawTablet === 'true';
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
density,
|
|
152
|
+
isLarge: density === 'large',
|
|
153
|
+
isTabletMode,
|
|
154
|
+
grid: DENSITY_GRID[density][isTabletMode ? 'tablet' : 'desktop'],
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* @returns {{density: string, isLarge: boolean, isTabletMode: boolean, grid: object}}
|
|
160
|
+
* `grid` is a stable reference out of `DENSITY_GRID`, so it is safe
|
|
161
|
+
* in a dependency array.
|
|
162
|
+
*/
|
|
163
|
+
export const useDensity = () => {
|
|
164
|
+
const snapshot = useSyncExternalStore(
|
|
165
|
+
subscribe,
|
|
166
|
+
readSnapshot,
|
|
167
|
+
getServerSnapshot
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
return useMemo(() => parseDensitySnapshot(snapshot), [snapshot]);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Height left on screen below `ref`'s element, for panes that should fill the
|
|
175
|
+
* window instead of taking a fraction of it.
|
|
176
|
+
*
|
|
177
|
+
* Returns `null` until it can measure — no element, no window, or a
|
|
178
|
+
* measurement that comes out non-positive (the element is display:none, or
|
|
179
|
+
* scrolled out of view) — so callers can keep their own fallback rather than
|
|
180
|
+
* flashing a zero-height pane.
|
|
181
|
+
*
|
|
182
|
+
* @param {{current: HTMLElement|null}} ref wrapper to measure from.
|
|
183
|
+
* @param {object} [options]
|
|
184
|
+
* @param {boolean} [options.enabled=true] false parks the hook at null.
|
|
185
|
+
* @param {number} [options.bottomGutter=16] px left below the element.
|
|
186
|
+
* @param {*} [options.recomputeKey] change it to force a re-measure
|
|
187
|
+
* (e.g. the app's window height).
|
|
188
|
+
* @returns {number|null}
|
|
189
|
+
*/
|
|
190
|
+
export const useAvailableHeight = (
|
|
191
|
+
ref,
|
|
192
|
+
{ enabled = true, bottomGutter = 16, recomputeKey } = {}
|
|
193
|
+
) => {
|
|
194
|
+
const [height, setHeight] = useState(null);
|
|
195
|
+
|
|
196
|
+
useEffect(() => {
|
|
197
|
+
if (!enabled || typeof window === 'undefined') {
|
|
198
|
+
setHeight(null);
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const measure = () => {
|
|
203
|
+
const node = ref?.current;
|
|
204
|
+
|
|
205
|
+
if (!node || typeof node.getBoundingClientRect !== 'function') {
|
|
206
|
+
setHeight(null);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const { top } = node.getBoundingClientRect();
|
|
211
|
+
const next = window.innerHeight - top - bottomGutter;
|
|
212
|
+
|
|
213
|
+
setHeight(Number.isFinite(next) && next > 0 ? next : null);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
measure();
|
|
217
|
+
window.addEventListener('resize', measure);
|
|
218
|
+
|
|
219
|
+
return () => window.removeEventListener('resize', measure);
|
|
220
|
+
}, [ref, enabled, bottomGutter, recomputeKey]);
|
|
221
|
+
|
|
222
|
+
return height;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
export default useDensity;
|
package/src/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import SortableList from './components/sorting/List';
|
|
|
7
7
|
/** Utility Components */
|
|
8
8
|
import { confirmDialog } from './components/utils/ConfirmDialog';
|
|
9
9
|
import { showConfirmDialog } from './components/generic/ConfirmationDialog';
|
|
10
|
+
import { useDensity, useAvailableHeight, DENSITY_GRID } from './components/utils/useDensity';
|
|
10
11
|
|
|
11
12
|
/** CRM Components */
|
|
12
13
|
import AsyncSelect from './components/AsyncSelect';
|
|
@@ -121,6 +122,7 @@ export {
|
|
|
121
122
|
DataGrid,
|
|
122
123
|
DataGridSearch,
|
|
123
124
|
DatePickerPortal,
|
|
125
|
+
DENSITY_GRID,
|
|
124
126
|
Download,
|
|
125
127
|
DropZone,
|
|
126
128
|
CategorizedDropZone,
|
|
@@ -173,6 +175,8 @@ export {
|
|
|
173
175
|
StandardModal,
|
|
174
176
|
Table,
|
|
175
177
|
TableFilter,
|
|
178
|
+
useAvailableHeight,
|
|
179
|
+
useDensity,
|
|
176
180
|
VariableInserter,
|
|
177
181
|
Verify,
|
|
178
182
|
};
|