@visns-studio/visns-components 6.32.3 → 6.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +470 -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 +9 -2
- package/src/components/auth/PasskeyEnrolPrompt.jsx +269 -0
- package/src/components/auth/Profile.jsx +422 -26
- 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/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 +2487 -0
- package/src/components/sms/SmsInbox.jsx +18 -0
- 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/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 +451 -0
- package/src/components/styles/StandardModal.module.scss +273 -0
- package/src/components/styles/global.css +24 -5
- 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 +103 -1
- package/src/utils/rememberTab.js +70 -0
|
@@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
|
2
2
|
import {
|
|
3
3
|
Inbox,
|
|
4
4
|
Loader2,
|
|
5
|
+
Megaphone,
|
|
5
6
|
MessageSquare,
|
|
6
7
|
PenSquare,
|
|
7
8
|
Unplug,
|
|
@@ -103,6 +104,13 @@ const SmsInboxInner = ({
|
|
|
103
104
|
echo = null,
|
|
104
105
|
channelFor = defaultChannelFor,
|
|
105
106
|
settingsUrl = '/settings/sms',
|
|
107
|
+
// Opt-in, and absent by default: a deployment with no campaigns screen
|
|
108
|
+
// must not grow a link to one. Drawn beside the settings link rather than
|
|
109
|
+
// beside "New message", because a campaign is somewhere you GO and a new
|
|
110
|
+
// message is something you DO — and it is deliberately a ghost button
|
|
111
|
+
// rather than a second primary, since the inbox's own primary action is
|
|
112
|
+
// still writing to one person.
|
|
113
|
+
campaignsUrl = null,
|
|
106
114
|
clientUrl = (id) => `/clients/${id}`,
|
|
107
115
|
// Both opt-in, both handed straight to the conversation pane — see
|
|
108
116
|
// SmsThreadPanel for the shapes. Absent, this page is exactly what it was.
|
|
@@ -301,6 +309,16 @@ const SmsInboxInner = ({
|
|
|
301
309
|
</div>
|
|
302
310
|
|
|
303
311
|
<div className={styles.headActions}>
|
|
312
|
+
{campaignsUrl && (
|
|
313
|
+
<button
|
|
314
|
+
type="button"
|
|
315
|
+
className={styles.ghostButton}
|
|
316
|
+
onClick={() => navigate(campaignsUrl)}
|
|
317
|
+
>
|
|
318
|
+
<Megaphone size={14} strokeWidth={2} aria-hidden="true" />
|
|
319
|
+
Campaigns
|
|
320
|
+
</button>
|
|
321
|
+
)}
|
|
304
322
|
{canManage && (
|
|
305
323
|
<button
|
|
306
324
|
type="button"
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
ArrowLeft,
|
|
13
13
|
CheckSquare,
|
|
14
14
|
ChevronUp,
|
|
15
|
+
CircleSlash,
|
|
15
16
|
ExternalLink,
|
|
16
17
|
FileText,
|
|
17
18
|
Link2,
|
|
@@ -1054,6 +1055,25 @@ const SmsThreadPanel = ({
|
|
|
1054
1055
|
</div>
|
|
1055
1056
|
) : (
|
|
1056
1057
|
<div className={styles.composer}>
|
|
1058
|
+
{/*
|
|
1059
|
+
* Opted out of CAMPAIGNS, which is not the same thing as
|
|
1060
|
+
* opted out of being spoken to.
|
|
1061
|
+
*
|
|
1062
|
+
* Nothing is disabled and nothing should be: this person
|
|
1063
|
+
* texted us, and refusing to let a member of staff answer a
|
|
1064
|
+
* question because a bulk send would skip them is the wrong
|
|
1065
|
+
* answer to the wrong question. What the line does is say so,
|
|
1066
|
+
* once, in the muted register — so a reply here is a decision
|
|
1067
|
+
* somebody made rather than one they made unaware.
|
|
1068
|
+
*/}
|
|
1069
|
+
{thread?.opted_out && (
|
|
1070
|
+
<p className={styles.optedOutNote}>
|
|
1071
|
+
<CircleSlash size={13} strokeWidth={2} aria-hidden="true" />
|
|
1072
|
+
This number has opted out of messages. A reply is still allowed,
|
|
1073
|
+
but Zoom blocks texts to them until they reply START.
|
|
1074
|
+
</p>
|
|
1075
|
+
)}
|
|
1076
|
+
|
|
1057
1077
|
<div className={styles.composerBox}>
|
|
1058
1078
|
<textarea
|
|
1059
1079
|
ref={composerRef}
|
|
@@ -36,6 +36,29 @@ export const makeSmsEndpoints = (base = '/ajax/sms') => ({
|
|
|
36
36
|
templates: `${base}/templates`,
|
|
37
37
|
template: (id) => `${base}/templates/${id}`,
|
|
38
38
|
|
|
39
|
+
// Bulk sends. `campaigns` is both the list and the create; everything else
|
|
40
|
+
// hangs off one campaign's id.
|
|
41
|
+
//
|
|
42
|
+
// `campaignPreview` is a POST that only reads, and it is a route rather
|
|
43
|
+
// than a client-side render on purpose: the server owns what `{first_name}`
|
|
44
|
+
// means, and a composer that filled the tokens itself would show a message
|
|
45
|
+
// that differed from the one that went out on any placeholder the two
|
|
46
|
+
// implementations disagreed about.
|
|
47
|
+
campaigns: `${base}/campaigns`,
|
|
48
|
+
campaignPreview: `${base}/campaigns/preview`,
|
|
49
|
+
campaign: (id) => `${base}/campaigns/${id}`,
|
|
50
|
+
campaignRecipients: (id) => `${base}/campaigns/${id}/recipients`,
|
|
51
|
+
campaignStart: (id) => `${base}/campaigns/${id}/start`,
|
|
52
|
+
campaignPause: (id) => `${base}/campaigns/${id}/pause`,
|
|
53
|
+
campaignCancel: (id) => `${base}/campaigns/${id}/cancel`,
|
|
54
|
+
campaignRetryFailed: (id) => `${base}/campaigns/${id}/retry-failed`,
|
|
55
|
+
|
|
56
|
+
// Numbers that must not be sent to again. Kept beside campaigns because a
|
|
57
|
+
// bulk send is the only place the list is applied in anger, but an opt-out
|
|
58
|
+
// is a fact about the number and outlives every campaign that honoured it.
|
|
59
|
+
optOuts: `${base}/opt-outs`,
|
|
60
|
+
optOut: (id) => `${base}/opt-outs/${id}`,
|
|
61
|
+
|
|
39
62
|
settingsLines: `${base}/settings/lines`,
|
|
40
63
|
settingsLine: (id) => `${base}/settings/lines/${id}`,
|
|
41
64
|
});
|
|
@@ -478,6 +478,28 @@
|
|
|
478
478
|
}
|
|
479
479
|
}
|
|
480
480
|
|
|
481
|
+
/* A row action that is PRESENT but not permitted on this row — `disabled` on a
|
|
482
|
+
settings entry (see resolveRowActionDisabled in utils/rowActionSettings.js).
|
|
483
|
+
The icon stays in the cell so the column does not change shape from row to
|
|
484
|
+
row; it simply stops being an offer.
|
|
485
|
+
|
|
486
|
+
POINTER EVENTS ARE NOT TURNED OFF, and that is deliberate rather than an
|
|
487
|
+
omission: the whole value of this over `condition` is the tooltip that says
|
|
488
|
+
why, and a glyph with `pointer-events: none` can never be hovered to show
|
|
489
|
+
one. The click is swallowed in JS instead, where it can also be stopped from
|
|
490
|
+
reaching the row underneath. */
|
|
491
|
+
.vs-action--disabled {
|
|
492
|
+
cursor: not-allowed;
|
|
493
|
+
opacity: 0.38;
|
|
494
|
+
|
|
495
|
+
/* The hover tint below would otherwise say the icon is still live. */
|
|
496
|
+
.tdaction,
|
|
497
|
+
.tdaction:hover {
|
|
498
|
+
cursor: not-allowed;
|
|
499
|
+
color: #6b7280;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
481
503
|
.uploadSection {
|
|
482
504
|
margin-bottom: 16px;
|
|
483
505
|
border-bottom: 1px solid #e5e7eb;
|
|
@@ -336,6 +336,16 @@ input[type='file'] {
|
|
|
336
336
|
justify-content: space-between;
|
|
337
337
|
gap: var(--spacing-md);
|
|
338
338
|
padding: var(--spacing-md) var(--spacing-lg);
|
|
339
|
+
/* STUCK TO THE TOP OF THE PANEL. The dialog is one scroll region now (see
|
|
340
|
+
`.modal__content` below and StandardModal.module.scss), and in a single
|
|
341
|
+
scroller a header that is not sticky is a header that scrolls away —
|
|
342
|
+
taking the close button with it, which on a phone is the only obvious
|
|
343
|
+
way out of the dialog. Opaque, or the form scrolls through it. */
|
|
344
|
+
position: sticky;
|
|
345
|
+
top: 0;
|
|
346
|
+
/* Above the form's own sticky button bar, which is 2. */
|
|
347
|
+
z-index: 3;
|
|
348
|
+
background: var(--tertiary-color);
|
|
339
349
|
|
|
340
350
|
h1 {
|
|
341
351
|
/* Was --secondary-color, the same red that means destructive
|
|
@@ -418,12 +428,25 @@ input[type='file'] {
|
|
|
418
428
|
}
|
|
419
429
|
}
|
|
420
430
|
|
|
431
|
+
/* ONE SCROLL REGION PER DIALOG, and it is the panel — not this.
|
|
432
|
+
*
|
|
433
|
+
* This used to be `max-height: 85vh; overflow-y: auto` INSIDE a panel that was
|
|
434
|
+
* also `max-height: 85vh; overflow-y: auto`, so a dialog had two nested
|
|
435
|
+
* scrollers of the same height. The form's Save bar is `position: sticky;
|
|
436
|
+
* bottom: 0`, which sticks it to the bottom of the INNER box — and the inner
|
|
437
|
+
* box starts below the header, so its bottom edge sits below the bottom of the
|
|
438
|
+
* outer one. The bar was therefore off-screen until the outer region had been
|
|
439
|
+
* scrolled as well: reach the end of the form, and Save is still not there.
|
|
440
|
+
* On a phone, where the sheet was `100vh` against a shorter visual viewport,
|
|
441
|
+
* it could not be reached at all.
|
|
442
|
+
*
|
|
443
|
+
* With the cap and the overflow gone this is ordinary content in the panel's
|
|
444
|
+
* scroller, the header above it is sticky and the button bar below it is
|
|
445
|
+
* sticky, and both stay on screen at every height. */
|
|
421
446
|
.modal__content {
|
|
422
447
|
width: 100%;
|
|
423
448
|
position: relative;
|
|
424
449
|
padding: 1rem;
|
|
425
|
-
max-height: 85vh;
|
|
426
|
-
overflow-y: auto;
|
|
427
450
|
min-height: 400px;
|
|
428
451
|
|
|
429
452
|
.btn {
|
|
@@ -675,3 +698,78 @@ input[type='file'] {
|
|
|
675
698
|
color-mix(in srgb, var(--highlight-color, #3cbf7d) 30%, transparent);
|
|
676
699
|
}
|
|
677
700
|
}
|
|
701
|
+
|
|
702
|
+
/* ============================================================================
|
|
703
|
+
* The dialog on a phone
|
|
704
|
+
* ----------------------------------------------------------------------------
|
|
705
|
+
* The panel's own shape is StandardModal.module.scss's job — full width, a
|
|
706
|
+
* bottom sheet, `100dvh`. What is left is what the panel CONTAINS, and all of
|
|
707
|
+
* it is inside a `max-width: 640px` query, so not one declaration applies to
|
|
708
|
+
* the desktop dialog.
|
|
709
|
+
*
|
|
710
|
+
* The field grid needs nothing: `.formcontainer` is a `container-type:
|
|
711
|
+
* inline-size` container and `size: "half"` / `"quarter"` already collapse to
|
|
712
|
+
* one track at 640px of CONTAINER width, which a full-width phone panel always
|
|
713
|
+
* is. See the block at the top of this file.
|
|
714
|
+
* ==========================================================================*/
|
|
715
|
+
@media (max-width: 640px) {
|
|
716
|
+
/* 400px of empty white under a two-line confirm dialog, on a screen with
|
|
717
|
+
maybe 700 to spend. The panel is bounded and scrolls; the content does
|
|
718
|
+
not need a floor. */
|
|
719
|
+
.modal__content {
|
|
720
|
+
min-height: 0;
|
|
721
|
+
padding: 0.875rem;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.modal__header {
|
|
725
|
+
padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
|
|
726
|
+
|
|
727
|
+
h1 {
|
|
728
|
+
/* One step down: a 1.125rem title beside a 44px close button wraps
|
|
729
|
+
to two lines on a 390px screen. */
|
|
730
|
+
font-size: var(--font-size-md, 1rem);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/* A 32px target is a miss. 2.75rem is the 44px Apple and Google both
|
|
735
|
+
publish, and it is the only way out of a dialog that fills the screen. */
|
|
736
|
+
.modal__close {
|
|
737
|
+
width: 2.75rem;
|
|
738
|
+
height: 2.75rem;
|
|
739
|
+
min-width: 2.75rem;
|
|
740
|
+
min-height: 2.75rem;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/* The bar keeps its full-bleed margins against the smaller padding above,
|
|
744
|
+
or it sits inset from the panel edges by the difference. */
|
|
745
|
+
.buttonContainer {
|
|
746
|
+
margin: 1rem -0.875rem -0.875rem;
|
|
747
|
+
padding: 0.75rem 0.875rem;
|
|
748
|
+
/* Both buttons reachable with one thumb; Save stays on the right. */
|
|
749
|
+
gap: 0.5rem;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/* ============================================================================
|
|
754
|
+
* iOS zooms the page when a control smaller than 16px takes focus
|
|
755
|
+
* ----------------------------------------------------------------------------
|
|
756
|
+
* Not a preference and not configurable: Safari on iPhone zooms to the focused
|
|
757
|
+
* field whenever its computed font-size is below 16px, and it does not zoom
|
|
758
|
+
* back out. The form's controls are `--font-size-sm` (13px), so every tap into
|
|
759
|
+
* a field in a dialog scaled the page up and left the user pinching back.
|
|
760
|
+
*
|
|
761
|
+
* Scoped to a coarse pointer as well as to the width, so a 640px-wide desktop
|
|
762
|
+
* window keeps the type scale it has always had — this is a fix for a touch
|
|
763
|
+
* keyboard, not for a narrow window. Scoped to the dialog because that is
|
|
764
|
+
* where the complaint is; a page form on a phone is a separate question with a
|
|
765
|
+
* separate blast radius.
|
|
766
|
+
* ==========================================================================*/
|
|
767
|
+
@media (max-width: 640px) and (pointer: coarse) {
|
|
768
|
+
.modalwrap {
|
|
769
|
+
input:not([type='checkbox']):not([type='radio']),
|
|
770
|
+
select,
|
|
771
|
+
textarea {
|
|
772
|
+
font-size: 16px;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
}
|
|
@@ -320,10 +320,22 @@
|
|
|
320
320
|
color: #fff;
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
/* THE HEADER BAR'S ICONS ONLY, and the two hiding declarations
|
|
324
|
+
have moved out of here for it — see `.hwrap .app-nav … svg` at
|
|
325
|
+
the foot of this file.
|
|
326
|
+
|
|
327
|
+
`.app-nav` is not a header class. The same list class is used by
|
|
328
|
+
the top bar and by the sidebar rail, so a bare `svg { display:
|
|
329
|
+
none }` in here reached the rail as well and hid every icon
|
|
330
|
+
`renderNav` draws for the `cms` / `simple` layouts. It was not
|
|
331
|
+
even a fight the rail could win on specificity: the rail's own
|
|
332
|
+
svg rule is five classes deep and outranks this one easily, but
|
|
333
|
+
it only ever set `position`, `right`, `opacity` and `color` — it
|
|
334
|
+
never mentioned `display`, so there was nothing for the higher
|
|
335
|
+
specificity to override and `display: none` simply applied. That
|
|
336
|
+
is the whole bug: a blank rail, expanded and collapsed alike. */
|
|
323
337
|
svg {
|
|
324
338
|
color: var(--secondary-color);
|
|
325
|
-
display: none;
|
|
326
|
-
visibility: hidden;
|
|
327
339
|
}
|
|
328
340
|
|
|
329
341
|
/* The current page: a filled pill AND the rule below. The pill
|
|
@@ -782,8 +794,14 @@
|
|
|
782
794
|
centre. The anchor is the chip itself, and the pill stays INSIDE its
|
|
783
795
|
box: the chip row is a scroll container (see the overflow comment
|
|
784
796
|
below), and a scroll container clips both axes — a pill overhanging
|
|
785
|
-
the edge by 2px rendered as a numberless green sliver.
|
|
786
|
-
|
|
797
|
+
the edge by 2px rendered as a numberless green sliver.
|
|
798
|
+
|
|
799
|
+
`:not([data-nav-overlay] *)` for the same reason every other rule in
|
|
800
|
+
this block carries it (see the long note below): a popover mounted in
|
|
801
|
+
the cluster advertises itself with `data-nav-overlay`, and a link in
|
|
802
|
+
ITS list is not a chip — this rule was pinning any `.nav-badge` inside
|
|
803
|
+
one to the corner of its row. */
|
|
804
|
+
> ul > li > a:not([data-nav-overlay] *) {
|
|
787
805
|
position: relative;
|
|
788
806
|
|
|
789
807
|
.nav-badge {
|
|
@@ -1272,6 +1290,9 @@
|
|
|
1272
1290
|
display: flex;
|
|
1273
1291
|
flex-wrap: nowrap;
|
|
1274
1292
|
align-items: center;
|
|
1293
|
+
/* Icon, then label. Only pays for itself when there is
|
|
1294
|
+
an icon: a row with one child has no gap to draw. */
|
|
1295
|
+
gap: 0.6rem;
|
|
1275
1296
|
color: var(--tertiary-color);
|
|
1276
1297
|
box-sizing: border-box;
|
|
1277
1298
|
padding: 0.5rem 1rem;
|
|
@@ -1285,11 +1306,30 @@
|
|
|
1285
1306
|
|
|
1286
1307
|
svg {
|
|
1287
1308
|
position: relative;
|
|
1288
|
-
right: 8px
|
|
1309
|
+
/* Was `right: 8px` — a nudge that made sense while
|
|
1310
|
+
these rows were text only and the occasional icon
|
|
1311
|
+
was hidden anyway. With the icon drawn it pulls the
|
|
1312
|
+
glyph into the row's left padding and out of line
|
|
1313
|
+
with the rows above and below it. The space between
|
|
1314
|
+
the icon and the label is the `gap` on the row now,
|
|
1315
|
+
which is what it should have been. */
|
|
1316
|
+
right: 0;
|
|
1289
1317
|
opacity: 1;
|
|
1290
1318
|
transition: opacity 0.65s
|
|
1291
1319
|
cubic-bezier(0.25, 0.8, 0.25, 1);
|
|
1292
1320
|
color: var(--secondary-color);
|
|
1321
|
+
/* STATED, not assumed. Something else in this file
|
|
1322
|
+
used to hide these (see the `.hwrap` note), and the
|
|
1323
|
+
reason it could is that this rule never said what
|
|
1324
|
+
`display` a rail icon has. It says so now, so the
|
|
1325
|
+
rail cannot be blanked by a rule it outranks. */
|
|
1326
|
+
display: block;
|
|
1327
|
+
visibility: visible;
|
|
1328
|
+
/* An 18px glyph in a flex row shrinks with the row
|
|
1329
|
+
unless it refuses to; a label long enough to squeeze
|
|
1330
|
+
it is a label that would otherwise take the icon's
|
|
1331
|
+
width and leave nothing. */
|
|
1332
|
+
flex: 0 0 auto;
|
|
1293
1333
|
}
|
|
1294
1334
|
|
|
1295
1335
|
&:hover a,
|
|
@@ -2469,3 +2509,280 @@ $drawer-top: var(
|
|
|
2469
2509
|
padding-right: var(--spacing-sm, 0.5rem);
|
|
2470
2510
|
}
|
|
2471
2511
|
}
|
|
2512
|
+
|
|
2513
|
+
/* ============================================================================
|
|
2514
|
+
* The collapsible rail (sidebar layouts, desktop only)
|
|
2515
|
+
* ----------------------------------------------------------------------------
|
|
2516
|
+
* The rail costs `--sidebar-width` of every page, permanently, to show a
|
|
2517
|
+
* handful of words. Collapsed, it is an icon strip and the page gets the rest
|
|
2518
|
+
* back; the tooltips the rows now carry become the labels (they are rendered
|
|
2519
|
+
* only in this state — see Navigation.jsx).
|
|
2520
|
+
*
|
|
2521
|
+
* ONE LEVER does the geometry: `--sidebar-width` is REDEFINED on the root when
|
|
2522
|
+
* the rail is collapsed. The rail's own `width`, the content's `margin-left`
|
|
2523
|
+
* and everything else in the product that already sizes itself against that
|
|
2524
|
+
* variable follow with no second rule to keep in step — and an app that has
|
|
2525
|
+
* retuned `--sidebar-width` keeps its own expanded width, because only the
|
|
2526
|
+
* collapsed value is stated here.
|
|
2527
|
+
*
|
|
2528
|
+
* --sidebar-collapsed-width the icon strip's width. default: 64px
|
|
2529
|
+
*
|
|
2530
|
+
* The state lives on `<html>` as `data-nav-collapsed="true"`, written by
|
|
2531
|
+
* Navigation from a preference remembered in localStorage (see
|
|
2532
|
+
* utils/navCollapsed.js). ABSENT IS EXPANDED, so an app that has never had the
|
|
2533
|
+
* toggle clicked matches not one selector in this block.
|
|
2534
|
+
*
|
|
2535
|
+
* EVERYTHING IS INSIDE `min-width: 1025px`. Below that the rail is the mobile
|
|
2536
|
+
* drawer — a panel that slides over the page and is dismissed by tapping the
|
|
2537
|
+
* backdrop — where there is no width to reclaim and no room for a second
|
|
2538
|
+
* dismissal mechanism. The drawer is untouched by every rule here, and the
|
|
2539
|
+
* component refuses to apply the state there as well (`railCollapsed`).
|
|
2540
|
+
* ==========================================================================*/
|
|
2541
|
+
|
|
2542
|
+
/* `.nav-label` — the label a collapsed row hides — deliberately has NO rule of
|
|
2543
|
+
its own here. On the nav rows it exists only while the rail is collapsed
|
|
2544
|
+
(Navigation.jsx emits the bare text node otherwise), and on the action rows
|
|
2545
|
+
it is the span those rows have always rendered, now merely named. Giving it
|
|
2546
|
+
so much as a `min-width` would be a declaration applying to an expanded rail
|
|
2547
|
+
that was not applying to it before. The only rules that touch it are inside
|
|
2548
|
+
the collapsed block below. */
|
|
2549
|
+
|
|
2550
|
+
/* The stand-in for a row with no icon. Rendered only while collapsed, so this
|
|
2551
|
+
never appears beside an expanded label. */
|
|
2552
|
+
.nav-initial {
|
|
2553
|
+
display: inline-flex;
|
|
2554
|
+
flex: 0 0 auto;
|
|
2555
|
+
align-items: center;
|
|
2556
|
+
justify-content: center;
|
|
2557
|
+
width: 22px;
|
|
2558
|
+
height: 22px;
|
|
2559
|
+
border-radius: 50%;
|
|
2560
|
+
background: rgba(var(--tertiary-color-rgb, 255, 255, 255), 0.14);
|
|
2561
|
+
color: var(--secondary-color);
|
|
2562
|
+
font-size: 0.75rem;
|
|
2563
|
+
font-weight: 700;
|
|
2564
|
+
line-height: 1;
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
/* Hidden by default, in both senses: the header layout has no rail, and below
|
|
2568
|
+
1024px the rail is a drawer. Only the block below turns it on. */
|
|
2569
|
+
.rail-toggle {
|
|
2570
|
+
display: none;
|
|
2571
|
+
}
|
|
2572
|
+
|
|
2573
|
+
@media (min-width: 1025px) {
|
|
2574
|
+
:root[data-nav-collapsed='true'] {
|
|
2575
|
+
--sidebar-width: var(--sidebar-collapsed-width, 64px);
|
|
2576
|
+
}
|
|
2577
|
+
|
|
2578
|
+
/* The only two properties that move, so the two move together. */
|
|
2579
|
+
.aside--cms {
|
|
2580
|
+
transition: width var(--speed, 0.18s) var(--ease, ease);
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
.content {
|
|
2584
|
+
transition: margin-left var(--speed, 0.18s) var(--ease, ease);
|
|
2585
|
+
}
|
|
2586
|
+
|
|
2587
|
+
/* The toggle sits ON the logo plate, absolutely positioned against it, so
|
|
2588
|
+
an expanded rail is laid out exactly as it was — the logo stays centred
|
|
2589
|
+
in the plate and nothing is pushed anywhere. */
|
|
2590
|
+
.aside--cms .logo .rail-toggle {
|
|
2591
|
+
display: inline-flex;
|
|
2592
|
+
align-items: center;
|
|
2593
|
+
justify-content: center;
|
|
2594
|
+
position: absolute;
|
|
2595
|
+
top: 50%;
|
|
2596
|
+
right: 4px;
|
|
2597
|
+
transform: translateY(-50%);
|
|
2598
|
+
width: 26px;
|
|
2599
|
+
height: 26px;
|
|
2600
|
+
box-sizing: border-box;
|
|
2601
|
+
margin: 0;
|
|
2602
|
+
padding: 0;
|
|
2603
|
+
border: none;
|
|
2604
|
+
border-radius: var(--br);
|
|
2605
|
+
background: transparent;
|
|
2606
|
+
/* The plate is the near-white band, so the ink is the page's, not the
|
|
2607
|
+
rail's white-on-dark. */
|
|
2608
|
+
color: var(--primary-color, #1b3933);
|
|
2609
|
+
opacity: 0.5;
|
|
2610
|
+
cursor: pointer;
|
|
2611
|
+
appearance: none;
|
|
2612
|
+
transition: opacity var(--speed, 0.18s) var(--ease, ease),
|
|
2613
|
+
background-color var(--speed, 0.18s) var(--ease, ease);
|
|
2614
|
+
}
|
|
2615
|
+
|
|
2616
|
+
.aside--cms .logo .rail-toggle:hover,
|
|
2617
|
+
.aside--cms .logo .rail-toggle:focus-visible {
|
|
2618
|
+
opacity: 1;
|
|
2619
|
+
background-color: rgba(var(--primary-rgb, 27, 57, 51), 0.08);
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
:root[data-nav-collapsed='true'] {
|
|
2623
|
+
/* With no square mark the plate holds the toggle alone (the link is
|
|
2624
|
+
not rendered), so it centres rather than hugging an edge. With one,
|
|
2625
|
+
the mark keeps clear of it. */
|
|
2626
|
+
.aside--cms .logo .rail-toggle {
|
|
2627
|
+
right: 50%;
|
|
2628
|
+
transform: translate(50%, -50%);
|
|
2629
|
+
}
|
|
2630
|
+
|
|
2631
|
+
.aside--cms .logo a {
|
|
2632
|
+
max-width: calc(100% - 26px);
|
|
2633
|
+
margin-right: 24px;
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
.aside--cms .logo a img {
|
|
2637
|
+
max-width: 100%;
|
|
2638
|
+
max-height: calc(var(--nav-logo-height, 50px) - 12px);
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
.aside--cms .logo:has(a) .rail-toggle {
|
|
2642
|
+
right: 2px;
|
|
2643
|
+
transform: translateY(-50%);
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
/* --- the nav rows ------------------------------------------------ */
|
|
2647
|
+
/* The rail's own rules are five classes deep
|
|
2648
|
+
(`.aside--cms .awrap .navwrap .app-nav .nav-item a`), so these
|
|
2649
|
+
carry the same chain rather than relying on source order to win a
|
|
2650
|
+
specificity tie. */
|
|
2651
|
+
.aside--cms .awrap .navwrap .app-nav .nav-item {
|
|
2652
|
+
padding: 0.15rem 0.35rem;
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
.aside--cms .awrap .navwrap .app-nav .nav-item > a,
|
|
2656
|
+
.aside--cms .awrap .navwrap .app-nav .nav-item > button,
|
|
2657
|
+
.aside--cms .awrap .navwrap .app-nav .nav-item > span {
|
|
2658
|
+
justify-content: center;
|
|
2659
|
+
gap: 0;
|
|
2660
|
+
padding: 0.6rem 0;
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2663
|
+
/* The rail nudges a row's icon 8px left to sit under the label's
|
|
2664
|
+
optical start. Centred in a square there is no label to sit under. */
|
|
2665
|
+
.aside--cms .awrap .navwrap .app-nav .nav-item > a > svg,
|
|
2666
|
+
.aside--cms .awrap .navwrap .app-nav .nav-item > button > svg,
|
|
2667
|
+
.aside--cms .awrap .navwrap .app-nav .nav-item > span > svg {
|
|
2668
|
+
right: 0;
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
/* A two-level menu has nowhere to open in a 64px strip; tapping a
|
|
2672
|
+
group expands the rail instead (see Navigation.jsx). */
|
|
2673
|
+
.aside--cms .navDropdown,
|
|
2674
|
+
.aside--cms .awrap .navwrap .app-nav .nav-item > ul {
|
|
2675
|
+
display: none;
|
|
2676
|
+
}
|
|
2677
|
+
|
|
2678
|
+
/* --- the pinned action rows -------------------------------------- */
|
|
2679
|
+
.rail-actions > ul > li {
|
|
2680
|
+
padding: 0.15rem 0.35rem;
|
|
2681
|
+
}
|
|
2682
|
+
|
|
2683
|
+
.rail-actions > ul > li > a,
|
|
2684
|
+
.rail-actions > ul > li > button,
|
|
2685
|
+
.rail-actions > ul > li > div > a,
|
|
2686
|
+
.rail-actions > ul > li > div > button {
|
|
2687
|
+
justify-content: center;
|
|
2688
|
+
gap: 0;
|
|
2689
|
+
padding: 0.6rem 0;
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
/* A mounted component's label is a `::after` fed by
|
|
2693
|
+
`--nav-action-label` (the account switcher, the vault palette). It
|
|
2694
|
+
is the same label the row's tooltip now carries. */
|
|
2695
|
+
.rail-actions > ul > li > div > a::after,
|
|
2696
|
+
.rail-actions > ul > li > div > button::after {
|
|
2697
|
+
display: none;
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
/* Every visible label, gone — but still in the accessibility tree.
|
|
2701
|
+
`clip-path` plus a 1px box is the treatment that survives being
|
|
2702
|
+
inside a flex row; `display: none` would take it out of the
|
|
2703
|
+
accessible name as well.
|
|
2704
|
+
|
|
2705
|
+
TWO SELECTORS, and the long one is not decoration. This rail styles
|
|
2706
|
+
`span` as a descendant of `.nav-item`, five classes deep
|
|
2707
|
+
(`.aside--cms .awrap .navwrap .app-nav .nav-item span` — the rule
|
|
2708
|
+
belongs to the group heading, which used to be a `<span>`), and it
|
|
2709
|
+
sets a background, a display and 1.25em of padding. A one-class
|
|
2710
|
+
`.nav-label` loses that cascade outright, which is a full-width
|
|
2711
|
+
`--primary-color` block where a hidden label should be. The action
|
|
2712
|
+
rows have no such rule, so the short selector is enough for them. */
|
|
2713
|
+
.nav-label {
|
|
2714
|
+
position: absolute;
|
|
2715
|
+
width: 1px;
|
|
2716
|
+
height: 1px;
|
|
2717
|
+
margin: -1px;
|
|
2718
|
+
padding: 0;
|
|
2719
|
+
overflow: hidden;
|
|
2720
|
+
clip: rect(0 0 0 0);
|
|
2721
|
+
clip-path: inset(50%);
|
|
2722
|
+
white-space: nowrap;
|
|
2723
|
+
border: 0;
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
.aside--cms .awrap .navwrap .app-nav .nav-item .nav-label {
|
|
2727
|
+
display: block;
|
|
2728
|
+
position: absolute;
|
|
2729
|
+
width: 1px;
|
|
2730
|
+
height: 1px;
|
|
2731
|
+
margin: -1px;
|
|
2732
|
+
padding: 0;
|
|
2733
|
+
overflow: hidden;
|
|
2734
|
+
clip: rect(0 0 0 0);
|
|
2735
|
+
clip-path: inset(50%);
|
|
2736
|
+
white-space: nowrap;
|
|
2737
|
+
border: 0;
|
|
2738
|
+
background: none;
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
/* Same cascade, same answer: the initial is a `<span>` inside a
|
|
2742
|
+
`.nav-item` too, so every property that makes it a circle has to be
|
|
2743
|
+
restated above that rule rather than inherited from the one-class
|
|
2744
|
+
definition at the top of this block. */
|
|
2745
|
+
.aside--cms .awrap .navwrap .app-nav .nav-item .nav-initial {
|
|
2746
|
+
display: inline-flex;
|
|
2747
|
+
position: static;
|
|
2748
|
+
flex: 0 0 auto;
|
|
2749
|
+
align-items: center;
|
|
2750
|
+
justify-content: center;
|
|
2751
|
+
width: 22px;
|
|
2752
|
+
height: 22px;
|
|
2753
|
+
margin: 0;
|
|
2754
|
+
padding: 0;
|
|
2755
|
+
border: none;
|
|
2756
|
+
border-radius: 50%;
|
|
2757
|
+
background: rgba(var(--tertiary-color-rgb, 255, 255, 255), 0.14);
|
|
2758
|
+
color: var(--secondary-color);
|
|
2759
|
+
font-size: 0.75rem;
|
|
2760
|
+
font-weight: 700;
|
|
2761
|
+
line-height: 1;
|
|
2762
|
+
text-align: center;
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
}
|
|
2766
|
+
|
|
2767
|
+
/* ============================================================================
|
|
2768
|
+
* The header bar hides its nav icons — and only the header bar
|
|
2769
|
+
* ----------------------------------------------------------------------------
|
|
2770
|
+
* This pair used to live inside `.app-nav .nav-item { svg { … } }`, which is a
|
|
2771
|
+
* class BOTH layouts use: the one-line bar across the top and the sidebar rail
|
|
2772
|
+
* down the left. So the bar's decision — the bar is a row of words, and an icon
|
|
2773
|
+
* per word at that density is noise — was silently taken for the rail as well,
|
|
2774
|
+
* where the icon is the point and, once collapsed, the only thing in the row.
|
|
2775
|
+
*
|
|
2776
|
+
* `.hwrap` is the header layout's own root element (`layout="full"`); the
|
|
2777
|
+
* sidebar layouts render `.cwrap > .aside--cms` and never match it. The
|
|
2778
|
+
* `full` layout's ≤1024px drawer is inside `.hwrap` too, but it draws its rows
|
|
2779
|
+
* through `.drawer-nav`, not `.app-nav`, so it is unaffected either way.
|
|
2780
|
+
*
|
|
2781
|
+
* Both declarations are kept exactly as they were, because the bar's rendering
|
|
2782
|
+
* is not what was wrong.
|
|
2783
|
+
* ==========================================================================*/
|
|
2784
|
+
.hwrap .app-nav .nav-item svg,
|
|
2785
|
+
.hwrap .app-nav .nav-item-alternate svg {
|
|
2786
|
+
display: none;
|
|
2787
|
+
visibility: hidden;
|
|
2788
|
+
}
|