@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.
Files changed (53) hide show
  1. package/README.md +483 -0
  2. package/package.json +1 -1
  3. package/src/components/DataGrid.jsx +44 -2
  4. package/src/components/Field.jsx +32 -0
  5. package/src/components/Form.jsx +39 -2
  6. package/src/components/Navigation.jsx +274 -15
  7. package/src/components/Notification.jsx +13 -2
  8. package/src/components/TableFilter.jsx +14 -2
  9. package/src/components/auth/Login.jsx +11 -4
  10. package/src/components/auth/PasskeyEnrolPrompt.jsx +269 -0
  11. package/src/components/auth/Profile.jsx +422 -26
  12. package/src/components/auth/Reset.jsx +1 -1
  13. package/src/components/auth/TwoFactorAuth.jsx +1 -1
  14. package/src/components/auth/Verify.jsx +2 -2
  15. package/src/components/auth/authEndpoints.js +43 -0
  16. package/src/components/auth/passkeyClient.js +102 -0
  17. package/src/components/auth/passkeyPrompt.js +408 -0
  18. package/src/components/auth/profileLayout.js +45 -0
  19. package/src/components/controls/DataGridSearch.jsx +16 -2
  20. package/src/components/controls/DataGridSortSheet.jsx +2 -0
  21. package/src/components/emailCampaigns/BlockEditor.jsx +486 -0
  22. package/src/components/emailCampaigns/CampaignEditor.jsx +614 -0
  23. package/src/components/emailCampaigns/CampaignReport.jsx +98 -0
  24. package/src/components/emailCampaigns/EmailCampaigns.jsx +422 -0
  25. package/src/components/emailCampaigns/EmailLists.jsx +505 -0
  26. package/src/components/emailCampaigns/emailCampaignApi.js +135 -0
  27. package/src/components/generic/ActionButtons.jsx +1 -1
  28. package/src/components/generic/GenericAuth.jsx +197 -21
  29. package/src/components/generic/GenericDetail.jsx +95 -3
  30. package/src/components/generic/GenericMain.jsx +7 -0
  31. package/src/components/generic/StandardModal.jsx +71 -158
  32. package/src/components/sms/SmsCampaigns.jsx +2517 -0
  33. package/src/components/sms/SmsInbox.jsx +20 -5
  34. package/src/components/sms/SmsLineSettings.jsx +2 -5
  35. package/src/components/sms/SmsThreadPanel.jsx +20 -0
  36. package/src/components/sms/smsEndpoints.js +23 -0
  37. package/src/components/styles/DataGrid.module.scss +22 -0
  38. package/src/components/styles/EmailCampaigns.module.scss +922 -0
  39. package/src/components/styles/Form.module.scss +100 -2
  40. package/src/components/styles/Navigation.module.scss +322 -5
  41. package/src/components/styles/Notification.module.scss +140 -8
  42. package/src/components/styles/PasskeyEnrolPrompt.module.scss +105 -0
  43. package/src/components/styles/Profile.module.scss +157 -0
  44. package/src/components/styles/Sms.module.scss +484 -4
  45. package/src/components/styles/StandardModal.module.scss +273 -0
  46. package/src/components/styles/Vault.module.scss +5 -3
  47. package/src/components/styles/global.css +38 -7
  48. package/src/components/utils/navCollapsed.js +201 -0
  49. package/src/components/utils/rowActionSettings.js +76 -0
  50. package/src/components/utils/usePasskeysEnabled.js +84 -0
  51. package/src/components/utils/useShellLayout.js +131 -0
  52. package/src/index.js +107 -1
  53. package/src/utils/rememberTab.js +70 -0
@@ -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
- > ul > li > a {
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
+ }
@@ -511,27 +511,123 @@
511
511
  transform: translateY(-50%);
512
512
  }
513
513
 
514
- // Beside the rail, bottom-aligned with the row that opened it.
514
+ /* Beside the rail, bottom-aligned with the row that opened it.
515
+
516
+ `top: 100%; right: 0` is a panel hanging off the bottom-right corner of
517
+ a header chip. From a row pinned to the FOOT of a full-height rail that
518
+ is the bottom-left corner of the viewport: the panel drops off the
519
+ screen and lies half on the rail, half on the page. Anchored to the
520
+ row's right edge instead, it opens over the content the way a rail's
521
+ flyout is expected to — the gap is the only thing between it and the
522
+ rail, so the two read as one surface hinged at the row.
523
+
524
+ `z-index` is stated because `.rail-actions` sits at 996 (above the
525
+ content header's sticky 995); a panel opening from a row inside it has
526
+ to clear both. */
515
527
  .dropdownWrapper {
516
528
  top: auto;
517
529
  right: auto;
518
530
  bottom: 0;
519
- left: calc(100% + 10px);
531
+ left: calc(100% + var(--nav-action-panel-gap, 10px));
520
532
  margin-top: 0;
533
+ z-index: 1200;
534
+ /* The rail's own shadow language, rather than the chip's flat drop —
535
+ this floats over the page, not over a bar. */
536
+ filter: drop-shadow(0 12px 32px rgba(0, 0, 0, 0.22));
537
+ }
538
+
539
+ /* A column, so the list is the only part that scrolls.
540
+
541
+ The box used to be as tall as its contents with a 320px scroller inside
542
+ it, which from the foot of the rail grew upward past the top of the
543
+ viewport before the scroller ever engaged. A height cap on the BOX and
544
+ `min-height: 0` on the list is what makes the header, the alerts row
545
+ and the footer button stay put while the notifications move. */
546
+ .notibox {
547
+ display: flex;
548
+ flex-direction: column;
549
+ max-height: min(70vh, 640px);
550
+ /* The radius the rail's own rows wear. */
551
+ border-radius: var(--br, 10px);
552
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.14);
553
+ }
554
+
555
+ .notiscrollwrap {
556
+ flex: 1 1 auto;
557
+ /* Without this a flex item floors at its content height and the cap
558
+ above does nothing. */
559
+ min-height: 0;
560
+ max-height: none;
561
+ }
562
+
563
+ /* The three fixed bands around it. `flex-shrink` only — every other
564
+ property they own is already `!important` in the rules above and is
565
+ deliberately left alone. */
566
+ .notiboxHeader,
567
+ .alertsRow,
568
+ .notiboxActions {
569
+ flex: 0 0 auto;
570
+ }
571
+
572
+ /* The footer button was mid-grey on a grey band: the same colour as a
573
+ disabled control, for the one action in the panel. It is the primary
574
+ button the rest of the product uses, on the project's own token. */
575
+ .notiboxActions {
576
+ background-color: transparent;
577
+ }
578
+
579
+ .viewAllBtn {
580
+ background-color: var(--primary-color, #0056b3) !important;
581
+ color: var(--tertiary-color, #fff) !important;
582
+ border: 1px solid var(--primary-color, #0056b3) !important;
583
+ border-radius: var(--br, 8px) !important;
584
+ font-weight: 600 !important;
585
+ height: 40px !important;
586
+ padding: 0 20px !important;
587
+ max-width: none !important;
588
+ box-shadow: none !important;
589
+ }
590
+
591
+ .viewAllBtn:hover:not(:disabled) {
592
+ /* No second colour token to reach for — every project sets
593
+ `--primary-color` and not all set a darker partner — so the hover
594
+ is the same ink with the page showing through it. */
595
+ background-color: color-mix(
596
+ in srgb,
597
+ var(--primary-color, #0056b3) 88%,
598
+ #000
599
+ ) !important;
600
+ border-color: color-mix(
601
+ in srgb,
602
+ var(--primary-color, #0056b3) 88%,
603
+ #000
604
+ ) !important;
605
+ box-shadow: 0 2px 8px
606
+ rgba(var(--primary-color-rgb, 0, 86, 179), 0.35) !important;
607
+ }
608
+
609
+ .viewAllBtn:focus:not(:disabled) {
610
+ box-shadow: 0 0 0 2px
611
+ rgba(var(--primary-color-rgb, 0, 86, 179), 0.35) !important;
521
612
  }
522
613
 
523
614
  @media (max-width: 1024px) {
524
- // Inside the drawer there is nothing to the right, so the panel
525
- // opens upward from the row.
615
+ /* Inside the drawer there is nothing to the right, so the panel
616
+ opens upward from the row — and takes the drawer's full width
617
+ rather than a 360px card floating inside a 300px column. */
526
618
  .dropdownWrapper {
527
619
  left: 0;
620
+ right: 0;
528
621
  bottom: calc(100% + 8px);
622
+ filter: drop-shadow(0 8px 24px rgba(0, 0, 0, 0.3));
529
623
  }
530
624
 
531
- // Keeps the panel inside a phone's viewport when the list is long —
532
- // it grows from the foot of the drawer towards the header.
533
- .notiscrollwrap {
534
- max-height: 40vh;
625
+ .notibox {
626
+ width: 100%;
627
+ max-width: none;
628
+ /* It grows from the foot of the drawer towards the header, so it
629
+ gets less of the viewport than it does beside a rail. */
630
+ max-height: min(60vh, 520px);
535
631
  }
536
632
  }
537
633
  }
@@ -638,3 +734,39 @@
638
734
  transition: none;
639
735
  }
640
736
  }
737
+
738
+ /* ----------------------------------------------------------------------------
739
+ …and the rail collapsed to an icon strip
740
+ ----------------------------------------------------------------------------
741
+ `<html data-nav-collapsed="true">` (Navigation writes it; see
742
+ utils/navCollapsed.js) means the rail is 64px wide and every row shows its
743
+ icon alone, with the tooltip doing the labelling. The bell draws its own
744
+ trigger, so the two things the rail cannot do for it are done here: drop the
745
+ `::after` label, and centre what is left.
746
+
747
+ Desktop only, exactly as the rail's own collapsed rules are — below 1024px
748
+ the rail is a drawer and the attribute is never written.
749
+
750
+ The panel itself needs nothing: it is anchored to the ROW's right edge
751
+ (`left: calc(100% + gap)`), so it follows the rail in and out without a
752
+ figure of its own.
753
+ ------------------------------------------------------------------------- */
754
+ @media (min-width: 1025px) {
755
+ :root[data-nav-collapsed='true'] [data-nav-actions='sidebar'] {
756
+ .notificationTrigger {
757
+ justify-content: center;
758
+
759
+ &::after {
760
+ display: none;
761
+ }
762
+ }
763
+
764
+ /* Back on the icon's shoulder: there is no line for it to sit at the
765
+ end of any more. */
766
+ .badge {
767
+ top: 2px;
768
+ right: 8px;
769
+ transform: none;
770
+ }
771
+ }
772
+ }