@visns-studio/visns-components 6.24.0 → 6.24.2
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/AsyncSelect.jsx +1 -1
- package/src/components/MultiSelect.jsx +1 -1
- package/src/components/Navigation.jsx +170 -34
- package/src/components/columns/ColumnRenderers.jsx +40 -24
- package/src/components/columns/SelectFilterEditor.jsx +79 -0
- package/src/components/generic/GenericAuth.jsx +9 -0
- package/src/components/generic/GenericDashboard.jsx +3 -1
- package/src/components/generic/GenericMain.jsx +7 -0
- package/src/components/navBadges.js +152 -0
- package/src/components/styles/Field.module.scss +46 -7
- package/src/components/styles/MultiSelect.module.scss +2 -2
- package/src/components/styles/Navigation.module.scss +100 -3
- package/src/components/styles/global-datagrid.css +176 -3
- package/src/components/styles/global.css +64 -31
package/package.json
CHANGED
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
|
94
94
|
},
|
|
95
95
|
"name": "@visns-studio/visns-components",
|
|
96
|
-
"version": "6.24.
|
|
96
|
+
"version": "6.24.2",
|
|
97
97
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
98
98
|
"main": "src/index.js",
|
|
99
99
|
"files": [
|
|
@@ -223,7 +223,7 @@ function VisnsAsyncSelect({
|
|
|
223
223
|
minHeight:
|
|
224
224
|
style && style.multi_select_height
|
|
225
225
|
? style.multi_select_height
|
|
226
|
-
: 'var(--field-height,
|
|
226
|
+
: 'var(--field-height, 50px)',
|
|
227
227
|
height: 'auto', // Allow height to grow with content
|
|
228
228
|
}),
|
|
229
229
|
valueContainer: (base) => ({
|
|
@@ -350,7 +350,7 @@ function MultiSelect({
|
|
|
350
350
|
? style.multi_select_height
|
|
351
351
|
: isTouchDevice()
|
|
352
352
|
? '44px'
|
|
353
|
-
: 'var(--field-height,
|
|
353
|
+
: 'var(--field-height, 50px)', // one token, same default as every native control
|
|
354
354
|
height: 'auto', // Allow height to grow with content
|
|
355
355
|
}),
|
|
356
356
|
valueContainer: (base) => ({
|
|
@@ -26,12 +26,14 @@ import {
|
|
|
26
26
|
Lock,
|
|
27
27
|
MessageSquare,
|
|
28
28
|
MessagesSquare,
|
|
29
|
+
TrendingUp,
|
|
29
30
|
} from 'lucide-react';
|
|
30
31
|
import CustomFetch from './Fetch';
|
|
31
32
|
import Notification from './Notification';
|
|
32
33
|
import SwitchAccount from './SwitchAccount';
|
|
33
34
|
import BusinessCardOcr from './BusinessCardOcr';
|
|
34
35
|
import { isNavActive, bestMatchingUrl, cleanUrl } from './navActive';
|
|
36
|
+
import { navBadge, badgeFor, badgeAriaLabel } from './navBadges';
|
|
35
37
|
import styles from './styles/Navigation.module.scss';
|
|
36
38
|
|
|
37
39
|
// Opt-in icons for the sidebar (simple/cms) layouts: a nav entry names one
|
|
@@ -51,6 +53,10 @@ const NAV_ICONS = {
|
|
|
51
53
|
phone: Phone,
|
|
52
54
|
reports: BarChart3,
|
|
53
55
|
safety: Shield,
|
|
56
|
+
// A pipeline group — deals, subscriptions, agreements. `billing` is the
|
|
57
|
+
// card and means "money already owed"; this one is money still being
|
|
58
|
+
// chased, which is a different heading in every nav that has both.
|
|
59
|
+
sales: TrendingUp,
|
|
54
60
|
settings: Settings,
|
|
55
61
|
sites: MapPin,
|
|
56
62
|
users: Users,
|
|
@@ -117,6 +123,24 @@ const actionLabel = (n) => {
|
|
|
117
123
|
.replace(/^./, (c) => c.toUpperCase());
|
|
118
124
|
};
|
|
119
125
|
|
|
126
|
+
/**
|
|
127
|
+
* The count pill on a navigation item.
|
|
128
|
+
*
|
|
129
|
+
* `aria-hidden` on purpose: the number is already in the item's accessible
|
|
130
|
+
* name (see badgeAriaLabel), and announcing it twice — once as the label,
|
|
131
|
+
* once as a stray "4" after it — is exactly the double-reading that makes
|
|
132
|
+
* badged navigation unpleasant with a screen reader on.
|
|
133
|
+
*
|
|
134
|
+
* Renders nothing without a badge, so every caller can hand it whatever
|
|
135
|
+
* navBadge() returned without guarding first.
|
|
136
|
+
*/
|
|
137
|
+
const BadgePill = ({ badge }) =>
|
|
138
|
+
badge ? (
|
|
139
|
+
<span className={styles['nav-badge']} aria-hidden="true">
|
|
140
|
+
{badge.display}
|
|
141
|
+
</span>
|
|
142
|
+
) : null;
|
|
143
|
+
|
|
120
144
|
const SearchComponent = ({
|
|
121
145
|
navigate,
|
|
122
146
|
search,
|
|
@@ -299,6 +323,20 @@ function Navigation({
|
|
|
299
323
|
* does not pass it.
|
|
300
324
|
*/
|
|
301
325
|
renderers = {},
|
|
326
|
+
/*
|
|
327
|
+
* Live counts for navigation items: `{ [navId]: number }`, or the object
|
|
328
|
+
* form `{ [navId]: { count, label } }` when the accessible name needs a
|
|
329
|
+
* noun other than "open". See navBadges.js, which owns every rule about
|
|
330
|
+
* what renders and what does not.
|
|
331
|
+
*
|
|
332
|
+
* Empty by default and additive: an id with no entry, a zero, or a value
|
|
333
|
+
* that will not parse draws nothing at all, so a consumer that publishes
|
|
334
|
+
* nothing gets exactly the bar it had before. The map is READ here and
|
|
335
|
+
* never fetched — where the numbers come from, how often they refresh and
|
|
336
|
+
* whether this user is allowed to see them are the app's business, and a
|
|
337
|
+
* shared bar has no way to know any of it.
|
|
338
|
+
*/
|
|
339
|
+
badges = {},
|
|
302
340
|
}) {
|
|
303
341
|
const [search, setSearch] = useState('');
|
|
304
342
|
const [searchResultShow, setSearchResultShow] = useState(false);
|
|
@@ -429,6 +467,13 @@ function Navigation({
|
|
|
429
467
|
const IconComponent = iconComponents[n.icon];
|
|
430
468
|
const label = actionLabel(n);
|
|
431
469
|
|
|
470
|
+
// The same badge contract the nav items honour, keyed by the
|
|
471
|
+
// setting's id — a workload count reads best in this chip row,
|
|
472
|
+
// beside the bell that already wears one. The pill stays
|
|
473
|
+
// aria-hidden; the count joins the accessible name instead,
|
|
474
|
+
// exactly as on a nav item (see BadgePill).
|
|
475
|
+
const settingBadge = badgeFor(badges, n.id);
|
|
476
|
+
|
|
432
477
|
return (
|
|
433
478
|
<li>
|
|
434
479
|
<Link
|
|
@@ -436,13 +481,14 @@ function Navigation({
|
|
|
436
481
|
data-tooltip-id="system-tooltip"
|
|
437
482
|
data-tooltip-place="bottom"
|
|
438
483
|
data-tooltip-content={label}
|
|
439
|
-
aria-label={label}
|
|
484
|
+
aria-label={badgeAriaLabel(label, settingBadge)}
|
|
440
485
|
className={`${activeClass}`}
|
|
441
486
|
target={n.target ? n.target : '_self'}
|
|
442
487
|
>
|
|
443
488
|
{IconComponent && (
|
|
444
489
|
<IconComponent size={19} strokeWidth={2} />
|
|
445
490
|
)}
|
|
491
|
+
<BadgePill badge={settingBadge} />
|
|
446
492
|
</Link>
|
|
447
493
|
</li>
|
|
448
494
|
);
|
|
@@ -604,37 +650,65 @@ function Navigation({
|
|
|
604
650
|
if (hasChildren) {
|
|
605
651
|
return (
|
|
606
652
|
<ul className={styles.navDropdown}>
|
|
607
|
-
{n.children.map(
|
|
608
|
-
(
|
|
609
|
-
child.hidden
|
|
610
|
-
child.permission
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
653
|
+
{n.children.map((child, childKey) => {
|
|
654
|
+
if (
|
|
655
|
+
child.hidden === true ||
|
|
656
|
+
child.permission !== true
|
|
657
|
+
) {
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const childBadge = badgeFor(badges, child.id);
|
|
662
|
+
|
|
663
|
+
return (
|
|
664
|
+
<li key={`nav-child-${childKey}`}>
|
|
665
|
+
<Link
|
|
666
|
+
to={child.url}
|
|
667
|
+
aria-label={badgeAriaLabel(
|
|
668
|
+
child.label,
|
|
669
|
+
childBadge
|
|
670
|
+
)}
|
|
671
|
+
className={
|
|
672
|
+
cleanUrl(child.url) ===
|
|
673
|
+
bestMatchingUrl(
|
|
674
|
+
n.children.map(
|
|
675
|
+
(one) => one.url
|
|
676
|
+
),
|
|
677
|
+
currentPage
|
|
678
|
+
)
|
|
679
|
+
? styles.active
|
|
680
|
+
: ''
|
|
681
|
+
}
|
|
682
|
+
onClick={() => setOpen(false)}
|
|
683
|
+
>
|
|
684
|
+
{child.label}
|
|
685
|
+
<BadgePill badge={childBadge} />
|
|
686
|
+
</Link>
|
|
687
|
+
</li>
|
|
688
|
+
);
|
|
689
|
+
})}
|
|
632
690
|
</ul>
|
|
633
691
|
);
|
|
634
692
|
}
|
|
635
693
|
return null;
|
|
636
694
|
};
|
|
637
695
|
|
|
696
|
+
/*
|
|
697
|
+
* The item's own count, plus its visible children's — a number
|
|
698
|
+
* published against a row inside a closed dropdown would otherwise be
|
|
699
|
+
* invisible until somebody hovered the group. navBadges.js holds the
|
|
700
|
+
* rule; null when there is nothing to show, which is the usual case.
|
|
701
|
+
*/
|
|
702
|
+
const itemBadge = navBadge(badges, n);
|
|
703
|
+
const ariaLabel = badgeAriaLabel(n.label, itemBadge);
|
|
704
|
+
const contents = (
|
|
705
|
+
<>
|
|
706
|
+
{NavItemIcon ? <NavItemIcon size={18} /> : null}
|
|
707
|
+
{n.label}
|
|
708
|
+
<BadgePill badge={itemBadge} />
|
|
709
|
+
</>
|
|
710
|
+
);
|
|
711
|
+
|
|
638
712
|
return (
|
|
639
713
|
<li
|
|
640
714
|
className={navItemClasses}
|
|
@@ -644,12 +718,51 @@ function Navigation({
|
|
|
644
718
|
{n.url ? (
|
|
645
719
|
// Close the mobile drawer when navigating; no-op for the
|
|
646
720
|
// header layout where the drawer state is never opened.
|
|
647
|
-
<Link
|
|
648
|
-
|
|
649
|
-
{
|
|
721
|
+
<Link
|
|
722
|
+
to={n.url}
|
|
723
|
+
aria-label={ariaLabel}
|
|
724
|
+
onClick={() => setOpen(false)}
|
|
725
|
+
>
|
|
726
|
+
{contents}
|
|
650
727
|
</Link>
|
|
728
|
+
) : hasChildren ? (
|
|
729
|
+
/*
|
|
730
|
+
* A group heading — no page of its own, so there is
|
|
731
|
+
* nothing to link to, but it still has to be REACHABLE.
|
|
732
|
+
* As a <span> it was not focusable and the menu opened on
|
|
733
|
+
* :hover alone, which left every page inside a group
|
|
734
|
+
* unreachable by keyboard and untappable on a touch
|
|
735
|
+
* screen. A button takes focus, and the stylesheet opens
|
|
736
|
+
* the dropdown on :focus-within as well as :hover.
|
|
737
|
+
*
|
|
738
|
+
* No onClick and no aria-expanded: the open state lives
|
|
739
|
+
* in CSS, not in React (deliberately — see the discarded
|
|
740
|
+
* `activeDropdown` setter above, which exists only to
|
|
741
|
+
* toggle the scroll container's overflow), and claiming a
|
|
742
|
+
* state this component does not hold would be a lie to
|
|
743
|
+
* the screen reader rather than a help.
|
|
744
|
+
*/
|
|
745
|
+
<button
|
|
746
|
+
type="button"
|
|
747
|
+
aria-haspopup="true"
|
|
748
|
+
aria-label={ariaLabel}
|
|
749
|
+
/* The menu opens on :hover OR :focus-within. A MOUSE
|
|
750
|
+
click leaves the button focused, so the menu it
|
|
751
|
+
opened stayed pinned while hovering the next group
|
|
752
|
+
opened that one too — two dropdowns at once. Blur
|
|
753
|
+
on pointer clicks only: e.detail is the click
|
|
754
|
+
count, 0 for keyboard "clicks", so Tab + Enter
|
|
755
|
+
users keep the focus that holds their menu open. */
|
|
756
|
+
onClick={(e) => {
|
|
757
|
+
if (e.detail > 0) {
|
|
758
|
+
e.currentTarget.blur();
|
|
759
|
+
}
|
|
760
|
+
}}
|
|
761
|
+
>
|
|
762
|
+
{contents}
|
|
763
|
+
</button>
|
|
651
764
|
) : (
|
|
652
|
-
<span>{
|
|
765
|
+
<span aria-label={ariaLabel}>{contents}</span>
|
|
653
766
|
)}
|
|
654
767
|
{renderChildren()}
|
|
655
768
|
</li>
|
|
@@ -719,10 +832,16 @@ function Navigation({
|
|
|
719
832
|
// says, and this state has to be reversible — switching a feature
|
|
720
833
|
// back on must restore the menu item without a page reload, which
|
|
721
834
|
// dropping it from the list would not.
|
|
835
|
+
//
|
|
836
|
+
// `hide: true` in the navigation config is the STATIC spelling of the
|
|
837
|
+
// same state — a page an app keeps routable but out of the menu (the
|
|
838
|
+
// same flag <Form> honours on a field). Deleting the entry would work
|
|
839
|
+
// too, but then the config could no longer say why it is absent.
|
|
722
840
|
const isSwitchedOff = (nav) =>
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
841
|
+
nav.hide === true ||
|
|
842
|
+
(featureFlags !== null &&
|
|
843
|
+
Boolean(nav.id) &&
|
|
844
|
+
featureFlags[nav.id] === false);
|
|
726
845
|
|
|
727
846
|
setNavData((prevNavData) => {
|
|
728
847
|
const updatePermissions = (nav) => {
|
|
@@ -743,7 +862,24 @@ function Navigation({
|
|
|
743
862
|
const updatedChildren = nav.children
|
|
744
863
|
.map(updatePermissions)
|
|
745
864
|
.filter(Boolean);
|
|
746
|
-
|
|
865
|
+
/*
|
|
866
|
+
* A group with nothing in it that this user may open is
|
|
867
|
+
* not a group, it is a dead heading — so the parent goes
|
|
868
|
+
* with its children.
|
|
869
|
+
*
|
|
870
|
+
* This was `updatedChildren.length > 0`, which is a test
|
|
871
|
+
* of whether the config declared any children at all:
|
|
872
|
+
* `updatePermissions` always returns an object, so the
|
|
873
|
+
* `.filter(Boolean)` above it never removes one and the
|
|
874
|
+
* length is a constant read off the JSON. Every group
|
|
875
|
+
* parent therefore showed for everybody, opening onto an
|
|
876
|
+
* empty menu — which is precisely what the cascade was
|
|
877
|
+
* written to prevent.
|
|
878
|
+
*/
|
|
879
|
+
const childPermission = updatedChildren.some(
|
|
880
|
+
(child) =>
|
|
881
|
+
child.permission === true && child.hidden !== true
|
|
882
|
+
);
|
|
747
883
|
|
|
748
884
|
if (nav.permissionKey) {
|
|
749
885
|
const matchingPermission = permissions.includes(
|
|
@@ -1329,6 +1329,40 @@ export const renderFileColumn = ({
|
|
|
1329
1329
|
handleDownload,
|
|
1330
1330
|
styles,
|
|
1331
1331
|
}) => {
|
|
1332
|
+
// `styles.tdaction` belongs on the WRAPPER, not on the glyph.
|
|
1333
|
+
//
|
|
1334
|
+
// The class is a 28px hit target — `display: inline-flex; width: 28px;
|
|
1335
|
+
// height: 28px` in DataGrid.module.scss, written so a row action is
|
|
1336
|
+
// something a finger can land on. Put it on an `<svg>` and those two
|
|
1337
|
+
// lengths do not draw a box around the icon, they ARE the icon: CSS
|
|
1338
|
+
// `width`/`height` outrank the `width="14"` / `height="14"` presentation
|
|
1339
|
+
// attributes lucide renders from `size`, so the declared 14px glyph was
|
|
1340
|
+
// painted at 28 — measured, not guessed — twice the size of the 13px row
|
|
1341
|
+
// text it sits in and twice the 18px the action column's own icons use.
|
|
1342
|
+
//
|
|
1343
|
+
// The action column never had the problem because its icons live inside
|
|
1344
|
+
// `.tdactions`, where `.tdactions span svg { width: 18px }` is the more
|
|
1345
|
+
// specific rule and wins. This column renders its icon on its own, so the
|
|
1346
|
+
// bare `.tdaction` was the only rule reaching it.
|
|
1347
|
+
//
|
|
1348
|
+
// Moving the class one level out is what the actions column already does:
|
|
1349
|
+
// the span becomes the 28px target (and picks up the hover wash and focus
|
|
1350
|
+
// ring that were being painted on the glyph), and the icon is free to be
|
|
1351
|
+
// the size it asks for. The span keeps its 10px right margin, so a row of
|
|
1352
|
+
// several files occupies exactly the width it did before.
|
|
1353
|
+
const icon = (tooltip) => (
|
|
1354
|
+
<Download
|
|
1355
|
+
data-tooltip-id="system-tooltip"
|
|
1356
|
+
data-tooltip-content={tooltip}
|
|
1357
|
+
strokeWidth={2}
|
|
1358
|
+
size={16}
|
|
1359
|
+
/>
|
|
1360
|
+
);
|
|
1361
|
+
|
|
1362
|
+
// `inline-flex` comes from the class; declaring `display` here as well
|
|
1363
|
+
// would beat it from the style attribute and un-centre the glyph.
|
|
1364
|
+
const wrapperStyle = { marginRight: '10px' };
|
|
1365
|
+
|
|
1332
1366
|
return {
|
|
1333
1367
|
...commonProps,
|
|
1334
1368
|
name: column.id,
|
|
@@ -1341,48 +1375,30 @@ export const renderFileColumn = ({
|
|
|
1341
1375
|
return data[column.id].map((file, index) => (
|
|
1342
1376
|
<span
|
|
1343
1377
|
key={index}
|
|
1378
|
+
className={styles.tdaction}
|
|
1344
1379
|
onClick={(e) => {
|
|
1345
1380
|
e.stopPropagation();
|
|
1346
1381
|
e.preventDefault();
|
|
1347
1382
|
handleDownload(file);
|
|
1348
1383
|
}}
|
|
1349
|
-
style={
|
|
1350
|
-
marginRight: '10px',
|
|
1351
|
-
display: 'inline-block',
|
|
1352
|
-
}}
|
|
1384
|
+
style={wrapperStyle}
|
|
1353
1385
|
>
|
|
1354
|
-
|
|
1355
|
-
data-tooltip-id="system-tooltip"
|
|
1356
|
-
data-tooltip-content={`Download File ${file.file_name}`}
|
|
1357
|
-
strokeWidth={2}
|
|
1358
|
-
size={14}
|
|
1359
|
-
className={styles.tdaction}
|
|
1360
|
-
/>
|
|
1386
|
+
{icon(`Download File ${file.file_name}`)}
|
|
1361
1387
|
</span>
|
|
1362
1388
|
));
|
|
1363
1389
|
} else {
|
|
1364
1390
|
// Return a single span for a single file with added margin
|
|
1365
1391
|
return (
|
|
1366
1392
|
<span
|
|
1393
|
+
className={styles.tdaction}
|
|
1367
1394
|
onClick={(e) => {
|
|
1368
1395
|
e.stopPropagation();
|
|
1369
1396
|
e.preventDefault();
|
|
1370
1397
|
handleDownload(data[column.id]);
|
|
1371
1398
|
}}
|
|
1372
|
-
style={
|
|
1373
|
-
marginRight: '10px',
|
|
1374
|
-
display: 'inline-block',
|
|
1375
|
-
}}
|
|
1399
|
+
style={wrapperStyle}
|
|
1376
1400
|
>
|
|
1377
|
-
|
|
1378
|
-
data-tooltip-id="system-tooltip"
|
|
1379
|
-
data-tooltip-content={`Download File ${
|
|
1380
|
-
data[column.id].file_name
|
|
1381
|
-
}`}
|
|
1382
|
-
strokeWidth={2}
|
|
1383
|
-
size={14}
|
|
1384
|
-
className={styles.tdaction}
|
|
1385
|
-
/>
|
|
1401
|
+
{icon(`Download File ${data[column.id].file_name}`)}
|
|
1386
1402
|
</span>
|
|
1387
1403
|
);
|
|
1388
1404
|
}
|
|
@@ -189,6 +189,68 @@ export const openedByGesture = (
|
|
|
189
189
|
return !!node && !!node.contains && node.contains(gesture.target);
|
|
190
190
|
};
|
|
191
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Breathing room left between a widened menu and the edge that would clip it.
|
|
194
|
+
*/
|
|
195
|
+
const MENU_GUTTER_PX = 8;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* How much room the option menu has to grow into before the grid clips it, in
|
|
199
|
+
* pixels — written onto the combo as `--vs-filter-menu-avail` for
|
|
200
|
+
* `global-datagrid.css` to cap the menu's `max-width` with.
|
|
201
|
+
*
|
|
202
|
+
* The menu is a direct child of the combo, absolutely positioned, and the
|
|
203
|
+
* nearest ancestor that clips is `.InovuaReactDataGrid__body` (`overflow:
|
|
204
|
+
* hidden`, so the grid can virtualise). The stylesheet now widens the menu to
|
|
205
|
+
* fit its longest option instead of ellipsising it into the column's width —
|
|
206
|
+
* but CSS cannot ask "how far is it from this column to the right edge of the
|
|
207
|
+
* grid?", so a menu on a right-hand column would grow straight into that clip
|
|
208
|
+
* and come out sliced. This measures the answer.
|
|
209
|
+
*
|
|
210
|
+
* Purely presentational: it sets one custom property and touches nothing else.
|
|
211
|
+
* The option list, the value and every event are untouched, and if this never
|
|
212
|
+
* runs the stylesheet's own 420px / 90vw caps still apply.
|
|
213
|
+
*
|
|
214
|
+
* Measured on the COMBO, not the menu, and before the menu mounts, so the
|
|
215
|
+
* first painted frame is already the right width. Recomputed on every open, so
|
|
216
|
+
* a resized grid or a dragged column is picked up with nothing to invalidate.
|
|
217
|
+
*/
|
|
218
|
+
export const measureMenuRoom = (node) => {
|
|
219
|
+
if (!node || typeof node.getBoundingClientRect !== 'function') {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const clip =
|
|
224
|
+
typeof node.closest === 'function'
|
|
225
|
+
? node.closest('.InovuaReactDataGrid__body') ||
|
|
226
|
+
node.closest('.InovuaReactDataGrid')
|
|
227
|
+
: null;
|
|
228
|
+
|
|
229
|
+
const right = clip
|
|
230
|
+
? clip.getBoundingClientRect().right
|
|
231
|
+
: typeof window !== 'undefined'
|
|
232
|
+
? window.innerWidth
|
|
233
|
+
: 0;
|
|
234
|
+
|
|
235
|
+
const box = node.getBoundingClientRect();
|
|
236
|
+
|
|
237
|
+
// Never narrower than the field itself: a combo scrolled half out of view
|
|
238
|
+
// would otherwise be told it has no room at all. The stylesheet's
|
|
239
|
+
// `min-width` would fight that and win, but an honest floor is cheaper
|
|
240
|
+
// than relying on which cap happens to be stronger.
|
|
241
|
+
return Math.max(right - box.left - MENU_GUTTER_PX, box.width);
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
const applyMenuRoom = (node) => {
|
|
245
|
+
const room = measureMenuRoom(node);
|
|
246
|
+
|
|
247
|
+
if (room == null || !node || !node.style) {
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
node.style.setProperty('--vs-filter-menu-avail', `${Math.round(room)}px`);
|
|
252
|
+
};
|
|
253
|
+
|
|
192
254
|
/**
|
|
193
255
|
* "Is something actually filtered here?" — `null` is the `select` filter
|
|
194
256
|
* type's emptyValue, `''` is what DataGrid's dependent-filter reset writes,
|
|
@@ -360,6 +422,23 @@ const SelectFilterEditor = forwardRef((props, ref) => {
|
|
|
360
422
|
editorProps.onExpandedChange(expanded);
|
|
361
423
|
}
|
|
362
424
|
|
|
425
|
+
// Unconditional, and before the clear gate: every open gets
|
|
426
|
+
// a menu sized to the room it has, whether or not this
|
|
427
|
+
// particular open also clears the filter.
|
|
428
|
+
if (expanded) {
|
|
429
|
+
const instance = innerRef.current;
|
|
430
|
+
const combo =
|
|
431
|
+
instance &&
|
|
432
|
+
instance.getInputRef &&
|
|
433
|
+
instance.getInputRef();
|
|
434
|
+
|
|
435
|
+
applyMenuRoom(
|
|
436
|
+
combo && combo.getComboNode
|
|
437
|
+
? combo.getComboNode()
|
|
438
|
+
: null
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
363
442
|
clearRef.current(expanded);
|
|
364
443
|
},
|
|
365
444
|
}}
|
|
@@ -112,6 +112,14 @@ const GenericAuth = ({
|
|
|
112
112
|
// Extra props handed to whatever `mainComponent` is, for a custom shell
|
|
113
113
|
// that needs something GenericMain does not take.
|
|
114
114
|
mainComponentProps = {},
|
|
115
|
+
// Live counts for navigation items, `{ tickets: 4 }`, handed down to
|
|
116
|
+
// Navigation through the shell. First-class rather than something to bury
|
|
117
|
+
// in `mainComponentProps` because it travels the same route `userProfile`
|
|
118
|
+
// does and belongs to the same interface — and an app whose counts are
|
|
119
|
+
// computed inside its own shell (the usual case, since the shell is where
|
|
120
|
+
// the websocket lives) simply passes its own `navBadges` from there
|
|
121
|
+
// instead, which lands after this one and wins.
|
|
122
|
+
navBadges = {},
|
|
115
123
|
}) => {
|
|
116
124
|
const navigate = useNavigate();
|
|
117
125
|
const location = useLocation();
|
|
@@ -404,6 +412,7 @@ const GenericAuth = ({
|
|
|
404
412
|
userProfile={userProfile}
|
|
405
413
|
enforce2FA={enforce2FA}
|
|
406
414
|
density={density}
|
|
415
|
+
navBadges={navBadges}
|
|
407
416
|
{...mainComponentProps}
|
|
408
417
|
/>
|
|
409
418
|
}
|
|
@@ -1101,7 +1101,9 @@ function GenericDashboard({ setting, userProfile, dynamicDashboard = false }) {
|
|
|
1101
1101
|
</label>
|
|
1102
1102
|
<select
|
|
1103
1103
|
name="year"
|
|
1104
|
-
style={{
|
|
1104
|
+
style={{
|
|
1105
|
+
height: 'var(--field-height, 50px)',
|
|
1106
|
+
}}
|
|
1105
1107
|
value={
|
|
1106
1108
|
filters[widget.id]?.[
|
|
1107
1109
|
filter.id
|
|
@@ -38,6 +38,11 @@ const GenericMain = ({
|
|
|
38
38
|
// Passed straight to Navigation: per-id renderers for account actions.
|
|
39
39
|
// See the `renderers` note in Navigation.jsx.
|
|
40
40
|
navRenderers = {},
|
|
41
|
+
// Passed straight to Navigation as `badges`: live counts keyed by nav id,
|
|
42
|
+
// `{ tickets: 4 }`. Threaded rather than fetched here for the same reason
|
|
43
|
+
// `navRenderers` is — the shell does not know what a project counts, who
|
|
44
|
+
// is allowed to see it, or how often it changes. See navBadges.js.
|
|
45
|
+
navBadges = {},
|
|
41
46
|
}) => {
|
|
42
47
|
const navigate = useNavigate();
|
|
43
48
|
const location = useLocation();
|
|
@@ -151,6 +156,7 @@ const GenericMain = ({
|
|
|
151
156
|
themeType={themeType}
|
|
152
157
|
userProfile={userProfile}
|
|
153
158
|
renderers={navRenderers}
|
|
159
|
+
badges={navBadges}
|
|
154
160
|
/>
|
|
155
161
|
</header>
|
|
156
162
|
{navigation.hasOwnProperty('quickActions') && (
|
|
@@ -182,6 +188,7 @@ const GenericMain = ({
|
|
|
182
188
|
themeType={themeType}
|
|
183
189
|
userProfile={userProfile}
|
|
184
190
|
renderers={navRenderers}
|
|
191
|
+
badges={navBadges}
|
|
185
192
|
>
|
|
186
193
|
<div className={styles['content-main']} id="app">
|
|
187
194
|
<div className={styles.paddingSimple}>
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The count pill a navigation item wears.
|
|
3
|
+
*
|
|
4
|
+
* Split out of Navigation.jsx for the same reason navActive.js was: the rule
|
|
5
|
+
* is a handful of judgement calls — what counts as "no badge", what a group
|
|
6
|
+
* parent shows, where the cap falls — and every one of them is testable
|
|
7
|
+
* without a DOM, a router or a stylesheet. Navigation.jsx renders what this
|
|
8
|
+
* file decides and decides nothing itself.
|
|
9
|
+
*
|
|
10
|
+
* The consumer publishes `badges` on <Navigation> as `{ [navId]: number }`:
|
|
11
|
+
*
|
|
12
|
+
* badges={{ tickets: 4 }} -> "4", "Tickets, 4 open"
|
|
13
|
+
* badges={{ tickets: { count: 4, label: 'unread' } }}
|
|
14
|
+
* -> "4", "Tickets, 4 unread"
|
|
15
|
+
*
|
|
16
|
+
* The object form exists because the pill itself is only a number and the
|
|
17
|
+
* accessible name has to say what the number IS — a screen reader announcing
|
|
18
|
+
* "Tickets, 4" has told the user nothing. `open` is the default noun because
|
|
19
|
+
* that is what a count on a work queue almost always means; anything else
|
|
20
|
+
* says so.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** The noun the accessible name falls back to. See the note above. */
|
|
24
|
+
export const DEFAULT_BADGE_NOUN = 'open';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Above this the pill reads `99+`.
|
|
28
|
+
*
|
|
29
|
+
* Not a style choice: the bar is a fixed-height row of items and a four-digit
|
|
30
|
+
* pill pushes the label it belongs to out of the bar. The ACCESSIBLE name
|
|
31
|
+
* still carries the real figure — a reader that cannot see the pill has no
|
|
32
|
+
* reason to be given a rounded one.
|
|
33
|
+
*/
|
|
34
|
+
export const BADGE_MAX = 99;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* One badge value, whatever shape the consumer published it in.
|
|
38
|
+
*
|
|
39
|
+
* Everything that is not a positive, finite number is "no badge" and returns
|
|
40
|
+
* null — 0, undefined, null, NaN, a string that will not parse, a negative.
|
|
41
|
+
* That is deliberate and it is the common case: a count endpoint that has not
|
|
42
|
+
* answered yet, or has answered zero, must render nothing at all rather than
|
|
43
|
+
* a "0" pill announcing that there is nothing to do.
|
|
44
|
+
*/
|
|
45
|
+
const normalise = (raw) => {
|
|
46
|
+
if (raw === null || raw === undefined) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const isObject = typeof raw === 'object';
|
|
51
|
+
const count = Number(isObject ? raw.count : raw);
|
|
52
|
+
|
|
53
|
+
if (!Number.isFinite(count) || count <= 0) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const whole = Math.floor(count);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
count: whole,
|
|
61
|
+
// EVERY badge carries what the pill prints, not only the summed one
|
|
62
|
+
// navBadge() builds. The settings chips feed BadgePill straight from
|
|
63
|
+
// badgeFor(), and the first one shipped without this line rendered a
|
|
64
|
+
// green circle with no number in it — the aria-label read "1 open"
|
|
65
|
+
// off `count` while the pill printed an undefined `display`.
|
|
66
|
+
display: badgeDisplay(whole),
|
|
67
|
+
noun: (isObject && raw.label) || DEFAULT_BADGE_NOUN,
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/** What the pill prints: the count, or `99+` once it runs past the cap. */
|
|
72
|
+
export const badgeDisplay = (count) =>
|
|
73
|
+
count > BADGE_MAX ? `${BADGE_MAX}+` : String(count);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The badge for one id, or null.
|
|
77
|
+
*
|
|
78
|
+
* @param {object} badges the `{ [navId]: number }` map, or nothing.
|
|
79
|
+
* @param {string} id the nav entry's id.
|
|
80
|
+
*/
|
|
81
|
+
export function badgeFor(badges, id) {
|
|
82
|
+
if (!badges || !id) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return normalise(badges[id]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The badge a nav ITEM shows: its own count plus its visible children's.
|
|
91
|
+
*
|
|
92
|
+
* A group parent has no page of its own, so the only honest thing for it to
|
|
93
|
+
* show is the sum of what is behind it — otherwise a count published against
|
|
94
|
+
* a child inside a closed dropdown is invisible until somebody happens to
|
|
95
|
+
* hover the parent, which defeats the point of a badge.
|
|
96
|
+
*
|
|
97
|
+
* Children that are hidden or not permitted are NOT counted. A number the
|
|
98
|
+
* reader cannot act on — because the row it belongs to is not in their menu —
|
|
99
|
+
* is worse than no number: they would go looking for work that is not theirs.
|
|
100
|
+
*
|
|
101
|
+
* @returns {{count: number, display: string, noun: string}|null}
|
|
102
|
+
*/
|
|
103
|
+
export function navBadge(badges, nav) {
|
|
104
|
+
if (!badges || !nav) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const found = [badgeFor(badges, nav.id)];
|
|
109
|
+
|
|
110
|
+
(nav.children || []).forEach((child) => {
|
|
111
|
+
if (child && child.hidden !== true && child.permission === true) {
|
|
112
|
+
found.push(badgeFor(badges, child.id));
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const parts = found.filter(Boolean);
|
|
117
|
+
|
|
118
|
+
if (parts.length === 0) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const count = parts.reduce((sum, part) => sum + part.count, 0);
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
count,
|
|
126
|
+
display: badgeDisplay(count),
|
|
127
|
+
// The parent borrows the noun of the first thing that contributed;
|
|
128
|
+
// a group whose children count different nouns cannot be summarised
|
|
129
|
+
// in one word anyway, and in practice one child carries the badge.
|
|
130
|
+
noun: parts[0].noun,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The item's accessible name, with the count folded into it.
|
|
136
|
+
*
|
|
137
|
+
* `undefined` when there is no badge, so the caller can spread it onto the
|
|
138
|
+
* element and leave the natural text as the name — an aria-label that merely
|
|
139
|
+
* repeats the visible label is noise a screen reader has to read anyway.
|
|
140
|
+
*
|
|
141
|
+
* The REAL count, not the capped display: "Tickets, 340 open" is useful and
|
|
142
|
+
* "Tickets, 99+ open" is a rendering artefact.
|
|
143
|
+
*/
|
|
144
|
+
export function badgeAriaLabel(label, badge) {
|
|
145
|
+
if (!badge || !label) {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return `${label}, ${badge.count} ${badge.noun}`;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export default navBadge;
|
|
@@ -36,10 +36,15 @@
|
|
|
36
36
|
box-shadow: none !important;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
/* Ensure consistent height between select and button
|
|
39
|
+
/* Ensure consistent height between select and button.
|
|
40
|
+
|
|
41
|
+
`3.1rem` (49.6px) was a guess at "typical input height" and it beat the
|
|
42
|
+
token rule further down, which is (0,10,1) to this selector's (0,3,1) — so
|
|
43
|
+
a dropdown with a create button beside it was the one select in the library
|
|
44
|
+
that did NOT follow `--field-height`. */
|
|
40
45
|
.dropdownContainer:has(.buttonRight) .selectFullWidth select {
|
|
41
|
-
height:
|
|
42
|
-
min-height:
|
|
46
|
+
height: var(--field-height, 50px);
|
|
47
|
+
min-height: var(--field-height, 50px);
|
|
43
48
|
}
|
|
44
49
|
|
|
45
50
|
.buttonRight {
|
|
@@ -47,7 +52,9 @@
|
|
|
47
52
|
align-items: center;
|
|
48
53
|
justify-content: center;
|
|
49
54
|
min-width: 42px;
|
|
50
|
-
|
|
55
|
+
/* The button is the right-hand half of the field it is joined to, so it
|
|
56
|
+
takes the field's height, not its own. */
|
|
57
|
+
min-height: var(--field-height, 50px);
|
|
51
58
|
background: rgba(var(--primary-color-rgb, 27, 57, 51), 0.08);
|
|
52
59
|
color: var(--primary-color);
|
|
53
60
|
border: 1px solid rgba(var(--primary-color-rgb, 27, 57, 51), 0.25);
|
|
@@ -113,8 +120,8 @@
|
|
|
113
120
|
}
|
|
114
121
|
|
|
115
122
|
.dropdownContainer.hasButton .selectFullWidth select {
|
|
116
|
-
height:
|
|
117
|
-
min-height:
|
|
123
|
+
height: var(--field-height, 50px);
|
|
124
|
+
min-height: var(--field-height, 50px);
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
.formcontainer {
|
|
@@ -382,7 +389,15 @@ select:not(:placeholder-shown) + .fi__span {
|
|
|
382
389
|
font-size: 0.85rem;
|
|
383
390
|
background: white;
|
|
384
391
|
border-radius: var(--br);
|
|
385
|
-
padding: 0.85rem
|
|
392
|
+
/* Was `padding: 0.85rem`, and that one declaration is what made a form
|
|
393
|
+
row uneven. This selector list carries three specificities: `.formItem
|
|
394
|
+
select` and `.formItem textarea` are (0,1,1) and BEAT the bare `select`
|
|
395
|
+
/ `textarea` in global.css, while `.formItem input[type]:not()…` is
|
|
396
|
+
(0,10,1) and LOSES to the twenty-clause chain there. So the same rule
|
|
397
|
+
set the select's padding and not the input's — 0.85rem on one, 1rem on
|
|
398
|
+
the other, 42.78px beside 50px. Padding is horizontal only now, the box
|
|
399
|
+
is `--field-height`, and both files state the same thing. */
|
|
400
|
+
padding: 0 1rem;
|
|
386
401
|
outline: none;
|
|
387
402
|
width: 100%;
|
|
388
403
|
box-sizing: border-box;
|
|
@@ -390,6 +405,23 @@ select:not(:placeholder-shown) + .fi__span {
|
|
|
390
405
|
transition: box-shadow 0.15s linear;
|
|
391
406
|
}
|
|
392
407
|
|
|
408
|
+
/* Everything above except the textarea is a single-line control and takes the
|
|
409
|
+
one height. Stated here as well as in global.css because of the specificity
|
|
410
|
+
split described above: without it the `select` in a form would keep sizing
|
|
411
|
+
itself off its own line box. */
|
|
412
|
+
.formItem select,
|
|
413
|
+
.formItem
|
|
414
|
+
input[type]:not([type='search']):not([type='url']):not([type='hidden']):not(
|
|
415
|
+
[type='checkbox']
|
|
416
|
+
):not([type='radio']):not(.inovua-react-toolkit-combo-box__input):not(
|
|
417
|
+
.inovua-react-toolkit-date-input__input
|
|
418
|
+
):not(.inovua-react-toolkit-text-input__input):not(
|
|
419
|
+
.inovua-react-toolkit-numeric-input__input
|
|
420
|
+
) {
|
|
421
|
+
height: var(--field-height, 50px);
|
|
422
|
+
min-height: var(--field-height, 50px);
|
|
423
|
+
}
|
|
424
|
+
|
|
393
425
|
.formItem textarea:hover,
|
|
394
426
|
.formItem select:hover,
|
|
395
427
|
.formItem
|
|
@@ -415,6 +447,13 @@ select:not(:placeholder-shown) + .fi__span {
|
|
|
415
447
|
|
|
416
448
|
.formItem textarea {
|
|
417
449
|
line-height: 1.65 !important;
|
|
450
|
+
/* The one control that keeps vertical padding — but the amount is derived
|
|
451
|
+
from the same token, so its first row sits on the same line as the
|
|
452
|
+
single-line fields beside it instead of at an unrelated 0.85rem. The
|
|
453
|
+
literal is the fallback for browsers without the `lh` unit. */
|
|
454
|
+
height: auto;
|
|
455
|
+
padding: 0.85rem 1rem;
|
|
456
|
+
padding: calc((var(--field-height, 50px) - 1lh) / 2 - 1px) 1rem;
|
|
418
457
|
}
|
|
419
458
|
|
|
420
459
|
input[type='file'] {
|
|
@@ -93,7 +93,7 @@
|
|
|
93
93
|
|
|
94
94
|
// Control styles
|
|
95
95
|
.visns-select__control {
|
|
96
|
-
min-height: var(--field-height,
|
|
96
|
+
min-height: var(--field-height, 50px) !important;
|
|
97
97
|
height: auto !important; /* Allow height to grow with content */
|
|
98
98
|
overflow: visible !important;
|
|
99
99
|
}
|
|
@@ -199,7 +199,7 @@
|
|
|
199
199
|
// CSS class fixes for React Select
|
|
200
200
|
.css-13cymwt-control,
|
|
201
201
|
.css-t3ipsp-control {
|
|
202
|
-
min-height: var(--field-height,
|
|
202
|
+
min-height: var(--field-height, 50px) !important;
|
|
203
203
|
height: auto !important;
|
|
204
204
|
overflow: visible !important;
|
|
205
205
|
}
|
|
@@ -429,7 +429,14 @@
|
|
|
429
429
|
}
|
|
430
430
|
}
|
|
431
431
|
|
|
432
|
-
|
|
432
|
+
/* `:focus-within` matches the item while the parent itself holds
|
|
433
|
+
focus, which is what opens the menu for a keyboard user: tab to
|
|
434
|
+
the group heading, the rows appear, tab again and focus moves
|
|
435
|
+
into them — and the menu stays open because focus is still
|
|
436
|
+
inside the item. Without this the dropdown was hover-only and
|
|
437
|
+
every page inside a group was unreachable without a mouse. */
|
|
438
|
+
&:hover > ul,
|
|
439
|
+
&:focus-within > ul {
|
|
433
440
|
display: block;
|
|
434
441
|
opacity: 1;
|
|
435
442
|
}
|
|
@@ -553,7 +560,9 @@
|
|
|
553
560
|
}
|
|
554
561
|
}
|
|
555
562
|
|
|
556
|
-
|
|
563
|
+
/* Same keyboard path as `.nav-item` above. */
|
|
564
|
+
&:hover > ul,
|
|
565
|
+
&:focus-within > ul {
|
|
557
566
|
display: block;
|
|
558
567
|
opacity: 1;
|
|
559
568
|
}
|
|
@@ -561,6 +570,65 @@
|
|
|
561
570
|
}
|
|
562
571
|
}
|
|
563
572
|
|
|
573
|
+
/* ----------------------------------------------------------------------------
|
|
574
|
+
Count pills
|
|
575
|
+
----------------------------------------------------------------------------
|
|
576
|
+
`<Navigation badges={{ tickets: 4 }} />`. The pill is inline — part of the
|
|
577
|
+
item's own flex row rather than a floating dot pinned to a corner — because
|
|
578
|
+
these items are text labels of wildly different widths, and an absolutely
|
|
579
|
+
positioned badge on "Support Blocks" lands somewhere quite different from
|
|
580
|
+
one on "Zoom". Sitting in the row, it simply follows the label everywhere,
|
|
581
|
+
dropdown rows and the sidebar included.
|
|
582
|
+
|
|
583
|
+
The colour carries a fallback for the same reason Notification's badge
|
|
584
|
+
does: a `var()` with no fallback on a token the host has not declared is
|
|
585
|
+
invalid at computed-value time and the WHOLE declaration is discarded, so
|
|
586
|
+
the pill would render its white text on nothing at all — and the pill is
|
|
587
|
+
the only place the count appears.
|
|
588
|
+
------------------------------------------------------------------------- */
|
|
589
|
+
.nav-badge {
|
|
590
|
+
/* Round at any single-digit width, and a lozenge once it reads `99+`.
|
|
591
|
+
SIZED TO MATCH the bell's and the SMS badge (16px box, 10px digit) —
|
|
592
|
+
"two unread counts side by side have to sit on the same line as each
|
|
593
|
+
other", per Sms.module.scss, and a 14px pill beside their 16px read as
|
|
594
|
+
an error rather than a choice. */
|
|
595
|
+
min-width: 16px;
|
|
596
|
+
height: 16px;
|
|
597
|
+
padding: 0 4px;
|
|
598
|
+
box-sizing: border-box;
|
|
599
|
+
display: inline-flex;
|
|
600
|
+
align-items: center;
|
|
601
|
+
justify-content: center;
|
|
602
|
+
flex: none;
|
|
603
|
+
border-radius: var(--radius-pill, 999px);
|
|
604
|
+
background-color: var(--secondary-color, #3cbf7d);
|
|
605
|
+
/* DARK ink, not white. The other badges are white-on-crimson; this one's
|
|
606
|
+
ground is the brand GREEN, and a 10px white digit on #3cbf7d has so
|
|
607
|
+
little contrast it reads as a solid dot — which is exactly how it was
|
|
608
|
+
reported: "a green circle with no number". */
|
|
609
|
+
color: var(--primary-color, #1b3933);
|
|
610
|
+
/* Never inheriting the semibold the active item puts on its label — a
|
|
611
|
+
badge that grows when you are standing on the page it counts reads as
|
|
612
|
+
a change in the number. */
|
|
613
|
+
font-size: 10px;
|
|
614
|
+
font-weight: 700;
|
|
615
|
+
line-height: 1;
|
|
616
|
+
font-variant-numeric: tabular-nums;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/* In a dropdown row the label is a block-level link, so the pill is pushed to
|
|
620
|
+
the row's trailing edge rather than butted against the text. Both themed
|
|
621
|
+
selectors are spelled out to match the specificity of the blocks above —
|
|
622
|
+
anything shorter loses to the `display: block` they already set on these
|
|
623
|
+
links and the rule would simply never apply. */
|
|
624
|
+
.app-nav .nav-item > ul li > a,
|
|
625
|
+
.app-nav .nav-item-alternate > ul li > a {
|
|
626
|
+
display: flex;
|
|
627
|
+
align-items: center;
|
|
628
|
+
justify-content: space-between;
|
|
629
|
+
gap: 0.5rem;
|
|
630
|
+
}
|
|
631
|
+
|
|
564
632
|
.hactions {
|
|
565
633
|
width: max-content;
|
|
566
634
|
justify-self: end;
|
|
@@ -572,6 +640,23 @@
|
|
|
572
640
|
padding-left: var(--spacing-md);
|
|
573
641
|
margin-left: var(--spacing-sm);
|
|
574
642
|
|
|
643
|
+
/* A count pill on an icon chip sits on the chip's shoulder, not inline —
|
|
644
|
+
these are 36px targets and an inline pill would push the icon off
|
|
645
|
+
centre. The anchor is the chip itself, and the pill stays INSIDE its
|
|
646
|
+
box: the chip row is a scroll container (see the overflow comment
|
|
647
|
+
below), and a scroll container clips both axes — a pill overhanging
|
|
648
|
+
the edge by 2px rendered as a numberless green sliver. */
|
|
649
|
+
> ul > li > a {
|
|
650
|
+
position: relative;
|
|
651
|
+
|
|
652
|
+
.nav-badge {
|
|
653
|
+
position: absolute;
|
|
654
|
+
top: 1px;
|
|
655
|
+
right: 1px;
|
|
656
|
+
margin: 0;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
575
660
|
> ul {
|
|
576
661
|
list-style: none;
|
|
577
662
|
display: flex;
|
|
@@ -1079,8 +1164,20 @@
|
|
|
1079
1164
|
opacity: 1;
|
|
1080
1165
|
}
|
|
1081
1166
|
|
|
1082
|
-
|
|
1167
|
+
/* The group heading — an item with children and no page
|
|
1168
|
+
of its own. It used to be a <span>; it is a <button>
|
|
1169
|
+
now so it can take focus (see Navigation.jsx), and the
|
|
1170
|
+
button has to keep the span's rendering exactly, hence
|
|
1171
|
+
the reset of the three properties a UA stylesheet would
|
|
1172
|
+
otherwise force on it. */
|
|
1173
|
+
span,
|
|
1174
|
+
> button {
|
|
1083
1175
|
display: block;
|
|
1176
|
+
width: 100%;
|
|
1177
|
+
text-align: left;
|
|
1178
|
+
border: none;
|
|
1179
|
+
font: inherit;
|
|
1180
|
+
cursor: pointer;
|
|
1084
1181
|
color: rgba(var(--paragraph-rgb, 43, 43, 43), 0.3);
|
|
1085
1182
|
box-sizing: border-box;
|
|
1086
1183
|
padding: 1.25em 0.85em;
|
|
@@ -788,15 +788,188 @@ body.tablet-mode .InovuaReactDataGrid__cell.cell-word-wrap > div {
|
|
|
788
788
|
theme selector the invisible rule uses, so it lands exactly where that rule
|
|
789
789
|
lands and nowhere else.
|
|
790
790
|
|
|
791
|
-
`--primary-color` rather than one of the `--dgx-*` tokens above
|
|
792
|
-
|
|
793
|
-
|
|
791
|
+
`--primary-color` rather than one of the `--dgx-*` tokens above, because
|
|
792
|
+
this selector is the vendored theme's, unscoped to the grid, and so also
|
|
793
|
+
matches a combo box that genuinely is outside `.InovuaReactDataGrid` (the
|
|
794
|
+
toolkit's `relativeToViewport` overlay), where a `--dgx-*` token would not
|
|
795
|
+
resolve. Inside a grid filter — which is where the list actually mounts,
|
|
796
|
+
see the block below — either would have worked. */
|
|
794
797
|
.inovua-react-toolkit-combo-box__list--theme-default-light
|
|
795
798
|
.inovua-react-toolkit-combo-box__list__item--active {
|
|
796
799
|
border-color: color-mix(in srgb, var(--primary-color, #1b3933) 55%, transparent) !important;
|
|
797
800
|
background: color-mix(in srgb, var(--primary-color, #1b3933) 12%, transparent) !important;
|
|
798
801
|
}
|
|
799
802
|
|
|
803
|
+
/* ==========================================================================
|
|
804
|
+
The dropdown filter's OPTION MENU
|
|
805
|
+
--------------------------------------------------------------------------
|
|
806
|
+
Two complaints, one rule set.
|
|
807
|
+
|
|
808
|
+
1. The options were being cut off. "Concrete, Concrete Precast & Concrete
|
|
809
|
+
Pumping" arrived as "CONCRETE, CONCRETE PRECAS…" and "General Building +
|
|
810
|
+
Construction Management" as "GENERAL BUILDING + CONSTR…", which makes two
|
|
811
|
+
different trades indistinguishable in the list you are choosing from.
|
|
812
|
+
|
|
813
|
+
That is not a text-length problem, it is a width problem. The vendored
|
|
814
|
+
base stylesheet pins the menu to the FIELD:
|
|
815
|
+
|
|
816
|
+
.inovua-react-toolkit-combo-box__list:not(…--relative-to-viewport) {
|
|
817
|
+
left: -1px; right: -1px; min-width: 100%; position: absolute;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
Both edges anchored to a ~104px column means the menu can only ever be
|
|
821
|
+
as wide as the column, and every item carries
|
|
822
|
+
`…__list__item--ellipsis { overflow:hidden; white-space:nowrap;
|
|
823
|
+
text-overflow:ellipsis }`, so the text is trimmed to fit. Measured in a
|
|
824
|
+
real grid: item scrollWidth 355px inside clientWidth 102px.
|
|
825
|
+
|
|
826
|
+
Releasing the right edge and sizing to `max-content` lets the menu grow
|
|
827
|
+
to its widest option; `min-width` keeps it at least as wide as the field
|
|
828
|
+
it drops out of, and the cap keeps a pathological option (a 300-character
|
|
829
|
+
description) from painting a menu across the whole screen. Options longer
|
|
830
|
+
than the cap still ellipsize — that is the cap doing its job, not the bug
|
|
831
|
+
above.
|
|
832
|
+
|
|
833
|
+
2. The menu did not look like anything else in the library. It kept the
|
|
834
|
+
vendored theme's 14px text, 1px radius, hard `0 0 4px rgba(0,0,0,.4)`
|
|
835
|
+
shadow and indigo `rgba(121,134,203,…)` highlights, while the grid around
|
|
836
|
+
it is 13px `--dgx-ink` on 6-8px radii and the app's own dropdowns
|
|
837
|
+
(react-select, `.visns-select__menu` in global.css) are a hairline
|
|
838
|
+
surface with `primary 8%` on the focused option. So the values below are
|
|
839
|
+
lifted from those two places rather than invented: the option size sits
|
|
840
|
+
on the grid's own type scale, the highlights are the house primary, and
|
|
841
|
+
the surface is the same popover treatment.
|
|
842
|
+
|
|
843
|
+
Scoping. The menu is NOT portalled — verified in a rendered grid, its parent
|
|
844
|
+
chain is `…__list > .inovua-react-toolkit-combo-box.InovuaReactDataGrid__column-header__filter
|
|
845
|
+
> …__filter-wrapper > …__column-header > … > .InovuaReactDataGrid`. It is a
|
|
846
|
+
direct child of the combo, which is exactly why `>` off the grid's own
|
|
847
|
+
filter class is enough to reach it and nothing else: a combo box anywhere
|
|
848
|
+
else in the app (the pagination toolbar's page-size picker, for one) carries
|
|
849
|
+
no `InovuaReactDataGrid__column-header__filter` and is untouched.
|
|
850
|
+
|
|
851
|
+
Because it is inside the grid element, the `--dgx-*` tokens declared on
|
|
852
|
+
`.InovuaReactDataGrid` DO resolve here (measured: `--dgx-ink` computes to
|
|
853
|
+
rgb(43,43,43) on a node appended to the menu). They are still written with
|
|
854
|
+
their own fallbacks so the rules survive if a future grid ever renders the
|
|
855
|
+
list through the toolkit's `relativeToViewport` overlay.
|
|
856
|
+
|
|
857
|
+
Presentational only. Nothing here changes what is in the list, what a click
|
|
858
|
+
does, or how the filter reports its value.
|
|
859
|
+
========================================================================== */
|
|
860
|
+
|
|
861
|
+
.InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
|
|
862
|
+
> .inovua-react-toolkit-combo-box__list {
|
|
863
|
+
/* The width fix. `right: auto` is the load-bearing line — while both
|
|
864
|
+
edges are pinned the box is stretched to the column and `width` is
|
|
865
|
+
ignored. */
|
|
866
|
+
right: auto !important;
|
|
867
|
+
width: max-content !important;
|
|
868
|
+
min-width: calc(100% + 2px) !important;
|
|
869
|
+
|
|
870
|
+
/* Three caps, whichever bites first.
|
|
871
|
+
|
|
872
|
+
`420px` is the design cap — past that a menu stops being a list and
|
|
873
|
+
starts being a wall of text. `90vw` is the small-screen cap.
|
|
874
|
+
|
|
875
|
+
`--vs-filter-menu-avail` is the one CSS cannot work out for itself: the
|
|
876
|
+
distance from this column to the right edge of the grid. The menu is
|
|
877
|
+
absolutely positioned INSIDE `.InovuaReactDataGrid__body`, which is
|
|
878
|
+
`overflow: hidden`, so a menu on a right-hand column that grew past that
|
|
879
|
+
edge would simply be sliced off — visibly worse than the ellipsis this
|
|
880
|
+
change removes. The number is measured and written onto the combo by
|
|
881
|
+
SelectFilterEditor as the menu opens (see the comment there); when it is
|
|
882
|
+
absent — a combo outside a grid, or the editor not in play — the cap
|
|
883
|
+
falls back to `100vw` and the other two do the work. */
|
|
884
|
+
max-width: min(420px, 90vw, var(--vs-filter-menu-avail, 100vw)) !important;
|
|
885
|
+
|
|
886
|
+
/* The same popover surface as `.visns-select__menu`, on the grid's radius. */
|
|
887
|
+
padding: 4px !important;
|
|
888
|
+
border: 1px solid var(--dgx-line, rgba(43, 43, 43, 0.12)) !important;
|
|
889
|
+
border-radius: 8px !important;
|
|
890
|
+
background: var(--dgx-surface, var(--background-color, #fff)) !important;
|
|
891
|
+
box-shadow: 0 12px 28px -12px rgba(0, 0, 0, 0.28) !important;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/* An RTL combo grows the other way, or it would run off the field it belongs
|
|
895
|
+
to. The toolkit marks the direction on the combo, not on the list. */
|
|
896
|
+
.InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box.inovua-react-toolkit-combo-box--rtl
|
|
897
|
+
> .inovua-react-toolkit-combo-box__list {
|
|
898
|
+
right: -1px !important;
|
|
899
|
+
left: auto !important;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
/* One option.
|
|
903
|
+
|
|
904
|
+
`text-transform` and `letter-spacing` are declared rather than left alone
|
|
905
|
+
because the column header two rows up IS uppercase-with-tracking
|
|
906
|
+
(`…__column-header__content`), and an option list is data, not a label — it
|
|
907
|
+
must not inherit that treatment from a host stylesheet. Note this cannot
|
|
908
|
+
un-shout an option whose VALUE is stored in capitals; that is the record,
|
|
909
|
+
not the styling.
|
|
910
|
+
|
|
911
|
+
`border` is deliberately absent: the theme gives every item
|
|
912
|
+
`border: 1px solid transparent` and the keyboard-highlight rule above
|
|
913
|
+
colours it, so declaring a border here would silently kill arrow-key
|
|
914
|
+
navigation's only visual. The radius is set so that highlight is rounded. */
|
|
915
|
+
.InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
|
|
916
|
+
> .inovua-react-toolkit-combo-box__list
|
|
917
|
+
.inovua-react-toolkit-combo-box__list__item {
|
|
918
|
+
padding: 0.375rem 0.625rem !important;
|
|
919
|
+
border-radius: 6px !important;
|
|
920
|
+
color: var(--dgx-ink, var(--paragraph-color, #2b2b2b)) !important;
|
|
921
|
+
font-size: 0.8125rem !important;
|
|
922
|
+
font-weight: 400 !important;
|
|
923
|
+
line-height: 1.45 !important;
|
|
924
|
+
letter-spacing: normal !important;
|
|
925
|
+
text-transform: none !important;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
.InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
|
|
929
|
+
> .inovua-react-toolkit-combo-box__list
|
|
930
|
+
.inovua-react-toolkit-combo-box__list__item:not(.inovua-react-toolkit-combo-box__list__item--disabled):hover {
|
|
931
|
+
background: color-mix(in srgb, var(--primary-color, #1b3933) 8%, transparent) !important;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
.InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
|
|
935
|
+
> .inovua-react-toolkit-combo-box__list
|
|
936
|
+
.inovua-react-toolkit-combo-box__list__item--selected {
|
|
937
|
+
background: color-mix(in srgb, var(--primary-color, #1b3933) 16%, transparent) !important;
|
|
938
|
+
font-weight: 600 !important;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/* Group headings and the "no options" line are the menu's own chrome, so they
|
|
942
|
+
take the quiet label treatment the column headers use rather than looking
|
|
943
|
+
like options you could pick. */
|
|
944
|
+
.InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
|
|
945
|
+
> .inovua-react-toolkit-combo-box__list
|
|
946
|
+
.inovua-react-toolkit-combo-box__list__group,
|
|
947
|
+
.InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
|
|
948
|
+
> .inovua-react-toolkit-combo-box__list
|
|
949
|
+
.inovua-react-toolkit-combo-box__list__empty-text {
|
|
950
|
+
padding: 0.375rem 0.625rem !important;
|
|
951
|
+
color: var(--dgx-muted, rgba(43, 43, 43, 0.6)) !important;
|
|
952
|
+
font-size: 0.6875rem !important;
|
|
953
|
+
font-weight: 600 !important;
|
|
954
|
+
letter-spacing: 0.05em !important;
|
|
955
|
+
text-transform: uppercase !important;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/* The field itself was 14px while the text, numeric and date filters beside it
|
|
959
|
+
are 0.75rem (the rule further up). The combo's displayed value and
|
|
960
|
+
placeholder are SPANS, not the `<input>` that rule reaches, which is how one
|
|
961
|
+
control in the filter row ended up a size larger than its neighbours. */
|
|
962
|
+
.InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box,
|
|
963
|
+
.InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
|
|
964
|
+
.inovua-react-toolkit-combo-box__value__display-value,
|
|
965
|
+
.InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
|
|
966
|
+
.inovua-react-toolkit-combo-box__input__placeholder,
|
|
967
|
+
.InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
|
|
968
|
+
.inovua-react-toolkit-combo-box__value__tag__label {
|
|
969
|
+
font-size: 0.75rem !important;
|
|
970
|
+
letter-spacing: normal !important;
|
|
971
|
+
}
|
|
972
|
+
|
|
800
973
|
/* The native date filter is the odd one out in the row.
|
|
801
974
|
|
|
802
975
|
Measured on /tickets: every other filter editor is 28px tall with a 6px
|
|
@@ -39,16 +39,6 @@ select {
|
|
|
39
39
|
font: inherit;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/* Floor under every textarea: the browser default (`rows` omitted) is two
|
|
43
|
-
lines, which renders a multi-line field as a slit. Three lines of text
|
|
44
|
-
plus the field's own padding is the least a free-text box can be and
|
|
45
|
-
still read as one. A bare element selector, so any component that sets
|
|
46
|
-
its own height with a class still wins. */
|
|
47
|
-
textarea {
|
|
48
|
-
min-height: 5.5em; /* ~3 lines + padding, for browsers without lh */
|
|
49
|
-
min-height: calc(3lh + 2rem);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
42
|
/* Global radio button styling */
|
|
53
43
|
input[type='radio'],
|
|
54
44
|
.radio-input {
|
|
@@ -295,7 +285,22 @@ input[type]:not([type='search']):not([type='url']):not([type='hidden']):not(
|
|
|
295
285
|
border-radius: var(--br);
|
|
296
286
|
border: 1px solid rgba(var(--paragraph-color-rgb, 43, 43, 43), 0.15);
|
|
297
287
|
box-shadow: none;
|
|
298
|
-
|
|
288
|
+
/* THE height of a form control, stated once. It used to be an accident of
|
|
289
|
+
`padding: 1rem` plus a 0.85rem line box — which computed to 50px on a
|
|
290
|
+
text input, 42.78px on a `<select>` (a CSS-module rule beat this one on
|
|
291
|
+
padding for the bare `select` selector but not for the twenty-clause
|
|
292
|
+
`input` chain), 34px on a date input and 51px on react-select, four
|
|
293
|
+
heights in one two-column form. Now the box is the token and the padding
|
|
294
|
+
is horizontal only, so every control below lands on the same number and
|
|
295
|
+
a consumer moves all of them together by setting `--field-height` on any
|
|
296
|
+
ancestor.
|
|
297
|
+
|
|
298
|
+
The 50px default is not a round number picked for looks: it is what a
|
|
299
|
+
plain text input measured under the old `padding: 1rem` rule, so a
|
|
300
|
+
project that never heard of this token renders exactly as it did. */
|
|
301
|
+
height: var(--field-height, 50px);
|
|
302
|
+
min-height: var(--field-height, 50px);
|
|
303
|
+
padding: 0 1rem;
|
|
299
304
|
outline: none;
|
|
300
305
|
box-sizing: border-box;
|
|
301
306
|
color: var(--paragraph-color);
|
|
@@ -309,33 +314,58 @@ input[type]:not([type='search']):not([type='url']):not([type='hidden']):not(
|
|
|
309
314
|
}
|
|
310
315
|
}
|
|
311
316
|
|
|
312
|
-
/*
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
317
|
+
/* A textarea is multi-line by definition, so it is the one control that opts
|
|
318
|
+
out of the fixed height — it keeps the font and the horizontal padding, and
|
|
319
|
+
takes the vertical padding that puts its FIRST row on the same line as the
|
|
320
|
+
single-line fields beside it. Written twice: the `lh` unit is what makes the
|
|
321
|
+
centring exact, and the literal is for browsers without it. No guard against
|
|
322
|
+
a `--field-height` shorter than one line is needed — CSS clamps a negative
|
|
323
|
+
`calc()` result for padding to zero rather than dropping the declaration.
|
|
324
|
+
|
|
325
|
+
The `min-height` is a floor: the browser default (`rows` omitted) is two
|
|
326
|
+
lines, which renders a free-text box as a slit. */
|
|
327
|
+
textarea {
|
|
328
|
+
height: auto;
|
|
329
|
+
padding: 0.85rem 1rem;
|
|
330
|
+
padding: calc((var(--field-height, 50px) - 1lh) / 2 - 1px) 1rem;
|
|
331
|
+
min-height: 5.5em; /* ~3 lines + padding, for browsers without lh */
|
|
332
|
+
min-height: calc(3lh + 2rem);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/* Native date / time inputs are excluded from the chained rule above — the
|
|
336
|
+
`:not([type='date'])` clauses are in it — so they restate the same box here
|
|
337
|
+
rather than inheriting it. They used to be pinned to a flat 34px, which is
|
|
338
|
+
the grid filter row's height and 16px short of every other field in a form.
|
|
339
|
+
|
|
340
|
+
The grid's own filter input keeps its opt-out: it lives inside a 32px
|
|
341
|
+
toolbar row that `global-datagrid.css` sizes, not inside a form. */
|
|
342
|
+
input[type='date']:not(.visns-native-date-filter__input),
|
|
343
|
+
input[type='datetime-local']:not(.visns-native-date-filter__input),
|
|
344
|
+
input[type='time']:not(.visns-native-date-filter__input) {
|
|
318
345
|
box-sizing: border-box;
|
|
319
|
-
height:
|
|
320
|
-
min-height:
|
|
321
|
-
padding: 0
|
|
346
|
+
height: var(--field-height, 50px);
|
|
347
|
+
min-height: var(--field-height, 50px);
|
|
348
|
+
padding: 0 1rem;
|
|
322
349
|
border: 1px solid rgba(var(--paragraph-color-rgb, 31, 31, 31), 0.15);
|
|
323
350
|
border-radius: var(--br, 4px);
|
|
324
351
|
background: #fff;
|
|
325
352
|
color: var(--paragraph-color, #1f1f1f);
|
|
326
353
|
font-family: inherit;
|
|
327
354
|
font-size: 0.85rem;
|
|
328
|
-
|
|
355
|
+
/* Was a hardcoded 32px, one pixel under the old 34px box. A number tied to
|
|
356
|
+
a height this rule no longer states has to go; the value is centred by
|
|
357
|
+
`Form.module.scss`, which flexes these inputs. */
|
|
358
|
+
line-height: normal;
|
|
329
359
|
font-variant-numeric: tabular-nums;
|
|
330
360
|
}
|
|
331
361
|
|
|
332
362
|
@-moz-document url-prefix() {
|
|
333
|
-
input[type='date'],
|
|
334
|
-
input[type='datetime-local'],
|
|
335
|
-
input[type='time'] {
|
|
336
|
-
height:
|
|
337
|
-
padding: 0
|
|
338
|
-
line-height:
|
|
363
|
+
input[type='date']:not(.visns-native-date-filter__input),
|
|
364
|
+
input[type='datetime-local']:not(.visns-native-date-filter__input),
|
|
365
|
+
input[type='time']:not(.visns-native-date-filter__input) {
|
|
366
|
+
height: var(--field-height, 50px);
|
|
367
|
+
padding: 0 1rem;
|
|
368
|
+
line-height: normal;
|
|
339
369
|
}
|
|
340
370
|
}
|
|
341
371
|
|
|
@@ -1052,10 +1082,13 @@ form div:has(.react-toggle) input[type='checkbox'] {
|
|
|
1052
1082
|
------------------------------------------------------------------------ */
|
|
1053
1083
|
|
|
1054
1084
|
.visns-select__control {
|
|
1055
|
-
/*
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1085
|
+
/* The same token every native control above is now sized from, and the
|
|
1086
|
+
same 50px default: react-select renders divs, so the chained `input[type]`
|
|
1087
|
+
rule never reached it and its height was a separate number (51px) that
|
|
1088
|
+
only happened to be close. `min-height` and not `height`, because a
|
|
1089
|
+
multi-select has to grow as chips wrap — with one value it is exactly a
|
|
1090
|
+
text input, with three it is taller than one on purpose. */
|
|
1091
|
+
min-height: var(--field-height, 50px);
|
|
1059
1092
|
border: 1px solid rgba(var(--paragraph-rgb, 43, 43, 43), 0.15) !important;
|
|
1060
1093
|
border-radius: var(--br, 5px) !important;
|
|
1061
1094
|
background: var(--background-color, #fff);
|