@visns-studio/visns-components 6.5.5 → 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/generic/GenericAuth.jsx +23 -1
- 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
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
chips. default: 4px
|
|
35
35
|
--nav-dropdown-item-padding padding on a dropdown menu row.
|
|
36
36
|
default: 8px 20px
|
|
37
|
+
--nav-logo-height height of the logo plate, and the same figure
|
|
38
|
+
everywhere the rail, the content header and the
|
|
39
|
+
mobile drawer offset themselves by it.
|
|
40
|
+
default: 50px
|
|
37
41
|
|
|
38
42
|
Two deliberate omissions: the icon-to-label gap inside a nav item and the
|
|
39
43
|
padding on the chips themselves are internal alignment, not density — the
|
|
@@ -711,7 +715,7 @@
|
|
|
711
715
|
A project whose page background is the same colour can merge the
|
|
712
716
|
plate into the rail instead: `--nav-logo-bg: var(--nav-bg)`. */
|
|
713
717
|
background: var(--nav-logo-bg, rgba(var(--tertiary-color-rgb), 0.95));
|
|
714
|
-
height: 50px;
|
|
718
|
+
height: var(--nav-logo-height, 50px);
|
|
715
719
|
display: flex;
|
|
716
720
|
flex-wrap: nowrap;
|
|
717
721
|
justify-content: center;
|
|
@@ -727,7 +731,7 @@
|
|
|
727
731
|
width: 100%;
|
|
728
732
|
position: relative;
|
|
729
733
|
overflow-y: auto; /* Enable vertical scrolling */
|
|
730
|
-
max-height: calc(100vh - 50px); /* Subtract the logo height */
|
|
734
|
+
max-height: calc(100vh - var(--nav-logo-height, 50px)); /* Subtract the logo height */
|
|
731
735
|
scrollbar-width: none; /* Firefox */
|
|
732
736
|
-ms-overflow-style: none; /* IE and Edge */
|
|
733
737
|
|
|
@@ -1029,7 +1033,7 @@
|
|
|
1029
1033
|
background: var(--tertiary-color);
|
|
1030
1034
|
box-sizing: border-box;
|
|
1031
1035
|
padding: 0.15rem;
|
|
1032
|
-
height: 50px;
|
|
1036
|
+
height: var(--nav-logo-height, 50px);
|
|
1033
1037
|
box-shadow: var(--box-shadow-small);
|
|
1034
1038
|
display: flex;
|
|
1035
1039
|
flex-wrap: nowrap;
|
|
@@ -1150,8 +1154,8 @@
|
|
|
1150
1154
|
below it: start the drawer under the 50px header instead of
|
|
1151
1155
|
letting the header paint over the drawer's top row. This also
|
|
1152
1156
|
keeps the hamburger tappable while the drawer is open. */
|
|
1153
|
-
top: 50px;
|
|
1154
|
-
height: calc(100% - 50px);
|
|
1157
|
+
top: var(--nav-logo-height, 50px);
|
|
1158
|
+
height: calc(100% - var(--nav-logo-height, 50px));
|
|
1155
1159
|
transform: translateX(-2000px);
|
|
1156
1160
|
transition: all 0.55s cubic-bezier(0.25, 0.8, 0.25, 1);
|
|
1157
1161
|
will-change: transform;
|
|
@@ -1214,13 +1218,13 @@
|
|
|
1214
1218
|
|
|
1215
1219
|
.content--full {
|
|
1216
1220
|
/* The 50px bar is still there below this width. */
|
|
1217
|
-
min-height: calc(100vh - 50px);
|
|
1221
|
+
min-height: calc(100vh - var(--nav-logo-height, 50px));
|
|
1218
1222
|
}
|
|
1219
1223
|
|
|
1220
1224
|
.drawer-backdrop {
|
|
1221
1225
|
display: block;
|
|
1222
1226
|
position: fixed;
|
|
1223
|
-
top: 50px;
|
|
1227
|
+
top: var(--nav-logo-height, 50px);
|
|
1224
1228
|
left: 0;
|
|
1225
1229
|
right: 0;
|
|
1226
1230
|
bottom: 0;
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
justify-content: center;
|
|
18
18
|
gap: 0.4rem;
|
|
19
19
|
width: max-content;
|
|
20
|
-
min-height: 2.5rem;
|
|
20
|
+
min-height: var(--control-height, 2.5rem);
|
|
21
21
|
padding: 0 var(--spacing-md);
|
|
22
22
|
border: 1px solid transparent;
|
|
23
23
|
border-radius: var(--radius-sm);
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
@mixin button-small {
|
|
105
|
-
min-height: 2rem;
|
|
105
|
+
min-height: var(--control-height-sm, 2rem);
|
|
106
106
|
padding: 0 var(--spacing-sm);
|
|
107
107
|
font-size: var(--font-size-xs);
|
|
108
108
|
}
|
|
@@ -111,8 +111,8 @@
|
|
|
111
111
|
|
|
112
112
|
@mixin input-base {
|
|
113
113
|
width: 100%;
|
|
114
|
-
min-height: 2.5rem;
|
|
115
|
-
padding: 0 0.65rem;
|
|
114
|
+
min-height: var(--control-height, 2.5rem);
|
|
115
|
+
padding: 0 var(--input-padding-x, 0.65rem);
|
|
116
116
|
border: 1px solid var(--border-color);
|
|
117
117
|
border-radius: var(--radius-sm);
|
|
118
118
|
background: var(--paper);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
Display density
|
|
3
|
+
----------------------------------------------------------------------------
|
|
4
|
+
One opt-in mode — `large` — for the projects that run on a shop-floor
|
|
5
|
+
monitor, a wall-mounted screen or in front of someone who would rather not
|
|
6
|
+
lean in. Everything in this file hangs off `:root[data-density='large']`,
|
|
7
|
+
and that attribute is only ever written by GenericAuth when the app passes
|
|
8
|
+
the `density` prop:
|
|
9
|
+
|
|
10
|
+
<GenericAuth density="large" ... />
|
|
11
|
+
|
|
12
|
+
No prop, no attribute, no rule in here matches — an app that never opts in
|
|
13
|
+
renders exactly as it always has. There is nothing to import, disable or
|
|
14
|
+
fork on that path.
|
|
15
|
+
|
|
16
|
+
The lever is rem, not a table of overrides. `font-size: 112.5%` on the root
|
|
17
|
+
takes the browser's 16px up to 18px, and every rem-based token, control,
|
|
18
|
+
gap and type size in the library follows on its own — that is most of the
|
|
19
|
+
mode, done once. The custom properties below only exist to catch what rem
|
|
20
|
+
cannot reach: the px-valued hooks the components expose (row heights, cell
|
|
21
|
+
padding, fixed chrome heights, pocket squares) and the page-width caps that
|
|
22
|
+
should simply stop capping when the screen is being used as a big screen.
|
|
23
|
+
|
|
24
|
+
Overriding a value
|
|
25
|
+
------------------
|
|
26
|
+
Apps override any of these from their own global stylesheet with the more
|
|
27
|
+
specific selector:
|
|
28
|
+
|
|
29
|
+
html:root[data-density='large'] {
|
|
30
|
+
--control-height: 3rem;
|
|
31
|
+
--grid-cell-padding: 12px 14px;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
`html:root[...]` outweighs the plain `:root[...]` used here on specificity
|
|
35
|
+
alone, so it wins no matter which stylesheet the bundler emits first — no
|
|
36
|
+
`!important`, no import-order archaeology.
|
|
37
|
+
|
|
38
|
+
The root font-size has its own escape hatch so an app can dial the whole
|
|
39
|
+
mode up or down without restating a single hook:
|
|
40
|
+
|
|
41
|
+
html:root[data-density='large'] { --density-root-font-size: 125%; }
|
|
42
|
+
|
|
43
|
+
Anything the app sets on plain `:root` stays the shared value for both
|
|
44
|
+
densities — set it here only when `large` should differ.
|
|
45
|
+
========================================================================= */
|
|
46
|
+
|
|
47
|
+
:root[data-density='large'] {
|
|
48
|
+
font-size: var(--density-root-font-size, 112.5%);
|
|
49
|
+
|
|
50
|
+
/* Page frame — a large display is asked for width, so stop capping it. */
|
|
51
|
+
--page-max-width: none;
|
|
52
|
+
--page-gutter-x: clamp(0.5rem, 1vw, 1rem);
|
|
53
|
+
--page-gutter-y: 0.75rem;
|
|
54
|
+
--page-gutter-narrow: 0.5rem;
|
|
55
|
+
|
|
56
|
+
/* Controls */
|
|
57
|
+
--control-height: 2.75rem;
|
|
58
|
+
--control-height-sm: 2.25rem;
|
|
59
|
+
|
|
60
|
+
/* Data grid — px, because the grid measures and lays out in px. */
|
|
61
|
+
--grid-toolbar-height: 46px;
|
|
62
|
+
--grid-cell-padding: 9px 10px;
|
|
63
|
+
--grid-checkbox-col-width: 72px;
|
|
64
|
+
|
|
65
|
+
/* Fixed chrome */
|
|
66
|
+
--content-header-height: 60px;
|
|
67
|
+
--nav-logo-height: 56px;
|
|
68
|
+
|
|
69
|
+
/* Field pockets (the square icon/affix tiles beside an input) */
|
|
70
|
+
--field-pocket-size: 44px;
|
|
71
|
+
--field-pocket-size-sm: 34px;
|
|
72
|
+
--field-suggestion-font-size: 16px;
|
|
73
|
+
|
|
74
|
+
/* Detail pages */
|
|
75
|
+
--detail-max-width: 1600px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* The row-select checkbox column, one step wider again on a tablet view —
|
|
79
|
+
the row is 60px tall there, so the tap zone should be square-ish. The JS
|
|
80
|
+
side of this number lives in utils/useDensity.js (checkboxColWidth); the
|
|
81
|
+
two must agree, since the grid lays the column out from the JS value and
|
|
82
|
+
these custom properties only hold the cell against Inovua's inline width. */
|
|
83
|
+
:root[data-density='large'] body.tablet-mode {
|
|
84
|
+
--grid-checkbox-col-width: 80px;
|
|
85
|
+
}
|
|
@@ -45,10 +45,14 @@ div[role="tooltip"],
|
|
|
45
45
|
border-radius: 0 !important;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
/* Ensure consistent border radius for cells and reduce padding for compact rows
|
|
49
|
-
|
|
48
|
+
/* Ensure consistent border radius for cells and reduce padding for compact rows.
|
|
49
|
+
:not(--no-padding): the grid zeroes padding inline on its spacer/expand
|
|
50
|
+
cells (group nesting columns) and sizes them width:0 when unused — an
|
|
51
|
+
unconditional !important here re-inflates those to pure padding and
|
|
52
|
+
shifts group rows out of line with the headers. */
|
|
53
|
+
.InovuaReactDataGrid__cell:not(.InovuaReactDataGrid__cell--no-padding) {
|
|
50
54
|
border-radius: 0 !important;
|
|
51
|
-
padding: 6px 8px !important; /* Reduced from default padding */
|
|
55
|
+
padding: var(--grid-cell-padding, 6px 8px) !important; /* Reduced from default padding */
|
|
52
56
|
}
|
|
53
57
|
|
|
54
58
|
/* Word wrap enabled cells - specific class for columns with wordWrap: true */
|
|
@@ -84,10 +88,13 @@ body.tablet-mode .InovuaReactDataGrid__cell.cell-word-wrap span {
|
|
|
84
88
|
max-height: 100% !important;
|
|
85
89
|
}
|
|
86
90
|
|
|
87
|
-
body.tablet-mode
|
|
91
|
+
body.tablet-mode
|
|
92
|
+
.InovuaReactDataGrid__cell.cell-word-wrap:not(
|
|
93
|
+
.InovuaReactDataGrid__cell--no-padding
|
|
94
|
+
) {
|
|
88
95
|
height: 100% !important;
|
|
89
96
|
align-items: center !important;
|
|
90
|
-
padding: 6px 8px !important;
|
|
97
|
+
padding: var(--grid-cell-padding, 6px 8px) !important;
|
|
91
98
|
}
|
|
92
99
|
|
|
93
100
|
body.tablet-mode .InovuaReactDataGrid__cell.cell-word-wrap > div {
|
|
@@ -103,10 +110,10 @@ body.tablet-mode .InovuaReactDataGrid__cell.cell-word-wrap > div {
|
|
|
103
110
|
overflow: hidden !important;
|
|
104
111
|
}
|
|
105
112
|
|
|
106
|
-
/*
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
/* NOTE: this fork renders header cells as .InovuaReactDataGrid__column-header
|
|
114
|
+
(there is no __header-cell class in its DOM), so header padding is left to
|
|
115
|
+
the module stylesheet; a __header-cell padding rule that used to sit here
|
|
116
|
+
was dead code and has been removed. */
|
|
110
117
|
|
|
111
118
|
/* Fix for focus outline */
|
|
112
119
|
.InovuaReactDataGrid:focus,
|
|
@@ -115,173 +122,49 @@ body.tablet-mode .InovuaReactDataGrid__cell.cell-word-wrap > div {
|
|
|
115
122
|
box-shadow: 0 0 0 2px rgba(var(--primary-rgb, 59, 130, 246), 0.2) !important;
|
|
116
123
|
}
|
|
117
124
|
|
|
118
|
-
/* Checkbox column
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
138
|
-
input[type='checkbox'] {
|
|
139
|
-
width: 20px !important;
|
|
140
|
-
height: 20px !important;
|
|
141
|
-
margin: 0 !important;
|
|
142
|
-
cursor: pointer !important;
|
|
143
|
-
position: relative !important;
|
|
144
|
-
z-index: 2 !important;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/* Create larger touch target area around checkbox */
|
|
148
|
-
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
149
|
-
.InovuaReactDataGrid__checkbox-wrapper,
|
|
150
|
-
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
125
|
+
/* Checkbox column.
|
|
126
|
+
|
|
127
|
+
The rules that used to live here keyed every declaration on
|
|
128
|
+
[data-column-id='__checkbox-column'] — an attribute this datagrid fork has
|
|
129
|
+
never rendered — so none of them ever matched: not the 60px width pins,
|
|
130
|
+
not the 44px touch wrappers. Discovered Aug 2026 when the column collapsed
|
|
131
|
+
to the grid's internal 40px default with no CSS backstop to catch it.
|
|
132
|
+
|
|
133
|
+
The column's width is owned by the JS side (checkboxColumnProps in
|
|
134
|
+
DataGrid.jsx, fed from utils/useDensity.js checkboxColWidth); CSS cannot
|
|
135
|
+
safely fight the grid's layout engine on width. What CSS can usefully add
|
|
136
|
+
is glyph size and cursor, on the classes that actually exist:
|
|
137
|
+
.InovuaReactDataGrid__checkbox is the toolkit checkbox root in body cells
|
|
138
|
+
(it draws a 16px svg with a proper viewBox, so CSS sizing scales it
|
|
139
|
+
cleanly), and the header cell holds a native input rendered by
|
|
140
|
+
DataGrid.jsx. Gated on large density; default-density apps keep the
|
|
141
|
+
rendering they have always had. */
|
|
142
|
+
:root[data-density='large']
|
|
143
|
+
.InovuaReactDataGrid__cell
|
|
151
144
|
.InovuaReactDataGrid__checkbox {
|
|
152
|
-
|
|
153
|
-
align-items: center !important;
|
|
154
|
-
justify-content: center !important;
|
|
155
|
-
min-height: 44px !important;
|
|
156
|
-
min-width: 44px !important;
|
|
157
|
-
padding: 12px !important;
|
|
158
|
-
margin: -12px !important;
|
|
159
|
-
cursor: pointer !important;
|
|
160
|
-
border-radius: 4px !important;
|
|
161
|
-
transition: background-color 0.2s ease !important;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/* Touch-friendly hover effect for the entire cell */
|
|
165
|
-
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']:hover {
|
|
166
|
-
background-color: rgba(var(--primary-rgb, 59, 130, 246), 0.05) !important;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/* Touch-friendly hover effect for checkbox wrapper */
|
|
170
|
-
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
171
|
-
.InovuaReactDataGrid__checkbox-wrapper:hover,
|
|
172
|
-
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
173
|
-
.InovuaReactDataGrid__checkbox:hover {
|
|
174
|
-
background-color: rgba(var(--primary-rgb, 59, 130, 246), 0.1) !important;
|
|
145
|
+
cursor: pointer;
|
|
175
146
|
}
|
|
176
147
|
|
|
177
|
-
|
|
178
|
-
.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
max-width: 60px !important;
|
|
148
|
+
:root[data-density='large']
|
|
149
|
+
.InovuaReactDataGrid__cell
|
|
150
|
+
.InovuaReactDataGrid__checkbox svg {
|
|
151
|
+
width: 22px;
|
|
152
|
+
height: 22px;
|
|
183
153
|
}
|
|
184
154
|
|
|
185
|
-
.
|
|
155
|
+
/* !important because global.css draws every non-react-toggle checkbox at a
|
|
156
|
+
fixed 14px !important — an inline style cannot beat that, and neither can
|
|
157
|
+
this rule without its own !important. The header cell in this fork is
|
|
158
|
+
.InovuaReactDataGrid__column-header (never __header-cell). */
|
|
159
|
+
:root[data-density='large']
|
|
160
|
+
.InovuaReactDataGrid__column-header
|
|
186
161
|
input[type='checkbox'] {
|
|
187
|
-
width:
|
|
188
|
-
height:
|
|
189
|
-
|
|
190
|
-
cursor: pointer !important;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/* Prevent text selection in checkbox column */
|
|
194
|
-
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column'] {
|
|
195
|
-
user-select: none !important;
|
|
196
|
-
-webkit-user-select: none !important;
|
|
197
|
-
-moz-user-select: none !important;
|
|
198
|
-
-ms-user-select: none !important;
|
|
162
|
+
width: 22px !important;
|
|
163
|
+
height: 22px !important;
|
|
164
|
+
cursor: pointer;
|
|
199
165
|
}
|
|
200
166
|
|
|
201
|
-
/* Additional checkbox styling for better UX */
|
|
202
|
-
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
203
|
-
.InovuaReactDataGrid__cell-content {
|
|
204
|
-
display: flex !important;
|
|
205
|
-
align-items: center !important;
|
|
206
|
-
justify-content: center !important;
|
|
207
|
-
width: 100% !important;
|
|
208
|
-
height: 100% !important;
|
|
209
|
-
}
|
|
210
167
|
|
|
211
|
-
/* Focus styles for accessibility */
|
|
212
|
-
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
213
|
-
input[type='checkbox']:focus {
|
|
214
|
-
outline: 2px solid rgba(var(--primary-rgb, 59, 130, 246), 0.5) !important;
|
|
215
|
-
outline-offset: 2px !important;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/* Ensure checkbox column doesn't interfere with row hover */
|
|
219
|
-
.InovuaReactDataGrid__row:hover
|
|
220
|
-
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column'] {
|
|
221
|
-
background-color: inherit !important;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/* Mobile-specific improvements - Compact version */
|
|
225
|
-
@media (max-width: 768px) {
|
|
226
|
-
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column'] {
|
|
227
|
-
min-width: 70px !important;
|
|
228
|
-
width: 70px !important;
|
|
229
|
-
max-width: 70px !important;
|
|
230
|
-
padding: 8px 12px !important; /* Reduced padding for compact rows */
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
234
|
-
input[type='checkbox'] {
|
|
235
|
-
width: 24px !important;
|
|
236
|
-
height: 24px !important;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
240
|
-
.InovuaReactDataGrid__checkbox-wrapper,
|
|
241
|
-
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
242
|
-
.InovuaReactDataGrid__checkbox {
|
|
243
|
-
min-height: 48px !important;
|
|
244
|
-
min-width: 48px !important;
|
|
245
|
-
padding: 16px !important;
|
|
246
|
-
margin: -16px !important;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
.InovuaReactDataGrid__header-cell[data-column-id='__checkbox-column'] {
|
|
250
|
-
min-width: 70px !important;
|
|
251
|
-
width: 70px !important;
|
|
252
|
-
max-width: 70px !important;
|
|
253
|
-
padding: 8px 12px !important; /* Reduced padding for compact rows */
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
.InovuaReactDataGrid__header-cell[data-column-id='__checkbox-column']
|
|
257
|
-
input[type='checkbox'] {
|
|
258
|
-
width: 24px !important;
|
|
259
|
-
height: 24px !important;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/* Touch device specific improvements - Compact version */
|
|
264
|
-
@media (hover: none) and (pointer: coarse) {
|
|
265
|
-
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column'] {
|
|
266
|
-
padding: 12px !important; /* Reduced padding for compact rows */
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
270
|
-
input[type='checkbox'] {
|
|
271
|
-
width: 28px !important;
|
|
272
|
-
height: 28px !important;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
276
|
-
.InovuaReactDataGrid__checkbox-wrapper,
|
|
277
|
-
.InovuaReactDataGrid__cell[data-column-id='__checkbox-column']
|
|
278
|
-
.InovuaReactDataGrid__checkbox {
|
|
279
|
-
min-height: 52px !important;
|
|
280
|
-
min-width: 52px !important;
|
|
281
|
-
padding: 18px !important;
|
|
282
|
-
margin: -18px !important;
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
168
|
|
|
286
169
|
/* Group expand/collapse icon fixes - Enhanced for better visibility */
|
|
287
170
|
.InovuaReactDataGrid__group-expand-icon,
|
|
@@ -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
|
};
|