@visns-studio/visns-components 6.5.5 → 6.6.1
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 +97 -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
package/package.json
CHANGED
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"react-dom": "^17.0.0 || ^18.0.0"
|
|
92
92
|
},
|
|
93
93
|
"name": "@visns-studio/visns-components",
|
|
94
|
-
"version": "6.
|
|
94
|
+
"version": "6.6.1",
|
|
95
95
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
96
96
|
"main": "src/index.js",
|
|
97
97
|
"files": [
|
|
@@ -43,6 +43,7 @@ import NativeDateFilterEditor from './columns/NativeDateFilterEditor';
|
|
|
43
43
|
import { toast } from 'react-toastify';
|
|
44
44
|
import fetchUtil from '../utils/fetchUtil';
|
|
45
45
|
import { confirmDialog } from './utils/ConfirmDialog';
|
|
46
|
+
import { useDensity } from './utils/useDensity';
|
|
46
47
|
import { DEFAULT_INTELLIGENT_SORTING_CONFIG } from '../utils/relationshipSortingUtils';
|
|
47
48
|
import {
|
|
48
49
|
extractColumnsMetadata,
|
|
@@ -870,6 +871,25 @@ const DataGrid = forwardRef(
|
|
|
870
871
|
return initialLimit;
|
|
871
872
|
});
|
|
872
873
|
const [search, setSearch] = useState('');
|
|
874
|
+
|
|
875
|
+
// This component instance survives client-side route changes — React Router
|
|
876
|
+
// reconciles the new page's grid into the same DataGrid element, and only the
|
|
877
|
+
// inner ReactDataGrid remounts (its key includes the ajax url). Without this
|
|
878
|
+
// reset the previous page's filters and search ride into the new page's very
|
|
879
|
+
// first request, including whereHas relations the new page's model may not
|
|
880
|
+
// have at all, which the backend answers with a 500. It has to happen during
|
|
881
|
+
// render rather than in an effect: an effect would fire only after the
|
|
882
|
+
// remounted inner grid has already mounted and issued that first fetch, so
|
|
883
|
+
// adjusting the state here is what keeps the stale values from ever reaching
|
|
884
|
+
// it. The equality guards keep this from looping.
|
|
885
|
+
const prevAjaxUrlRef = useRef(ajaxSetting?.url);
|
|
886
|
+
if (prevAjaxUrlRef.current !== ajaxSetting?.url) {
|
|
887
|
+
prevAjaxUrlRef.current = ajaxSetting?.url;
|
|
888
|
+
if (filterValue.length > 0) setFilterValue([]);
|
|
889
|
+
if (search !== '') setSearch('');
|
|
890
|
+
if (dSearch !== '') setDSearch('');
|
|
891
|
+
}
|
|
892
|
+
|
|
873
893
|
const [selected, setSelected] = useState({});
|
|
874
894
|
const [inputDebounceTimers] = useState({});
|
|
875
895
|
const [localInputValues, setLocalInputValues] = useState({});
|
|
@@ -923,31 +943,19 @@ const DataGrid = forwardRef(
|
|
|
923
943
|
};
|
|
924
944
|
}, []);
|
|
925
945
|
|
|
926
|
-
//
|
|
927
|
-
//
|
|
946
|
+
// The px geometry this grid lays out with — row heights, the width one
|
|
947
|
+
// action icon needs, the floor under a column — comes from the density
|
|
948
|
+
// table rather than from constants here, so app density (data-density
|
|
949
|
+
// on <html>) and per-view tablet mode (body.tablet-mode) are decided in
|
|
950
|
+
// one place. `grid` is a stable reference out of DENSITY_GRID, so it is
|
|
951
|
+
// safe in the dependency arrays below.
|
|
952
|
+
//
|
|
953
|
+
// Why a fixed rowHeight matters in the roomier modes: with
|
|
954
|
+
// rowHeight={null} Inovua measures each row's content, which is
|
|
928
955
|
// unreliable after auto-refresh on an idle tab (background-throttled
|
|
929
|
-
// rAF/timers). A fixed height
|
|
930
|
-
//
|
|
931
|
-
const
|
|
932
|
-
typeof document !== 'undefined' &&
|
|
933
|
-
document.body.classList.contains('tablet-mode')
|
|
934
|
-
);
|
|
935
|
-
useEffect(() => {
|
|
936
|
-
if (typeof document === 'undefined') {
|
|
937
|
-
return undefined;
|
|
938
|
-
}
|
|
939
|
-
const sync = () =>
|
|
940
|
-
setIsTabletMode(
|
|
941
|
-
document.body.classList.contains('tablet-mode')
|
|
942
|
-
);
|
|
943
|
-
sync();
|
|
944
|
-
const observer = new MutationObserver(sync);
|
|
945
|
-
observer.observe(document.body, {
|
|
946
|
-
attributes: true,
|
|
947
|
-
attributeFilter: ['class'],
|
|
948
|
-
});
|
|
949
|
-
return () => observer.disconnect();
|
|
950
|
-
}, []);
|
|
956
|
+
// rAF/timers). A fixed height bypasses measurement entirely and
|
|
957
|
+
// prevents action buttons from being clipped.
|
|
958
|
+
const { grid } = useDensity();
|
|
951
959
|
|
|
952
960
|
// Effect to handle autoRefresh setting
|
|
953
961
|
useEffect(() => {
|
|
@@ -3577,6 +3585,7 @@ const DataGrid = forwardRef(
|
|
|
3577
3585
|
};
|
|
3578
3586
|
|
|
3579
3587
|
useEffect(() => {
|
|
3588
|
+
const requestUrl = ajaxSetting?.url;
|
|
3580
3589
|
const dropdownFetches = columns
|
|
3581
3590
|
.filter((column) =>
|
|
3582
3591
|
['dropdown', 'multiDropdown'].includes(column.type)
|
|
@@ -3593,6 +3602,12 @@ const DataGrid = forwardRef(
|
|
|
3593
3602
|
|
|
3594
3603
|
Promise.all(dropdownFetches)
|
|
3595
3604
|
.then((results) => {
|
|
3605
|
+
// The page changed while these dropdown fetches were in
|
|
3606
|
+
// flight — these columns/filters belong to the previous page.
|
|
3607
|
+
if (requestUrl !== prevAjaxUrlRef.current) {
|
|
3608
|
+
return;
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3596
3611
|
// Merge all resolved objects into a single object
|
|
3597
3612
|
const dropdownData = results.reduce((acc, result) => {
|
|
3598
3613
|
return { ...acc, ...result };
|
|
@@ -3613,14 +3628,11 @@ const DataGrid = forwardRef(
|
|
|
3613
3628
|
.filter((entry) => !isRowActionConfig(entry))
|
|
3614
3629
|
);
|
|
3615
3630
|
|
|
3616
|
-
// Width to fit N action icons:
|
|
3617
|
-
// tap target/margin
|
|
3631
|
+
// Width to fit N action icons: roomier densities give icons a
|
|
3632
|
+
// larger tap target/margin than the default, plus cell
|
|
3618
3633
|
// padding. Used by both action columns and the trailing column.
|
|
3619
3634
|
const actionColumnWidthFor = (count) =>
|
|
3620
|
-
Math.max(
|
|
3621
|
-
100,
|
|
3622
|
-
count * (isTabletMode ? 44 : 34) + 16
|
|
3623
|
-
);
|
|
3635
|
+
Math.max(100, count * grid.actionColUnit + 16);
|
|
3624
3636
|
|
|
3625
3637
|
const renderColumn = (column) => {
|
|
3626
3638
|
let childValue;
|
|
@@ -3652,7 +3664,7 @@ const DataGrid = forwardRef(
|
|
|
3652
3664
|
// clamped columns exceed the container (narrow
|
|
3653
3665
|
// screens) — columns then render a few px wide
|
|
3654
3666
|
// with no horizontal scrollbar ever appearing.
|
|
3655
|
-
minWidth: column.minWidth ??
|
|
3667
|
+
minWidth: column.minWidth ?? grid.columnMinWidth,
|
|
3656
3668
|
maxWidth: column.maxWidth ?? undefined,
|
|
3657
3669
|
textAlign: column.textAlign ?? undefined,
|
|
3658
3670
|
textVerticalAlign:
|
|
@@ -3822,9 +3834,7 @@ const DataGrid = forwardRef(
|
|
|
3822
3834
|
// what the icons themselves need — tap targets
|
|
3823
3835
|
// stay intact in tablet mode.
|
|
3824
3836
|
const iconsNeed =
|
|
3825
|
-
actionCount *
|
|
3826
|
-
(isTabletMode ? 44 : 34) +
|
|
3827
|
-
16;
|
|
3837
|
+
actionCount * grid.actionColUnit + 16;
|
|
3828
3838
|
const width =
|
|
3829
3839
|
column.maxWidth != null
|
|
3830
3840
|
? Math.max(
|
|
@@ -4757,7 +4767,7 @@ const DataGrid = forwardRef(
|
|
|
4757
4767
|
.catch((error) => {
|
|
4758
4768
|
console.error('Error in fetching dropdown data: ', error);
|
|
4759
4769
|
});
|
|
4760
|
-
}, [columns, filterDataSource, memoizedSettings,
|
|
4770
|
+
}, [columns, filterDataSource, memoizedSettings, grid]);
|
|
4761
4771
|
|
|
4762
4772
|
// Force re-render when filterDataSource updates to ensure dropdown filters get their data
|
|
4763
4773
|
const [renderKey, setRenderKey] = useState(0);
|
|
@@ -4886,13 +4896,16 @@ const DataGrid = forwardRef(
|
|
|
4886
4896
|
} else if (ajaxSetting?.take >= 35) {
|
|
4887
4897
|
setLimit(ajaxSetting.take);
|
|
4888
4898
|
} else {
|
|
4899
|
+
// Divide by the row height actually in force. This was
|
|
4900
|
+
// hardcoded to 32 even in tablet mode, so tablet pages asked
|
|
4901
|
+
// for far more rows than could ever fit on screen.
|
|
4889
4902
|
const calculateRows = (height) =>
|
|
4890
|
-
Math.floor((height - 100) /
|
|
4903
|
+
Math.floor((height - 100) / grid.rowUnit);
|
|
4891
4904
|
|
|
4892
4905
|
const rowCount = calculateRows(minHeight);
|
|
4893
4906
|
setLimit(rowCount);
|
|
4894
4907
|
}
|
|
4895
|
-
}, [dataCount, gridHeight, window.innerHeight, userProfile?.rows_per_page, userProfile?.settings?.rows_per_page]);
|
|
4908
|
+
}, [dataCount, gridHeight, window.innerHeight, grid, userProfile?.rows_per_page, userProfile?.settings?.rows_per_page]);
|
|
4896
4909
|
|
|
4897
4910
|
useEffect(() => {
|
|
4898
4911
|
const debouncedHandleResize = debounce(() => {
|
|
@@ -5578,7 +5591,7 @@ const DataGrid = forwardRef(
|
|
|
5578
5591
|
}
|
|
5579
5592
|
headerProps={headerProps}
|
|
5580
5593
|
idProperty="id"
|
|
5581
|
-
minRowHeight={
|
|
5594
|
+
minRowHeight={grid.minRowHeight}
|
|
5582
5595
|
onFilterValueChange={(fv) => {
|
|
5583
5596
|
handleFilterChange(fv, filterValue);
|
|
5584
5597
|
}}
|
|
@@ -5590,7 +5603,12 @@ const DataGrid = forwardRef(
|
|
|
5590
5603
|
renderRowContextMenu={renderRowContextMenu}
|
|
5591
5604
|
renderColumnContextMenu={renderColumnContextMenu}
|
|
5592
5605
|
rowClassName={rowClassName}
|
|
5593
|
-
|
|
5606
|
+
// null lets the grid measure each row; a number
|
|
5607
|
+
// fixes the height, which the roomier densities
|
|
5608
|
+
// need — measurement is unreliable in a
|
|
5609
|
+
// background-throttled tab and clips action
|
|
5610
|
+
// buttons. See the density table in useDensity.
|
|
5611
|
+
rowHeight={grid.rowHeight}
|
|
5594
5612
|
rowStyle={getRowStyle}
|
|
5595
5613
|
selected={selected}
|
|
5596
5614
|
showZebraRows={true}
|
|
@@ -5616,15 +5634,36 @@ const DataGrid = forwardRef(
|
|
|
5616
5634
|
? 'InovuaReactDataGrid--grouped datagrid-with-grouping datagrid-fixed-group-headers'
|
|
5617
5635
|
: 'datagrid-without-grouping'
|
|
5618
5636
|
}
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5637
|
+
// The real prop is `checkboxColumn` (bool | column
|
|
5638
|
+
// object) — {...tableSetting} spreads the view's
|
|
5639
|
+
// `checkboxColumn: true` and this object, placed
|
|
5640
|
+
// after the spread, replaces it. It previously sat
|
|
5641
|
+
// on a prop named `checkboxColumnProps`, which no
|
|
5642
|
+
// version of the datagrid has ever read: the grid
|
|
5643
|
+
// fell back to its internal 40px default column and
|
|
5644
|
+
// the custom select-all header below never
|
|
5645
|
+
// rendered.
|
|
5646
|
+
checkboxColumn={
|
|
5647
|
+
tableSetting?.checkboxColumn
|
|
5648
|
+
? {
|
|
5649
|
+
// The ?? 60 is a backstop, not a second
|
|
5650
|
+
// source of truth: if the density table ever
|
|
5651
|
+
// fails to supply a width (a stale-HMR module
|
|
5652
|
+
// mix did exactly that once), the grid would
|
|
5653
|
+
// otherwise fall back to its internal 40px
|
|
5654
|
+
// default and clip the checkbox.
|
|
5655
|
+
width: grid.checkboxColWidth ?? 60,
|
|
5656
|
+
minWidth: grid.checkboxColWidth ?? 60,
|
|
5657
|
+
maxWidth: grid.checkboxColWidth ?? 60,
|
|
5623
5658
|
resizable: false,
|
|
5624
5659
|
sortable: false,
|
|
5625
5660
|
headerAlign: 'center',
|
|
5626
5661
|
textAlign: 'center',
|
|
5627
|
-
renderHeader
|
|
5662
|
+
// `header`, not `renderHeader`: the column
|
|
5663
|
+
// merge is {...default, ...this}, and the
|
|
5664
|
+
// default supplies its select-all through
|
|
5665
|
+
// `header` — this key must shadow that one.
|
|
5666
|
+
header: () => {
|
|
5628
5667
|
// Custom header checkbox that works with grouped data
|
|
5629
5668
|
const allRowsSelected =
|
|
5630
5669
|
dataRef.current &&
|
|
@@ -5649,7 +5688,12 @@ const DataGrid = forwardRef(
|
|
|
5649
5688
|
return (
|
|
5650
5689
|
<input
|
|
5651
5690
|
type="checkbox"
|
|
5652
|
-
|
|
5691
|
+
// Boolean() — the && chain yields
|
|
5692
|
+
// undefined before data arrives,
|
|
5693
|
+
// which React reads as
|
|
5694
|
+
// "uncontrolled" and then warns
|
|
5695
|
+
// when it flips to controlled.
|
|
5696
|
+
checked={Boolean(allRowsSelected)}
|
|
5653
5697
|
ref={(checkbox) => {
|
|
5654
5698
|
if (checkbox) {
|
|
5655
5699
|
checkbox.indeterminate =
|
|
@@ -5695,8 +5739,11 @@ const DataGrid = forwardRef(
|
|
|
5695
5739
|
}
|
|
5696
5740
|
}}
|
|
5697
5741
|
style={{
|
|
5698
|
-
|
|
5699
|
-
|
|
5742
|
+
// rem so it rides the density
|
|
5743
|
+
// root: 20px default, 22.5px
|
|
5744
|
+
// in large.
|
|
5745
|
+
width: '1.25rem',
|
|
5746
|
+
height: '1.25rem',
|
|
5700
5747
|
cursor: 'pointer',
|
|
5701
5748
|
accentColor:
|
|
5702
5749
|
'var(--primary-color, #3b82f6)',
|
|
@@ -5704,7 +5751,9 @@ const DataGrid = forwardRef(
|
|
|
5704
5751
|
/>
|
|
5705
5752
|
);
|
|
5706
5753
|
},
|
|
5707
|
-
}
|
|
5754
|
+
}
|
|
5755
|
+
: undefined
|
|
5756
|
+
}
|
|
5708
5757
|
checkboxOnlyRowSelect={true}
|
|
5709
5758
|
/>
|
|
5710
5759
|
) : null}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
1
|
+
import React, { useState, useEffect, useInsertionEffect } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
useNavigate,
|
|
4
4
|
useLocation,
|
|
@@ -128,6 +128,7 @@ const GenericAuth = ({
|
|
|
128
128
|
enforce2FA = false, // Default to not enforcing 2FA
|
|
129
129
|
clientPortalConfig = null, // Configuration for client portal with component and URLs
|
|
130
130
|
config = {}, // Configuration object for auth features
|
|
131
|
+
density = 'default', // 'default' | 'large' — app-wide display density
|
|
131
132
|
}) => {
|
|
132
133
|
const navigate = useNavigate();
|
|
133
134
|
const location = useLocation();
|
|
@@ -138,6 +139,26 @@ const GenericAuth = ({
|
|
|
138
139
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
|
139
140
|
const [isClientAuthenticated, setIsClientAuthenticated] = useState(false);
|
|
140
141
|
|
|
142
|
+
// Mirror the `density` prop onto <html> as data-density, which is what
|
|
143
|
+
// styles/density.css keys off. 'default' writes nothing, so an app that
|
|
144
|
+
// never passes the prop leaves no attribute and matches no rule.
|
|
145
|
+
// useInsertionEffect fires before child useLayoutEffects, so the root
|
|
146
|
+
// font-size is in place before anything downstream (the DataGrid most of
|
|
147
|
+
// all) measures itself against it.
|
|
148
|
+
useInsertionEffect(() => {
|
|
149
|
+
if (
|
|
150
|
+
typeof document === 'undefined' ||
|
|
151
|
+
!density ||
|
|
152
|
+
density === 'default'
|
|
153
|
+
) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
document.documentElement.setAttribute('data-density', density);
|
|
157
|
+
return () => {
|
|
158
|
+
document.documentElement.removeAttribute('data-density');
|
|
159
|
+
};
|
|
160
|
+
}, [density]);
|
|
161
|
+
|
|
141
162
|
const updateAuthStatus = async () => {
|
|
142
163
|
// The loading curtain covers two moments and stays down for neither
|
|
143
164
|
// more nor less: the first profile check of a page load (no usable
|
|
@@ -390,6 +411,7 @@ const GenericAuth = ({
|
|
|
390
411
|
themeType={themeType}
|
|
391
412
|
userProfile={userProfile}
|
|
392
413
|
enforce2FA={enforce2FA}
|
|
414
|
+
density={density}
|
|
393
415
|
/>
|
|
394
416
|
}
|
|
395
417
|
/>
|
|
@@ -16,6 +16,7 @@ import TableFilter from '../TableFilter';
|
|
|
16
16
|
import GenericGrid from './GenericGrid';
|
|
17
17
|
import GenericReportForm from './GenericReportForm';
|
|
18
18
|
import StandardModal from './StandardModal';
|
|
19
|
+
import { useDensity, useAvailableHeight } from '../utils/useDensity';
|
|
19
20
|
|
|
20
21
|
import styles from '../styles/GenericIndex.module.scss';
|
|
21
22
|
|
|
@@ -302,6 +303,23 @@ function GenericIndex({
|
|
|
302
303
|
const [customActionConfig, setCustomActionConfig] = useState(null);
|
|
303
304
|
const { windowHeight, windowWidth } = useWindowDimensions();
|
|
304
305
|
|
|
306
|
+
// Grid height. The long-standing behaviour is a flat 80% of the viewport,
|
|
307
|
+
// which leaves a dead band below the table; at large density the screen is
|
|
308
|
+
// the point, so measure what is actually left below the grid instead.
|
|
309
|
+
// `useAvailableHeight` returns null until it can measure (and always when
|
|
310
|
+
// disabled), so the 80% rule stays the fallback and remains exactly what
|
|
311
|
+
// every non-density app gets.
|
|
312
|
+
const { isLarge } = useDensity();
|
|
313
|
+
const gridWrapperRef = useRef(null);
|
|
314
|
+
const measuredGridHeight = useAvailableHeight(gridWrapperRef, {
|
|
315
|
+
enabled: isLarge,
|
|
316
|
+
recomputeKey: windowHeight,
|
|
317
|
+
});
|
|
318
|
+
const gridHeight =
|
|
319
|
+
isLarge && measuredGridHeight != null
|
|
320
|
+
? measuredGridHeight
|
|
321
|
+
: windowHeight * 0.8;
|
|
322
|
+
|
|
305
323
|
// Toggle a body-level "tablet-mode" class when this view opts in via
|
|
306
324
|
// setting.tabletMode. Lets consumer projects style the whole UI
|
|
307
325
|
// (including app-level chrome) for tablet use without touching this lib.
|
|
@@ -663,12 +681,12 @@ function GenericIndex({
|
|
|
663
681
|
style: config.style || userProfile?.settings?.style || {},
|
|
664
682
|
};
|
|
665
683
|
|
|
666
|
-
|
|
684
|
+
const table = (
|
|
667
685
|
<Table
|
|
668
686
|
{...configWithStyle}
|
|
669
687
|
ref={gridRef}
|
|
670
688
|
hoverColor={configWithStyle.style?.hoverColor}
|
|
671
|
-
gridHeight={
|
|
689
|
+
gridHeight={gridHeight}
|
|
672
690
|
setConfig={setConfig}
|
|
673
691
|
setTableData={setTableData}
|
|
674
692
|
setTotal={setTotal}
|
|
@@ -677,6 +695,19 @@ function GenericIndex({
|
|
|
677
695
|
userProfile={userProfile}
|
|
678
696
|
/>
|
|
679
697
|
);
|
|
698
|
+
|
|
699
|
+
// The measuring wrapper only exists in large density — every other
|
|
700
|
+
// app renders the exact tree it always has.
|
|
701
|
+
return isLarge ? (
|
|
702
|
+
<div
|
|
703
|
+
ref={gridWrapperRef}
|
|
704
|
+
style={{ flex: '1 1 auto', minHeight: 0 }}
|
|
705
|
+
>
|
|
706
|
+
{table}
|
|
707
|
+
</div>
|
|
708
|
+
) : (
|
|
709
|
+
table
|
|
710
|
+
);
|
|
680
711
|
};
|
|
681
712
|
|
|
682
713
|
if (subnav) {
|
|
@@ -693,7 +724,8 @@ function GenericIndex({
|
|
|
693
724
|
}, [
|
|
694
725
|
config,
|
|
695
726
|
subnav,
|
|
696
|
-
|
|
727
|
+
gridHeight,
|
|
728
|
+
isLarge,
|
|
697
729
|
userProfile,
|
|
698
730
|
setConfig,
|
|
699
731
|
setTotal,
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// Density first: its `:root[data-density='large']` block is inert until the
|
|
2
|
+
// app opts in via GenericAuth's `density` prop, but the root font-size it
|
|
3
|
+
// sets is what every rem in the stylesheets below resolves against.
|
|
4
|
+
import '../styles/density.css';
|
|
1
5
|
import '../styles/global.css';
|
|
2
6
|
|
|
3
7
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
@@ -27,6 +31,10 @@ const GenericMain = ({
|
|
|
27
31
|
themeType,
|
|
28
32
|
userProfile,
|
|
29
33
|
enforce2FA = false, // Default to not enforcing 2FA
|
|
34
|
+
// Accepted so density is part of the main-layout interface too; the
|
|
35
|
+
// attribute that actually drives the CSS is written by GenericAuth, and
|
|
36
|
+
// nothing in here reads the value yet.
|
|
37
|
+
density = 'default',
|
|
30
38
|
}) => {
|
|
31
39
|
const navigate = useNavigate();
|
|
32
40
|
const location = useLocation();
|
|
@@ -690,7 +690,7 @@
|
|
|
690
690
|
}
|
|
691
691
|
|
|
692
692
|
.searchInput {
|
|
693
|
-
height: 38px;
|
|
693
|
+
height: var(--grid-toolbar-height, 38px);
|
|
694
694
|
border-radius: 0;
|
|
695
695
|
border: none;
|
|
696
696
|
padding: 0 10px 0 50px;
|
|
@@ -722,7 +722,7 @@
|
|
|
722
722
|
display: inline-flex;
|
|
723
723
|
align-items: center;
|
|
724
724
|
padding: 0 15px;
|
|
725
|
-
height: 38px;
|
|
725
|
+
height: var(--grid-toolbar-height, 38px);
|
|
726
726
|
background-color: #f9fafb;
|
|
727
727
|
border: 1px solid #d1d5db;
|
|
728
728
|
margin-bottom: 0;
|
|
@@ -734,7 +734,7 @@
|
|
|
734
734
|
display: inline-flex;
|
|
735
735
|
align-items: center;
|
|
736
736
|
padding: 0 15px;
|
|
737
|
-
height: 38px;
|
|
737
|
+
height: var(--grid-toolbar-height, 38px);
|
|
738
738
|
background-color: #f9fafb;
|
|
739
739
|
border: 1px solid #d1d5db;
|
|
740
740
|
border-radius: var(--br, 4px);
|
|
@@ -771,11 +771,11 @@
|
|
|
771
771
|
.actionButtons {
|
|
772
772
|
display: flex;
|
|
773
773
|
align-items: center;
|
|
774
|
-
height: 38px;
|
|
774
|
+
height: var(--grid-toolbar-height, 38px);
|
|
775
775
|
gap: 0;
|
|
776
776
|
|
|
777
777
|
> .btn {
|
|
778
|
-
height: 38px;
|
|
778
|
+
height: var(--grid-toolbar-height, 38px);
|
|
779
779
|
border-radius: 0;
|
|
780
780
|
display: flex;
|
|
781
781
|
align-items: center;
|
|
@@ -799,11 +799,11 @@
|
|
|
799
799
|
.actionButtonsStandalone {
|
|
800
800
|
display: flex;
|
|
801
801
|
align-items: center;
|
|
802
|
-
height: 38px;
|
|
802
|
+
height: var(--grid-toolbar-height, 38px);
|
|
803
803
|
gap: 0;
|
|
804
804
|
|
|
805
805
|
> .btn {
|
|
806
|
-
height: 38px;
|
|
806
|
+
height: var(--grid-toolbar-height, 38px);
|
|
807
807
|
display: flex;
|
|
808
808
|
align-items: center;
|
|
809
809
|
justify-content: center;
|
|
@@ -884,7 +884,7 @@
|
|
|
884
884
|
justify-content: flex-start;
|
|
885
885
|
margin-bottom: 0;
|
|
886
886
|
width: 100%;
|
|
887
|
-
height: 38px;
|
|
887
|
+
height: var(--grid-toolbar-height, 38px);
|
|
888
888
|
position: relative;
|
|
889
889
|
border: 1px solid #d1d5db;
|
|
890
890
|
border-bottom: none;
|
|
@@ -906,7 +906,7 @@
|
|
|
906
906
|
}
|
|
907
907
|
|
|
908
908
|
input {
|
|
909
|
-
height: 38px;
|
|
909
|
+
height: var(--grid-toolbar-height, 38px);
|
|
910
910
|
border-radius: 0;
|
|
911
911
|
border: none;
|
|
912
912
|
padding: 0 10px 0 50px;
|
|
@@ -930,14 +930,14 @@
|
|
|
930
930
|
|
|
931
931
|
.filterAction--alt {
|
|
932
932
|
position: relative;
|
|
933
|
-
height: 38px;
|
|
933
|
+
height: var(--grid-toolbar-height, 38px);
|
|
934
934
|
z-index: 5;
|
|
935
935
|
display: flex;
|
|
936
936
|
gap: 0;
|
|
937
937
|
/* margin-left is now controlled in the component */
|
|
938
938
|
|
|
939
939
|
> .btn {
|
|
940
|
-
height: 38px;
|
|
940
|
+
height: var(--grid-toolbar-height, 38px);
|
|
941
941
|
border-radius: 0;
|
|
942
942
|
display: flex;
|
|
943
943
|
align-items: center;
|
|
@@ -960,7 +960,7 @@ input[type='file']:hover {
|
|
|
960
960
|
border-radius: var(--br, 6px);
|
|
961
961
|
font-size: 0.8rem;
|
|
962
962
|
transition: all 0.2s ease;
|
|
963
|
-
min-height: 36px;
|
|
963
|
+
min-height: var(--field-pocket-size, 36px);
|
|
964
964
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
|
965
965
|
position: relative;
|
|
966
966
|
overflow: hidden;
|
|
@@ -997,8 +997,8 @@ input[type='file']:hover {
|
|
|
997
997
|
padding: 0.35em;
|
|
998
998
|
border-radius: 4px;
|
|
999
999
|
transition: all 0.2s ease;
|
|
1000
|
-
min-width: 28px;
|
|
1001
|
-
height: 28px;
|
|
1000
|
+
min-width: var(--field-pocket-size-sm, 28px);
|
|
1001
|
+
height: var(--field-pocket-size-sm, 28px);
|
|
1002
1002
|
flex-shrink: 0;
|
|
1003
1003
|
}
|
|
1004
1004
|
|
|
@@ -1151,7 +1151,7 @@ input[type='file']:hover {
|
|
|
1151
1151
|
transition: all 0.2s ease;
|
|
1152
1152
|
margin-bottom: 0.25em;
|
|
1153
1153
|
border: 1px solid transparent;
|
|
1154
|
-
min-height: 36px;
|
|
1154
|
+
min-height: var(--field-pocket-size, 36px);
|
|
1155
1155
|
}
|
|
1156
1156
|
|
|
1157
1157
|
.file-list-item-image {
|
|
@@ -1370,7 +1370,7 @@ input[type='file']:hover {
|
|
|
1370
1370
|
z-index: 999; /* Match react-select z-index */
|
|
1371
1371
|
max-height: 200px;
|
|
1372
1372
|
overflow-y: auto;
|
|
1373
|
-
font-size: 14px; /* Match react-select font size */
|
|
1373
|
+
font-size: var(--field-suggestion-font-size, 14px); /* Match react-select font size */
|
|
1374
1374
|
}
|
|
1375
1375
|
|
|
1376
1376
|
.emailSuggestion {
|
|
@@ -260,7 +260,7 @@
|
|
|
260
260
|
close under its heading and the whole block is separated from the
|
|
261
261
|
content below rather than floating between the two. */
|
|
262
262
|
margin: calc(-1 * var(--spacing-sm, 0.5rem)) 0 var(--spacing-lg, 1.5rem);
|
|
263
|
-
max-width: 70ch;
|
|
263
|
+
max-width: var(--detail-prose-width, 70ch);
|
|
264
264
|
font-size: var(--font-size-sm, 0.8125rem);
|
|
265
265
|
line-height: 1.55;
|
|
266
266
|
color: var(--muted-color, #6e7276);
|
|
@@ -765,6 +765,9 @@
|
|
|
765
765
|
display: flex;
|
|
766
766
|
align-items: center;
|
|
767
767
|
min-width: 0;
|
|
768
|
+
/* Flex drops whitespace-only text nodes — same fix as the index
|
|
769
|
+
header: one word-space back between title fragments. */
|
|
770
|
+
column-gap: 0.3em;
|
|
768
771
|
}
|
|
769
772
|
|
|
770
773
|
/* The parent crumb. Same size and baseline as the entity it leads to, so
|
|
@@ -877,7 +880,7 @@
|
|
|
877
880
|
background: #f9f9f9; /* Subtle background to match the theme */
|
|
878
881
|
border: 1px solid #ddd; /* Light border for structure */
|
|
879
882
|
border-radius: 6px; /* Slight rounding for a modern feel */
|
|
880
|
-
max-width: 600px; /* Constrain width for a balanced layout */
|
|
883
|
+
max-width: var(--detail-panel-max-width, 600px); /* Constrain width for a balanced layout */
|
|
881
884
|
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.1); /* Gentle shadow for depth */
|
|
882
885
|
font-style: italic; /* Optional: Add a subtle font style */
|
|
883
886
|
}
|
|
@@ -1078,7 +1081,7 @@
|
|
|
1078
1081
|
|
|
1079
1082
|
.modalWide {
|
|
1080
1083
|
width: 90vw;
|
|
1081
|
-
max-width: 1400px;
|
|
1084
|
+
max-width: var(--detail-max-width, 1400px);
|
|
1082
1085
|
min-width: 600px;
|
|
1083
1086
|
}
|
|
1084
1087
|
|
|
@@ -501,7 +501,9 @@
|
|
|
501
501
|
background-color: transparent;
|
|
502
502
|
color: var(--paragraph-color, #212529);
|
|
503
503
|
border: 1px solid rgba(var(--paragraph-color-rgb, 33, 37, 41), 0.2);
|
|
504
|
-
height
|
|
504
|
+
/* Fixed height, so the density hook has to reach it directly — this is
|
|
505
|
+
the one filter-bar control button-base doesn't size. */
|
|
506
|
+
height: var(--control-height-sm, 30px);
|
|
505
507
|
|
|
506
508
|
svg {
|
|
507
509
|
margin-right: 4px;
|
|
@@ -818,6 +820,11 @@
|
|
|
818
820
|
display: flex;
|
|
819
821
|
align-items: center;
|
|
820
822
|
min-width: 0;
|
|
823
|
+
/* Flex drops whitespace-only text nodes, so a title written as
|
|
824
|
+
`Forklift <span>(warning)</span>` rendered with the two parts
|
|
825
|
+
jammed together. One word-space back between fragments; a title
|
|
826
|
+
that is a single text node has one flex item and no gap. */
|
|
827
|
+
column-gap: 0.3em;
|
|
821
828
|
}
|
|
822
829
|
|
|
823
830
|
/* A breadcrumb crumb: still a link, but it should not compete with the
|
|
@@ -1177,7 +1184,7 @@
|
|
|
1177
1184
|
|
|
1178
1185
|
.modalWide {
|
|
1179
1186
|
width: 90vw;
|
|
1180
|
-
max-width: 1400px;
|
|
1187
|
+
max-width: var(--detail-max-width, 1400px);
|
|
1181
1188
|
min-width: 600px;
|
|
1182
1189
|
}
|
|
1183
1190
|
|