@visns-studio/visns-components 6.32.3 → 6.34.3
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/README.md +483 -0
- package/package.json +1 -1
- package/src/components/DataGrid.jsx +44 -2
- package/src/components/Field.jsx +32 -0
- package/src/components/Form.jsx +39 -2
- package/src/components/Navigation.jsx +274 -15
- package/src/components/Notification.jsx +13 -2
- package/src/components/TableFilter.jsx +14 -2
- package/src/components/auth/Login.jsx +11 -4
- package/src/components/auth/PasskeyEnrolPrompt.jsx +269 -0
- package/src/components/auth/Profile.jsx +422 -26
- package/src/components/auth/Reset.jsx +1 -1
- package/src/components/auth/TwoFactorAuth.jsx +1 -1
- package/src/components/auth/Verify.jsx +2 -2
- package/src/components/auth/authEndpoints.js +43 -0
- package/src/components/auth/passkeyClient.js +102 -0
- package/src/components/auth/passkeyPrompt.js +408 -0
- package/src/components/auth/profileLayout.js +45 -0
- package/src/components/controls/DataGridSearch.jsx +16 -2
- package/src/components/controls/DataGridSortSheet.jsx +2 -0
- package/src/components/emailCampaigns/BlockEditor.jsx +486 -0
- package/src/components/emailCampaigns/CampaignEditor.jsx +614 -0
- package/src/components/emailCampaigns/CampaignReport.jsx +98 -0
- package/src/components/emailCampaigns/EmailCampaigns.jsx +422 -0
- package/src/components/emailCampaigns/EmailLists.jsx +505 -0
- package/src/components/emailCampaigns/emailCampaignApi.js +135 -0
- package/src/components/generic/ActionButtons.jsx +1 -1
- package/src/components/generic/GenericAuth.jsx +197 -21
- package/src/components/generic/GenericDetail.jsx +95 -3
- package/src/components/generic/GenericMain.jsx +7 -0
- package/src/components/generic/StandardModal.jsx +71 -158
- package/src/components/sms/SmsCampaigns.jsx +2517 -0
- package/src/components/sms/SmsInbox.jsx +20 -5
- package/src/components/sms/SmsLineSettings.jsx +2 -5
- package/src/components/sms/SmsThreadPanel.jsx +20 -0
- package/src/components/sms/smsEndpoints.js +23 -0
- package/src/components/styles/DataGrid.module.scss +22 -0
- package/src/components/styles/EmailCampaigns.module.scss +922 -0
- package/src/components/styles/Form.module.scss +100 -2
- package/src/components/styles/Navigation.module.scss +322 -5
- package/src/components/styles/Notification.module.scss +140 -8
- package/src/components/styles/PasskeyEnrolPrompt.module.scss +105 -0
- package/src/components/styles/Profile.module.scss +157 -0
- package/src/components/styles/Sms.module.scss +484 -4
- package/src/components/styles/StandardModal.module.scss +273 -0
- package/src/components/styles/Vault.module.scss +5 -3
- package/src/components/styles/global.css +38 -7
- package/src/components/utils/navCollapsed.js +201 -0
- package/src/components/utils/rowActionSettings.js +76 -0
- package/src/components/utils/usePasskeysEnabled.js +84 -0
- package/src/components/utils/useShellLayout.js +131 -0
- package/src/index.js +107 -1
- package/src/utils/rememberTab.js +70 -0
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, {
|
|
2
|
+
useEffect,
|
|
3
|
+
useInsertionEffect,
|
|
4
|
+
useRef,
|
|
5
|
+
useState,
|
|
6
|
+
useCallback,
|
|
7
|
+
} from 'react';
|
|
2
8
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
|
3
9
|
import {
|
|
4
10
|
AudioLines,
|
|
@@ -26,6 +32,8 @@ import {
|
|
|
26
32
|
Lock,
|
|
27
33
|
MessageSquare,
|
|
28
34
|
MessagesSquare,
|
|
35
|
+
PanelLeftClose,
|
|
36
|
+
PanelLeftOpen,
|
|
29
37
|
TrendingUp,
|
|
30
38
|
} from 'lucide-react';
|
|
31
39
|
import CustomFetch from './Fetch';
|
|
@@ -40,6 +48,13 @@ import {
|
|
|
40
48
|
DEFAULT_BADGE_TONE,
|
|
41
49
|
} from './navBadges';
|
|
42
50
|
import { drawerRows } from './navDrawer';
|
|
51
|
+
// The collapsed-rail preference: remembered in localStorage, published on
|
|
52
|
+
// <html> as `data-nav-collapsed`. See utils/navCollapsed.js.
|
|
53
|
+
import {
|
|
54
|
+
applyNavCollapsed,
|
|
55
|
+
getStoredNavCollapsed,
|
|
56
|
+
storeNavCollapsed,
|
|
57
|
+
} from './utils/navCollapsed';
|
|
43
58
|
import styles from './styles/Navigation.module.scss';
|
|
44
59
|
|
|
45
60
|
/**
|
|
@@ -444,6 +459,16 @@ function Navigation({
|
|
|
444
459
|
* shared bar has no way to know any of it.
|
|
445
460
|
*/
|
|
446
461
|
badges = {},
|
|
462
|
+
/*
|
|
463
|
+
* The square brand mark for the collapsed rail, if the project has one.
|
|
464
|
+
*
|
|
465
|
+
* `logo` is a wide lock-up — a mark plus a wordmark — and there is no room
|
|
466
|
+
* for a wordmark in a 64px strip. Passing nothing is fine and is the
|
|
467
|
+
* default: the logo plate then holds the collapse toggle alone, which
|
|
468
|
+
* reads as deliberate in a way a wordmark cropped to its first two letters
|
|
469
|
+
* does not.
|
|
470
|
+
*/
|
|
471
|
+
collapsedLogo,
|
|
447
472
|
}) {
|
|
448
473
|
const [search, setSearch] = useState('');
|
|
449
474
|
const [searchResultShow, setSearchResultShow] = useState(false);
|
|
@@ -463,6 +488,65 @@ function Navigation({
|
|
|
463
488
|
const isNarrow = useMediaQuery(DRAWER_BREAKPOINT);
|
|
464
489
|
const isCoarsePointer = useMediaQuery(COARSE_POINTER);
|
|
465
490
|
|
|
491
|
+
/*
|
|
492
|
+
* THE COLLAPSED RAIL.
|
|
493
|
+
*
|
|
494
|
+
* Only the sidebar layouts have a rail to collapse; the header bar is
|
|
495
|
+
* untouched by every line below, and writes no attribute.
|
|
496
|
+
*
|
|
497
|
+
* The initial value is read from localStorage in a LAZY INITIALISER, not
|
|
498
|
+
* in an effect. An effect runs after the first paint, so the rail would
|
|
499
|
+
* render 216px wide, the content would lay out beside it, and both would
|
|
500
|
+
* then jump — which is exactly the flash a remembered layout preference
|
|
501
|
+
* exists to avoid.
|
|
502
|
+
*/
|
|
503
|
+
const isRailLayout = layout === 'simple' || layout === 'cms';
|
|
504
|
+
const [collapsed, setCollapsed] = useState(() =>
|
|
505
|
+
isRailLayout ? getStoredNavCollapsed() : false
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
/*
|
|
509
|
+
* Below 1024px the rail IS the drawer — a panel that slides over the page
|
|
510
|
+
* and is dismissed by tapping the backdrop — so there is nothing to
|
|
511
|
+
* collapse and nothing the extra width is being taken from. The state is
|
|
512
|
+
* kept (widening the window restores what the user chose); it simply does
|
|
513
|
+
* not apply.
|
|
514
|
+
*/
|
|
515
|
+
const railCollapsed = isRailLayout && collapsed && !isNarrow;
|
|
516
|
+
|
|
517
|
+
/*
|
|
518
|
+
* Publish it on <html>, the way GenericAuth publishes `data-layout` and
|
|
519
|
+
* `data-density` — so the rail's own stylesheet, and a consuming app's,
|
|
520
|
+
* key off one attribute rather than off a class threaded through the tree.
|
|
521
|
+
* `useInsertionEffect` so the width is in place before the first paint.
|
|
522
|
+
*/
|
|
523
|
+
useInsertionEffect(() => {
|
|
524
|
+
if (!isRailLayout) {
|
|
525
|
+
return undefined;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
applyNavCollapsed(railCollapsed);
|
|
529
|
+
|
|
530
|
+
return () => applyNavCollapsed(false);
|
|
531
|
+
}, [isRailLayout, railCollapsed]);
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Collapse, or expand. The preference is remembered against the browser,
|
|
535
|
+
* not the account: it is about this screen and this window.
|
|
536
|
+
*/
|
|
537
|
+
/** The toggle is an icon in both states, so it is named in both. */
|
|
538
|
+
const collapseLabel = railCollapsed ? 'Expand menu' : 'Collapse menu';
|
|
539
|
+
|
|
540
|
+
const toggleCollapsed = useCallback(() => {
|
|
541
|
+
setCollapsed((previous) => {
|
|
542
|
+
const next = !previous;
|
|
543
|
+
|
|
544
|
+
storeNavCollapsed(next);
|
|
545
|
+
|
|
546
|
+
return next;
|
|
547
|
+
});
|
|
548
|
+
}, []);
|
|
549
|
+
|
|
466
550
|
// The mobile drawer is a full-viewport fixed overlay; without a scroll
|
|
467
551
|
// lock the page behind it keeps scrolling under the user's thumb.
|
|
468
552
|
useEffect(() => {
|
|
@@ -675,6 +759,36 @@ function Navigation({
|
|
|
675
759
|
const labelVar = { '--nav-action-label': JSON.stringify(label) };
|
|
676
760
|
const IconComponent = SETTING_ICONS[n.icon];
|
|
677
761
|
|
|
762
|
+
/*
|
|
763
|
+
* The same tooltip the header chips carry, placed to the RIGHT of the
|
|
764
|
+
* rail and drawn only while the rail is collapsed — see the note in
|
|
765
|
+
* `renderNav`, which does this for the nav rows above these. Profile
|
|
766
|
+
* and Logout now behave like the bell, which was the only labelled
|
|
767
|
+
* thing in the rail. `actionLabel` already falls back to the humanised
|
|
768
|
+
* id, so a settings entry with no label of its own still says
|
|
769
|
+
* something.
|
|
770
|
+
*/
|
|
771
|
+
const railTooltip =
|
|
772
|
+
railCollapsed && label
|
|
773
|
+
? {
|
|
774
|
+
'data-tooltip-id': 'system-tooltip',
|
|
775
|
+
'data-tooltip-place': 'right',
|
|
776
|
+
'data-tooltip-content': label,
|
|
777
|
+
}
|
|
778
|
+
: {};
|
|
779
|
+
|
|
780
|
+
// Same fallback the nav rows use: an action with no icon would
|
|
781
|
+
// collapse to an empty square, so it shows its initial instead.
|
|
782
|
+
const railInitial =
|
|
783
|
+
railCollapsed && !IconComponent && label
|
|
784
|
+
? String(label).trim().charAt(0).toUpperCase()
|
|
785
|
+
: '';
|
|
786
|
+
const railInitialMark = railInitial ? (
|
|
787
|
+
<span className={styles['nav-initial']} aria-hidden="true">
|
|
788
|
+
{railInitial}
|
|
789
|
+
</span>
|
|
790
|
+
) : null;
|
|
791
|
+
|
|
678
792
|
// Same slot as the header's, publishing the row label the same way a
|
|
679
793
|
// mounted component gets one (see the note above renderRailSetting).
|
|
680
794
|
if (typeof renderers[n.id] === 'function') {
|
|
@@ -692,7 +806,14 @@ function Navigation({
|
|
|
692
806
|
if (n.id === 'notifications') {
|
|
693
807
|
return (
|
|
694
808
|
<li style={labelVar}>
|
|
695
|
-
|
|
809
|
+
{/* The bell draws its own trigger, so the placement is
|
|
810
|
+
the one thing it has to be told: its tooltip is
|
|
811
|
+
hard-placed below the chip for the header bar, and
|
|
812
|
+
below a row in a rail is on top of the next row. */}
|
|
813
|
+
<Notification
|
|
814
|
+
setSystemAuth={setSystemAuth}
|
|
815
|
+
tooltipPlace="right"
|
|
816
|
+
/>
|
|
696
817
|
</li>
|
|
697
818
|
);
|
|
698
819
|
} else if (n.id === 'businessCardOcr') {
|
|
@@ -702,9 +823,11 @@ function Navigation({
|
|
|
702
823
|
onClick={() => setShowBusinessCardOcr(true)}
|
|
703
824
|
className={activeClass}
|
|
704
825
|
type="button"
|
|
826
|
+
aria-label={label}
|
|
827
|
+
{...railTooltip}
|
|
705
828
|
>
|
|
706
829
|
<CreditCard size={19} strokeWidth={2} />
|
|
707
|
-
<span>{label}</span>
|
|
830
|
+
<span className={styles['nav-label']}>{label}</span>
|
|
708
831
|
</button>
|
|
709
832
|
</li>
|
|
710
833
|
);
|
|
@@ -725,11 +848,14 @@ function Navigation({
|
|
|
725
848
|
onClick={handleLogout}
|
|
726
849
|
className={activeClass}
|
|
727
850
|
type="button"
|
|
851
|
+
aria-label={label}
|
|
852
|
+
{...railTooltip}
|
|
728
853
|
>
|
|
729
854
|
{IconComponent && (
|
|
730
855
|
<IconComponent size={19} strokeWidth={2} />
|
|
731
856
|
)}
|
|
732
|
-
|
|
857
|
+
{railInitialMark}
|
|
858
|
+
<span className={styles['nav-label']}>{label}</span>
|
|
733
859
|
</button>
|
|
734
860
|
</li>
|
|
735
861
|
);
|
|
@@ -741,6 +867,8 @@ function Navigation({
|
|
|
741
867
|
to={n.url}
|
|
742
868
|
className={`${activeClass}`}
|
|
743
869
|
target={n.target ? n.target : '_self'}
|
|
870
|
+
aria-label={label}
|
|
871
|
+
{...railTooltip}
|
|
744
872
|
// The rail is the mobile drawer below 1024px; navigating
|
|
745
873
|
// from it has to close the drawer, as the nav list does.
|
|
746
874
|
onClick={() => setOpen(false)}
|
|
@@ -748,7 +876,8 @@ function Navigation({
|
|
|
748
876
|
{IconComponent && (
|
|
749
877
|
<IconComponent size={19} strokeWidth={2} />
|
|
750
878
|
)}
|
|
751
|
-
|
|
879
|
+
{railInitialMark}
|
|
880
|
+
<span className={styles['nav-label']}>{label}</span>
|
|
752
881
|
</Link>
|
|
753
882
|
</li>
|
|
754
883
|
);
|
|
@@ -847,10 +976,42 @@ function Navigation({
|
|
|
847
976
|
|
|
848
977
|
const renderNav = (n) => {
|
|
849
978
|
let navItemClasses;
|
|
850
|
-
const NavItemIcon =
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
979
|
+
const NavItemIcon = isRailLayout
|
|
980
|
+
? NAV_ICONS[n.icon || n.id]
|
|
981
|
+
: undefined;
|
|
982
|
+
|
|
983
|
+
/*
|
|
984
|
+
* The rail's rows get the same tooltip every account chip in the
|
|
985
|
+
* header has always had — `data-tooltip-*` read by the one
|
|
986
|
+
* `<Tooltip id="system-tooltip">` GenericAuth mounts.
|
|
987
|
+
*
|
|
988
|
+
* ONLY WHILE COLLAPSED, and that is the whole point of it: an icon
|
|
989
|
+
* strip is unreadable without one, and a tooltip repeating a label
|
|
990
|
+
* that is already printed six pixels to its right is noise. The
|
|
991
|
+
* attributes are omitted rather than the tooltip disabled — react-
|
|
992
|
+
* tooltip shows nothing for an anchor with no `data-tooltip-content`.
|
|
993
|
+
*
|
|
994
|
+
* `place="right"`, not the header's "bottom": a tooltip under a row
|
|
995
|
+
* in a vertical list covers the next row, which is the one you are
|
|
996
|
+
* about to read. To the right of the rail it covers page content
|
|
997
|
+
* nobody is pointing at.
|
|
998
|
+
*
|
|
999
|
+
* No `title` attribute anywhere near this — the native tooltip and
|
|
1000
|
+
* react-tooltip both fire, half a second apart, and you get the label
|
|
1001
|
+
* twice in two different boxes.
|
|
1002
|
+
*
|
|
1003
|
+
* Spread as an object rather than written inline so the HEADER layout
|
|
1004
|
+
* renders exactly the attributes it rendered before: on `full` this
|
|
1005
|
+
* is empty and nothing is added.
|
|
1006
|
+
*/
|
|
1007
|
+
const railTooltip =
|
|
1008
|
+
railCollapsed && n.label
|
|
1009
|
+
? {
|
|
1010
|
+
'data-tooltip-id': 'system-tooltip',
|
|
1011
|
+
'data-tooltip-place': 'right',
|
|
1012
|
+
'data-tooltip-content': n.label,
|
|
1013
|
+
}
|
|
1014
|
+
: {};
|
|
854
1015
|
|
|
855
1016
|
if (n.class.includes('active')) {
|
|
856
1017
|
navItemClasses =
|
|
@@ -939,10 +1100,45 @@ function Navigation({
|
|
|
939
1100
|
*/
|
|
940
1101
|
const itemBadge = navBadge(badges, n);
|
|
941
1102
|
const ariaLabel = badgeAriaLabel(n.label, itemBadge);
|
|
1103
|
+
/*
|
|
1104
|
+
* The label, wrapped in a span ONLY WHILE THE RAIL IS COLLAPSED.
|
|
1105
|
+
*
|
|
1106
|
+
* A collapsed row shows its icon alone, and a bare text node cannot be
|
|
1107
|
+
* hidden — only deleted, which takes the row's accessible name with
|
|
1108
|
+
* it. Wrapped, it is clipped to a pixel and stays in the accessibility
|
|
1109
|
+
* tree where the screen reader and the tooltip both still find it.
|
|
1110
|
+
*
|
|
1111
|
+
* CONDITIONAL, not permanent, and that is the whole lesson of the
|
|
1112
|
+
* first attempt: this rail's own stylesheet styles `span` as a
|
|
1113
|
+
* DESCENDANT of `.nav-item` — the rule was written for the group
|
|
1114
|
+
* heading, which used to be a `<span>` — so an unconditional wrapper
|
|
1115
|
+
* turned every expanded row into a `--primary-color` block with 1.25em
|
|
1116
|
+
* of padding and squeezed the icon beside it to nothing. Emitting the
|
|
1117
|
+
* bare text node when expanded, exactly as before, means there is no
|
|
1118
|
+
* selector to audit: the expanded rail renders what it has always
|
|
1119
|
+
* rendered, in both layouts.
|
|
1120
|
+
*
|
|
1121
|
+
* An item with no icon would collapse to an empty square, so it falls
|
|
1122
|
+
* back to the first letter of its label in the icon's slot — again
|
|
1123
|
+
* only while collapsed, where it is the only thing in the row.
|
|
1124
|
+
*/
|
|
1125
|
+
const railInitial =
|
|
1126
|
+
railCollapsed && !NavItemIcon && n.label
|
|
1127
|
+
? String(n.label).trim().charAt(0).toUpperCase()
|
|
1128
|
+
: '';
|
|
942
1129
|
const contents = (
|
|
943
1130
|
<>
|
|
944
1131
|
{NavItemIcon ? <NavItemIcon size={18} /> : null}
|
|
945
|
-
{
|
|
1132
|
+
{railInitial ? (
|
|
1133
|
+
<span className={styles['nav-initial']} aria-hidden="true">
|
|
1134
|
+
{railInitial}
|
|
1135
|
+
</span>
|
|
1136
|
+
) : null}
|
|
1137
|
+
{railCollapsed ? (
|
|
1138
|
+
<span className={styles['nav-label']}>{n.label}</span>
|
|
1139
|
+
) : (
|
|
1140
|
+
n.label
|
|
1141
|
+
)}
|
|
946
1142
|
<BadgePill badge={itemBadge} />
|
|
947
1143
|
</>
|
|
948
1144
|
);
|
|
@@ -953,7 +1149,7 @@ function Navigation({
|
|
|
953
1149
|
onMouseEnter={handlePin}
|
|
954
1150
|
onFocus={handlePin}
|
|
955
1151
|
>
|
|
956
|
-
{n.url && !(hasChildren && isCoarsePointer) ? (
|
|
1152
|
+
{n.url && !(hasChildren && (isCoarsePointer || railCollapsed)) ? (
|
|
957
1153
|
// Close the mobile drawer when navigating; no-op for the
|
|
958
1154
|
// header layout where the drawer state is never opened.
|
|
959
1155
|
//
|
|
@@ -971,6 +1167,7 @@ function Navigation({
|
|
|
971
1167
|
to={n.url}
|
|
972
1168
|
aria-label={ariaLabel}
|
|
973
1169
|
onClick={() => setOpen(false)}
|
|
1170
|
+
{...railTooltip}
|
|
974
1171
|
>
|
|
975
1172
|
{contents}
|
|
976
1173
|
</Link>
|
|
@@ -1020,6 +1217,28 @@ function Navigation({
|
|
|
1020
1217
|
since a fixed panel has no position of its own until
|
|
1021
1218
|
somebody measures the item for it. */
|
|
1022
1219
|
onClick={(e) => {
|
|
1220
|
+
/*
|
|
1221
|
+
* COLLAPSED: there is nowhere for a submenu to go.
|
|
1222
|
+
* A 64px strip cannot hold a list of child labels,
|
|
1223
|
+
* and a flyout anchored to a row is a second
|
|
1224
|
+
* floating surface with its own placement,
|
|
1225
|
+
* clipping and dismissal problems. Expanding the
|
|
1226
|
+
* rail is the simple answer that cannot go wrong:
|
|
1227
|
+
* the group is then an ordinary heading in an
|
|
1228
|
+
* ordinary rail, and the next hover opens it.
|
|
1229
|
+
*
|
|
1230
|
+
* `n.url` groups take this path too while
|
|
1231
|
+
* collapsed (see the branch condition above) —
|
|
1232
|
+
* otherwise the tap would navigate to the parent
|
|
1233
|
+
* page and the children would be unreachable
|
|
1234
|
+
* without expanding the rail by hand.
|
|
1235
|
+
*/
|
|
1236
|
+
if (railCollapsed) {
|
|
1237
|
+
toggleCollapsed();
|
|
1238
|
+
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1023
1242
|
if (isCoarsePointer) {
|
|
1024
1243
|
const next = openGroup === n.id ? null : n.id;
|
|
1025
1244
|
setOpenGroup(next);
|
|
@@ -1035,11 +1254,14 @@ function Navigation({
|
|
|
1035
1254
|
e.currentTarget.blur();
|
|
1036
1255
|
}
|
|
1037
1256
|
}}
|
|
1257
|
+
{...railTooltip}
|
|
1038
1258
|
>
|
|
1039
1259
|
{contents}
|
|
1040
1260
|
</button>
|
|
1041
1261
|
) : (
|
|
1042
|
-
<span aria-label={ariaLabel}
|
|
1262
|
+
<span aria-label={ariaLabel} {...railTooltip}>
|
|
1263
|
+
{contents}
|
|
1264
|
+
</span>
|
|
1043
1265
|
)}
|
|
1044
1266
|
{renderChildren()}
|
|
1045
1267
|
</li>
|
|
@@ -1514,18 +1736,55 @@ function Navigation({
|
|
|
1514
1736
|
return layout === 'simple' || layout === 'cms' ? (
|
|
1515
1737
|
<div className={styles.cwrap}>
|
|
1516
1738
|
<aside
|
|
1739
|
+
id="nav-rail"
|
|
1517
1740
|
className={[
|
|
1518
1741
|
styles['aside--cms'],
|
|
1519
1742
|
isOpen ? styles['aside--cms--open'] : '',
|
|
1520
1743
|
actionsInSidebar ? styles['aside--cms--rail'] : '',
|
|
1744
|
+
railCollapsed ? styles['aside--cms--collapsed'] : '',
|
|
1521
1745
|
]
|
|
1522
1746
|
.filter(Boolean)
|
|
1523
1747
|
.join(' ')}
|
|
1524
1748
|
>
|
|
1525
1749
|
<div className={styles.logo}>
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1750
|
+
{/* A square mark for the 64px strip when the project has
|
|
1751
|
+
one. With none, the plate holds the collapse toggle
|
|
1752
|
+
alone — a wide lock-up cropped to its first two letters
|
|
1753
|
+
is worse than no logo, and the plate is still the
|
|
1754
|
+
brand-coloured band it always was. */}
|
|
1755
|
+
{railCollapsed && !collapsedLogo ? null : (
|
|
1756
|
+
<Link to="/">
|
|
1757
|
+
<img
|
|
1758
|
+
src={
|
|
1759
|
+
railCollapsed && collapsedLogo
|
|
1760
|
+
? collapsedLogo
|
|
1761
|
+
: logo
|
|
1762
|
+
}
|
|
1763
|
+
alt="System Logo"
|
|
1764
|
+
/>
|
|
1765
|
+
</Link>
|
|
1766
|
+
)}
|
|
1767
|
+
{/* Desktop only — below 1024px the rail is a drawer and
|
|
1768
|
+
there is no width to reclaim. Hidden in CSS as well,
|
|
1769
|
+
because `isNarrow` resolves from a media query that
|
|
1770
|
+
answers `false` for one render on a cold load. */}
|
|
1771
|
+
<button
|
|
1772
|
+
type="button"
|
|
1773
|
+
className={styles['rail-toggle']}
|
|
1774
|
+
aria-expanded={!railCollapsed}
|
|
1775
|
+
aria-controls="nav-rail"
|
|
1776
|
+
data-tooltip-id="system-tooltip"
|
|
1777
|
+
data-tooltip-place="right"
|
|
1778
|
+
data-tooltip-content={collapseLabel}
|
|
1779
|
+
aria-label={collapseLabel}
|
|
1780
|
+
onClick={toggleCollapsed}
|
|
1781
|
+
>
|
|
1782
|
+
{railCollapsed ? (
|
|
1783
|
+
<PanelLeftOpen size={18} strokeWidth={2} />
|
|
1784
|
+
) : (
|
|
1785
|
+
<PanelLeftClose size={18} strokeWidth={2} />
|
|
1786
|
+
)}
|
|
1787
|
+
</button>
|
|
1529
1788
|
</div>
|
|
1530
1789
|
<div className={styles.awrap}>
|
|
1531
1790
|
<nav className={styles.navwrap} ref={navWrapRef}>
|
|
@@ -100,7 +100,18 @@ function Notification(props) {
|
|
|
100
100
|
const [show, setShow] = useState(false);
|
|
101
101
|
const [loading, setLoading] = useState(false);
|
|
102
102
|
const [processingIds, setProcessingIds] = useState([]);
|
|
103
|
-
const {
|
|
103
|
+
const {
|
|
104
|
+
setSystemAuth,
|
|
105
|
+
echo = null,
|
|
106
|
+
channel = null,
|
|
107
|
+
/**
|
|
108
|
+
* Where the trigger's tooltip opens. 'bottom' is the header chip,
|
|
109
|
+
* which is what this has always been and what every existing mount
|
|
110
|
+
* keeps. The sidebar rail passes 'right': below a row in a vertical
|
|
111
|
+
* list is on top of the next row.
|
|
112
|
+
*/
|
|
113
|
+
tooltipPlace = 'bottom',
|
|
114
|
+
} = props;
|
|
104
115
|
|
|
105
116
|
// Live means *confirmed*: Echo hands back a channel object long before
|
|
106
117
|
// /broadcasting/auth has approved it, and treating that as live is how a
|
|
@@ -451,7 +462,7 @@ function Notification(props) {
|
|
|
451
462
|
className={styles.notificationTrigger}
|
|
452
463
|
onClick={toggleNotification}
|
|
453
464
|
data-tooltip-id="system-tooltip"
|
|
454
|
-
data-tooltip-place=
|
|
465
|
+
data-tooltip-place={tooltipPlace}
|
|
455
466
|
data-tooltip-content="Notifications"
|
|
456
467
|
aria-label="Notifications"
|
|
457
468
|
>
|
|
@@ -34,8 +34,10 @@ function TableFilter({
|
|
|
34
34
|
// `.flatFilter` block in the stylesheet), promoted to an opt-in.
|
|
35
35
|
//
|
|
36
36
|
// A vertical rail spends a fifth of the page on navigation. Detail pages
|
|
37
|
-
// now ask for the strip unconditionally (see GenericDetail);
|
|
38
|
-
//
|
|
37
|
+
// now ask for the strip unconditionally (see GenericDetail); Profile asks
|
|
38
|
+
// for it on the sidebar shells and keeps the rail on the header-bar one
|
|
39
|
+
// (see auth/profileLayout.js); index pages still get the rail unless the
|
|
40
|
+
// host says otherwise. The default here is unchanged.
|
|
39
41
|
//
|
|
40
42
|
// In this variant the strip also carries real tab semantics — tablist /
|
|
41
43
|
// tab / aria-selected, one tab stop for the whole strip, and arrow keys to
|
|
@@ -565,6 +567,14 @@ function TableFilter({
|
|
|
565
567
|
}
|
|
566
568
|
: { 'aria-current': isSelected ? 'page' : undefined };
|
|
567
569
|
|
|
570
|
+
// The strip's own tooltip. A tab's label is a noun ("Documents") and
|
|
571
|
+
// what a reader wants back from a hover is the verb — which verb it is
|
|
572
|
+
// depends on whether they are standing on it already. Both branches
|
|
573
|
+
// below carry it: a detail page's strip renders the anchor and an
|
|
574
|
+
// index page's `simple` strip the button, and one strip must not
|
|
575
|
+
// answer a hover differently depending on where it is drawn.
|
|
576
|
+
const hint = isSelected ? `You are on ${d.label}` : `Show ${d.label}`;
|
|
577
|
+
|
|
568
578
|
if (type === 'simple') {
|
|
569
579
|
return (
|
|
570
580
|
<li className={styles[d.class]}>
|
|
@@ -574,6 +584,7 @@ function TableFilter({
|
|
|
574
584
|
data-url={d.url || ''}
|
|
575
585
|
data-value={d.hasOwnProperty('value') ? d.value : ''}
|
|
576
586
|
data-active={isSelected ? 'true' : undefined}
|
|
587
|
+
title={hint}
|
|
577
588
|
onClick={filterTable}
|
|
578
589
|
{...a11y}
|
|
579
590
|
>
|
|
@@ -591,6 +602,7 @@ function TableFilter({
|
|
|
591
602
|
data-url={d.url || ''}
|
|
592
603
|
data-value={d.hasOwnProperty('value') ? d.value : ''}
|
|
593
604
|
data-active={isSelected ? 'true' : undefined}
|
|
605
|
+
title={hint}
|
|
594
606
|
onClick={filterTable}
|
|
595
607
|
className={parentClass}
|
|
596
608
|
{...a11y}
|
|
@@ -266,7 +266,14 @@ const Login = ({
|
|
|
266
266
|
// Stays pending: GenericAuth's curtain takes over from here,
|
|
267
267
|
// and a button snapping back to idle underneath it reads as
|
|
268
268
|
// "nothing happened".
|
|
269
|
-
|
|
269
|
+
//
|
|
270
|
+
// The second argument says HOW this sign-in happened.
|
|
271
|
+
// GenericAuth uses it to decide whether to offer a passkey
|
|
272
|
+
// afterwards — and offering one to somebody who just used one
|
|
273
|
+
// is the one thing that offer must never do. A host passing a
|
|
274
|
+
// plain `useState` setter here ignores the extra argument, so
|
|
275
|
+
// this is safe for every existing caller.
|
|
276
|
+
setSystemAuth(true, { method: 'passkey' });
|
|
270
277
|
}
|
|
271
278
|
} catch (error) {
|
|
272
279
|
setIsPasskeyPending(false);
|
|
@@ -383,7 +390,7 @@ const Login = ({
|
|
|
383
390
|
// profile check performs the one navigation
|
|
384
391
|
// to '/'. Navigating here as well produced a
|
|
385
392
|
// double route change and a visible flicker.
|
|
386
|
-
setSystemAuth(true);
|
|
393
|
+
setSystemAuth(true, { method: 'password' });
|
|
387
394
|
}
|
|
388
395
|
}
|
|
389
396
|
} else {
|
|
@@ -517,7 +524,7 @@ const Login = ({
|
|
|
517
524
|
value={auth.email}
|
|
518
525
|
onChange={handleChange}
|
|
519
526
|
tabIndex="1"
|
|
520
|
-
className={authClass.username}
|
|
527
|
+
className={`visns-auth-input ${authClass.username}`.trim()}
|
|
521
528
|
autoComplete="username"
|
|
522
529
|
placeholder=" "
|
|
523
530
|
disabled={isBusy}
|
|
@@ -533,7 +540,7 @@ const Login = ({
|
|
|
533
540
|
value={auth.password}
|
|
534
541
|
onChange={handleChange}
|
|
535
542
|
tabIndex="2"
|
|
536
|
-
className={authClass.password}
|
|
543
|
+
className={`visns-auth-input ${authClass.password}`.trim()}
|
|
537
544
|
autoComplete="current-password"
|
|
538
545
|
placeholder=" "
|
|
539
546
|
disabled={isBusy}
|