@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
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.0",
|
|
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,
|
|
@@ -923,31 +924,19 @@ const DataGrid = forwardRef(
|
|
|
923
924
|
};
|
|
924
925
|
}, []);
|
|
925
926
|
|
|
926
|
-
//
|
|
927
|
-
//
|
|
927
|
+
// The px geometry this grid lays out with — row heights, the width one
|
|
928
|
+
// action icon needs, the floor under a column — comes from the density
|
|
929
|
+
// table rather than from constants here, so app density (data-density
|
|
930
|
+
// on <html>) and per-view tablet mode (body.tablet-mode) are decided in
|
|
931
|
+
// one place. `grid` is a stable reference out of DENSITY_GRID, so it is
|
|
932
|
+
// safe in the dependency arrays below.
|
|
933
|
+
//
|
|
934
|
+
// Why a fixed rowHeight matters in the roomier modes: with
|
|
935
|
+
// rowHeight={null} Inovua measures each row's content, which is
|
|
928
936
|
// 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
|
-
}, []);
|
|
937
|
+
// rAF/timers). A fixed height bypasses measurement entirely and
|
|
938
|
+
// prevents action buttons from being clipped.
|
|
939
|
+
const { grid } = useDensity();
|
|
951
940
|
|
|
952
941
|
// Effect to handle autoRefresh setting
|
|
953
942
|
useEffect(() => {
|
|
@@ -3613,14 +3602,11 @@ const DataGrid = forwardRef(
|
|
|
3613
3602
|
.filter((entry) => !isRowActionConfig(entry))
|
|
3614
3603
|
);
|
|
3615
3604
|
|
|
3616
|
-
// Width to fit N action icons:
|
|
3617
|
-
// tap target/margin
|
|
3605
|
+
// Width to fit N action icons: roomier densities give icons a
|
|
3606
|
+
// larger tap target/margin than the default, plus cell
|
|
3618
3607
|
// padding. Used by both action columns and the trailing column.
|
|
3619
3608
|
const actionColumnWidthFor = (count) =>
|
|
3620
|
-
Math.max(
|
|
3621
|
-
100,
|
|
3622
|
-
count * (isTabletMode ? 44 : 34) + 16
|
|
3623
|
-
);
|
|
3609
|
+
Math.max(100, count * grid.actionColUnit + 16);
|
|
3624
3610
|
|
|
3625
3611
|
const renderColumn = (column) => {
|
|
3626
3612
|
let childValue;
|
|
@@ -3652,7 +3638,7 @@ const DataGrid = forwardRef(
|
|
|
3652
3638
|
// clamped columns exceed the container (narrow
|
|
3653
3639
|
// screens) — columns then render a few px wide
|
|
3654
3640
|
// with no horizontal scrollbar ever appearing.
|
|
3655
|
-
minWidth: column.minWidth ??
|
|
3641
|
+
minWidth: column.minWidth ?? grid.columnMinWidth,
|
|
3656
3642
|
maxWidth: column.maxWidth ?? undefined,
|
|
3657
3643
|
textAlign: column.textAlign ?? undefined,
|
|
3658
3644
|
textVerticalAlign:
|
|
@@ -3822,9 +3808,7 @@ const DataGrid = forwardRef(
|
|
|
3822
3808
|
// what the icons themselves need — tap targets
|
|
3823
3809
|
// stay intact in tablet mode.
|
|
3824
3810
|
const iconsNeed =
|
|
3825
|
-
actionCount *
|
|
3826
|
-
(isTabletMode ? 44 : 34) +
|
|
3827
|
-
16;
|
|
3811
|
+
actionCount * grid.actionColUnit + 16;
|
|
3828
3812
|
const width =
|
|
3829
3813
|
column.maxWidth != null
|
|
3830
3814
|
? Math.max(
|
|
@@ -4757,7 +4741,7 @@ const DataGrid = forwardRef(
|
|
|
4757
4741
|
.catch((error) => {
|
|
4758
4742
|
console.error('Error in fetching dropdown data: ', error);
|
|
4759
4743
|
});
|
|
4760
|
-
}, [columns, filterDataSource, memoizedSettings,
|
|
4744
|
+
}, [columns, filterDataSource, memoizedSettings, grid]);
|
|
4761
4745
|
|
|
4762
4746
|
// Force re-render when filterDataSource updates to ensure dropdown filters get their data
|
|
4763
4747
|
const [renderKey, setRenderKey] = useState(0);
|
|
@@ -4886,13 +4870,16 @@ const DataGrid = forwardRef(
|
|
|
4886
4870
|
} else if (ajaxSetting?.take >= 35) {
|
|
4887
4871
|
setLimit(ajaxSetting.take);
|
|
4888
4872
|
} else {
|
|
4873
|
+
// Divide by the row height actually in force. This was
|
|
4874
|
+
// hardcoded to 32 even in tablet mode, so tablet pages asked
|
|
4875
|
+
// for far more rows than could ever fit on screen.
|
|
4889
4876
|
const calculateRows = (height) =>
|
|
4890
|
-
Math.floor((height - 100) /
|
|
4877
|
+
Math.floor((height - 100) / grid.rowUnit);
|
|
4891
4878
|
|
|
4892
4879
|
const rowCount = calculateRows(minHeight);
|
|
4893
4880
|
setLimit(rowCount);
|
|
4894
4881
|
}
|
|
4895
|
-
}, [dataCount, gridHeight, window.innerHeight, userProfile?.rows_per_page, userProfile?.settings?.rows_per_page]);
|
|
4882
|
+
}, [dataCount, gridHeight, window.innerHeight, grid, userProfile?.rows_per_page, userProfile?.settings?.rows_per_page]);
|
|
4896
4883
|
|
|
4897
4884
|
useEffect(() => {
|
|
4898
4885
|
const debouncedHandleResize = debounce(() => {
|
|
@@ -5578,7 +5565,7 @@ const DataGrid = forwardRef(
|
|
|
5578
5565
|
}
|
|
5579
5566
|
headerProps={headerProps}
|
|
5580
5567
|
idProperty="id"
|
|
5581
|
-
minRowHeight={
|
|
5568
|
+
minRowHeight={grid.minRowHeight}
|
|
5582
5569
|
onFilterValueChange={(fv) => {
|
|
5583
5570
|
handleFilterChange(fv, filterValue);
|
|
5584
5571
|
}}
|
|
@@ -5590,7 +5577,12 @@ const DataGrid = forwardRef(
|
|
|
5590
5577
|
renderRowContextMenu={renderRowContextMenu}
|
|
5591
5578
|
renderColumnContextMenu={renderColumnContextMenu}
|
|
5592
5579
|
rowClassName={rowClassName}
|
|
5593
|
-
|
|
5580
|
+
// null lets the grid measure each row; a number
|
|
5581
|
+
// fixes the height, which the roomier densities
|
|
5582
|
+
// need — measurement is unreliable in a
|
|
5583
|
+
// background-throttled tab and clips action
|
|
5584
|
+
// buttons. See the density table in useDensity.
|
|
5585
|
+
rowHeight={grid.rowHeight}
|
|
5594
5586
|
rowStyle={getRowStyle}
|
|
5595
5587
|
selected={selected}
|
|
5596
5588
|
showZebraRows={true}
|
|
@@ -5616,15 +5608,36 @@ const DataGrid = forwardRef(
|
|
|
5616
5608
|
? 'InovuaReactDataGrid--grouped datagrid-with-grouping datagrid-fixed-group-headers'
|
|
5617
5609
|
: 'datagrid-without-grouping'
|
|
5618
5610
|
}
|
|
5619
|
-
|
|
5620
|
-
|
|
5621
|
-
|
|
5622
|
-
|
|
5611
|
+
// The real prop is `checkboxColumn` (bool | column
|
|
5612
|
+
// object) — {...tableSetting} spreads the view's
|
|
5613
|
+
// `checkboxColumn: true` and this object, placed
|
|
5614
|
+
// after the spread, replaces it. It previously sat
|
|
5615
|
+
// on a prop named `checkboxColumnProps`, which no
|
|
5616
|
+
// version of the datagrid has ever read: the grid
|
|
5617
|
+
// fell back to its internal 40px default column and
|
|
5618
|
+
// the custom select-all header below never
|
|
5619
|
+
// rendered.
|
|
5620
|
+
checkboxColumn={
|
|
5621
|
+
tableSetting?.checkboxColumn
|
|
5622
|
+
? {
|
|
5623
|
+
// The ?? 60 is a backstop, not a second
|
|
5624
|
+
// source of truth: if the density table ever
|
|
5625
|
+
// fails to supply a width (a stale-HMR module
|
|
5626
|
+
// mix did exactly that once), the grid would
|
|
5627
|
+
// otherwise fall back to its internal 40px
|
|
5628
|
+
// default and clip the checkbox.
|
|
5629
|
+
width: grid.checkboxColWidth ?? 60,
|
|
5630
|
+
minWidth: grid.checkboxColWidth ?? 60,
|
|
5631
|
+
maxWidth: grid.checkboxColWidth ?? 60,
|
|
5623
5632
|
resizable: false,
|
|
5624
5633
|
sortable: false,
|
|
5625
5634
|
headerAlign: 'center',
|
|
5626
5635
|
textAlign: 'center',
|
|
5627
|
-
renderHeader
|
|
5636
|
+
// `header`, not `renderHeader`: the column
|
|
5637
|
+
// merge is {...default, ...this}, and the
|
|
5638
|
+
// default supplies its select-all through
|
|
5639
|
+
// `header` — this key must shadow that one.
|
|
5640
|
+
header: () => {
|
|
5628
5641
|
// Custom header checkbox that works with grouped data
|
|
5629
5642
|
const allRowsSelected =
|
|
5630
5643
|
dataRef.current &&
|
|
@@ -5649,7 +5662,12 @@ const DataGrid = forwardRef(
|
|
|
5649
5662
|
return (
|
|
5650
5663
|
<input
|
|
5651
5664
|
type="checkbox"
|
|
5652
|
-
|
|
5665
|
+
// Boolean() — the && chain yields
|
|
5666
|
+
// undefined before data arrives,
|
|
5667
|
+
// which React reads as
|
|
5668
|
+
// "uncontrolled" and then warns
|
|
5669
|
+
// when it flips to controlled.
|
|
5670
|
+
checked={Boolean(allRowsSelected)}
|
|
5653
5671
|
ref={(checkbox) => {
|
|
5654
5672
|
if (checkbox) {
|
|
5655
5673
|
checkbox.indeterminate =
|
|
@@ -5695,8 +5713,11 @@ const DataGrid = forwardRef(
|
|
|
5695
5713
|
}
|
|
5696
5714
|
}}
|
|
5697
5715
|
style={{
|
|
5698
|
-
|
|
5699
|
-
|
|
5716
|
+
// rem so it rides the density
|
|
5717
|
+
// root: 20px default, 22.5px
|
|
5718
|
+
// in large.
|
|
5719
|
+
width: '1.25rem',
|
|
5720
|
+
height: '1.25rem',
|
|
5700
5721
|
cursor: 'pointer',
|
|
5701
5722
|
accentColor:
|
|
5702
5723
|
'var(--primary-color, #3b82f6)',
|
|
@@ -5704,7 +5725,9 @@ const DataGrid = forwardRef(
|
|
|
5704
5725
|
/>
|
|
5705
5726
|
);
|
|
5706
5727
|
},
|
|
5707
|
-
}
|
|
5728
|
+
}
|
|
5729
|
+
: undefined
|
|
5730
|
+
}
|
|
5708
5731
|
checkboxOnlyRowSelect={true}
|
|
5709
5732
|
/>
|
|
5710
5733
|
) : 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
|
|
|
@@ -1,3 +1,56 @@
|
|
|
1
|
+
/* ============================================================================
|
|
2
|
+
Density hooks (page, controls, grid, fields, detail)
|
|
3
|
+
----------------------------------------------------------------------------
|
|
4
|
+
The second half of the set documented at the top of Navigation.module.scss,
|
|
5
|
+
written the same way: every declaration carrying one of these reads
|
|
6
|
+
`var(--hook, <the value it always had>)`, so a project that sets none of them
|
|
7
|
+
renders byte-identically to before. They exist for the density system — an
|
|
8
|
+
app overrides them on `:root` in its own global stylesheet to tighten (or
|
|
9
|
+
loosen) the whole UI without forking a component.
|
|
10
|
+
|
|
11
|
+
Page shell — this file
|
|
12
|
+
--page-gutter-y vertical padding of the content gutter
|
|
13
|
+
(`.padding` / `.paddingSimple`).
|
|
14
|
+
default: var(--spacing-lg)
|
|
15
|
+
--page-gutter-x horizontal padding of the same.
|
|
16
|
+
default: clamp(var(--spacing-md), 3vw, var(--spacing-xl))
|
|
17
|
+
--page-gutter-narrow the flat gutter used below 960px, both axes.
|
|
18
|
+
default: var(--spacing-md)
|
|
19
|
+
--page-max-width cap on the content column's width. default: 2200px
|
|
20
|
+
--content-header-height the page's own content-header band. default: 52.5px
|
|
21
|
+
|
|
22
|
+
Controls — _controls.scss
|
|
23
|
+
--control-height min-height of every button (`button-base`) and
|
|
24
|
+
input (`input-base`). default: 2.5rem
|
|
25
|
+
--control-height-sm min-height of the small button variant.
|
|
26
|
+
default: 2rem
|
|
27
|
+
--input-padding-x horizontal padding inside an input. default: 0.65rem
|
|
28
|
+
|
|
29
|
+
Data grid — DataGrid.module.scss, global-datagrid.css
|
|
30
|
+
--grid-toolbar-height height of the toolbar / search strip and the
|
|
31
|
+
buttons sitting in it (11 declarations).
|
|
32
|
+
default: 38px
|
|
33
|
+
--grid-cell-padding padding inside a grid body cell. default: 6px 8px
|
|
34
|
+
--grid-checkbox-col-width width of the row-select checkbox column, held
|
|
35
|
+
in step with checkboxColWidth in
|
|
36
|
+
utils/useDensity.js. default: 60px
|
|
37
|
+
|
|
38
|
+
Fields — Field.module.scss
|
|
39
|
+
--field-pocket-size min-height of a file-list row. default: 36px
|
|
40
|
+
--field-pocket-size-sm the square trash chip on that row. default: 28px
|
|
41
|
+
--field-suggestion-font-size type size of the email-suggestion list
|
|
42
|
+
(kept in step with react-select). default: 14px
|
|
43
|
+
|
|
44
|
+
Detail / index — GenericDetail.module.scss, GenericIndex.module.scss
|
|
45
|
+
--detail-max-width cap on the wide modal, shared by both files so the
|
|
46
|
+
two surfaces stay the same width. default: 1400px
|
|
47
|
+
--detail-prose-width measure of a description paragraph. default: 70ch
|
|
48
|
+
--detail-panel-max-width cap on the centred empty-state panel. default: 600px
|
|
49
|
+
|
|
50
|
+
Nav-bar hooks (`--nav-*`, including `--nav-logo-height`) are documented at
|
|
51
|
+
the top of Navigation.module.scss and are not repeated here.
|
|
52
|
+
========================================================================= */
|
|
53
|
+
|
|
1
54
|
.header {
|
|
2
55
|
background: var(--primary-color);
|
|
3
56
|
color: var(--primary-color);
|
|
@@ -36,7 +89,7 @@
|
|
|
36
89
|
border-bottom: 1px solid rgba(0, 0, 0, 0.14);
|
|
37
90
|
box-sizing: border-box;
|
|
38
91
|
padding: 1em;
|
|
39
|
-
height: 52.5px;
|
|
92
|
+
height: var(--content-header-height, 52.5px);
|
|
40
93
|
box-shadow: 1px 0px 0px rgba(255, 255, 255, 0.5);
|
|
41
94
|
display: flex;
|
|
42
95
|
flex-wrap: nowrap;
|
|
@@ -94,13 +147,13 @@
|
|
|
94
147
|
and on a phone gave the same gutter as a desktop. */
|
|
95
148
|
.padding,
|
|
96
149
|
.paddingSimple {
|
|
97
|
-
padding: var(--spacing-lg) clamp(var(--spacing-md), 3vw, var(--spacing-xl));
|
|
150
|
+
padding: var(--page-gutter-y, var(--spacing-lg)) var(--page-gutter-x, clamp(var(--spacing-md), 3vw, var(--spacing-xl)));
|
|
98
151
|
/* Long lines of data are hard to track across an ultrawide screen. This
|
|
99
152
|
is generous enough never to bite on a laptop. */
|
|
100
|
-
max-width: 2200px;
|
|
153
|
+
max-width: var(--page-max-width, 2200px);
|
|
101
154
|
margin: 0 auto;
|
|
102
155
|
|
|
103
156
|
@media (max-width: 960px) {
|
|
104
|
-
padding: var(--spacing-md) var(--spacing-md);
|
|
157
|
+
padding: var(--page-gutter-narrow, var(--spacing-md)) var(--page-gutter-narrow, var(--spacing-md));
|
|
105
158
|
}
|
|
106
159
|
}
|