@visns-studio/visns-components 6.24.0 → 6.24.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/AsyncSelect.jsx +1 -1
- package/src/components/MultiSelect.jsx +1 -1
- package/src/components/Navigation.jsx +170 -34
- 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.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.1",
|
|
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(
|
|
@@ -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;
|
|
@@ -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);
|