@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.
Files changed (41) hide show
  1. package/README.md +470 -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 +9 -2
  10. package/src/components/auth/PasskeyEnrolPrompt.jsx +269 -0
  11. package/src/components/auth/Profile.jsx +422 -26
  12. package/src/components/auth/authEndpoints.js +43 -0
  13. package/src/components/auth/passkeyClient.js +102 -0
  14. package/src/components/auth/passkeyPrompt.js +408 -0
  15. package/src/components/auth/profileLayout.js +45 -0
  16. package/src/components/controls/DataGridSearch.jsx +16 -2
  17. package/src/components/controls/DataGridSortSheet.jsx +2 -0
  18. package/src/components/generic/ActionButtons.jsx +1 -1
  19. package/src/components/generic/GenericAuth.jsx +197 -21
  20. package/src/components/generic/GenericDetail.jsx +95 -3
  21. package/src/components/generic/GenericMain.jsx +7 -0
  22. package/src/components/generic/StandardModal.jsx +71 -158
  23. package/src/components/sms/SmsCampaigns.jsx +2487 -0
  24. package/src/components/sms/SmsInbox.jsx +18 -0
  25. package/src/components/sms/SmsThreadPanel.jsx +20 -0
  26. package/src/components/sms/smsEndpoints.js +23 -0
  27. package/src/components/styles/DataGrid.module.scss +22 -0
  28. package/src/components/styles/Form.module.scss +100 -2
  29. package/src/components/styles/Navigation.module.scss +322 -5
  30. package/src/components/styles/Notification.module.scss +140 -8
  31. package/src/components/styles/PasskeyEnrolPrompt.module.scss +105 -0
  32. package/src/components/styles/Profile.module.scss +157 -0
  33. package/src/components/styles/Sms.module.scss +451 -0
  34. package/src/components/styles/StandardModal.module.scss +273 -0
  35. package/src/components/styles/global.css +24 -5
  36. package/src/components/utils/navCollapsed.js +201 -0
  37. package/src/components/utils/rowActionSettings.js +76 -0
  38. package/src/components/utils/usePasskeysEnabled.js +84 -0
  39. package/src/components/utils/useShellLayout.js +131 -0
  40. package/src/index.js +103 -1
  41. package/src/utils/rememberTab.js +70 -0
@@ -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
+ }
@@ -0,0 +1,105 @@
1
+ @use 'controls' as *;
2
+
3
+ /* ============================================================================
4
+ PasskeyEnrolPrompt — the offer made once, after signing in
5
+ ----------------------------------------------------------------------------
6
+ Inside a StandardModal at `size="small"`, so the panel's width, its phone
7
+ geometry and its z-index all come from StandardModal.module.scss and none of
8
+ it is restated here. What is here is the inside of the panel: a line of
9
+ explanation and a row of three buttons whose weights say what they are.
10
+
11
+ The three are deliberately NOT three equal buttons. "Add passkey" is the
12
+ thing being offered; "Not now" is the way out anybody can take without
13
+ thinking about it; "Don't ask again" is quieter still and reads as a link,
14
+ because a permanent decision should not be the easiest button on the screen
15
+ to hit by accident.
16
+
17
+ Every measurement is `var(--hook, <value>)` so an app can retune it from
18
+ `:root` without forking the component, the same way Profile.module.scss
19
+ does.
20
+ ========================================================================= */
21
+
22
+ .body {
23
+ display: flex;
24
+ flex-direction: column;
25
+ gap: var(--spacing-md, 1rem);
26
+ padding: var(--spacing-md, 1rem) 0 0;
27
+ }
28
+
29
+ /* The mark. A key in a tinted disc — enough to say "this is about signing in"
30
+ without an illustration nobody asked to download. */
31
+ .mark {
32
+ display: flex;
33
+ align-items: center;
34
+ justify-content: center;
35
+ /* A column flex parent stretches its children; without this the disc
36
+ becomes a lozenge the width of the panel. */
37
+ align-self: flex-start;
38
+ width: 2.75rem;
39
+ height: 2.75rem;
40
+ border-radius: 50%;
41
+ background: var(--primary-color-light, rgba(0, 0, 0, 0.06));
42
+ color: var(--primary-color, #1b3a57);
43
+ flex: 0 0 auto;
44
+ }
45
+
46
+ .text {
47
+ margin: 0;
48
+ font-size: var(--font-size-sm, 0.875rem);
49
+ line-height: 1.55;
50
+ color: var(--text-color, #333);
51
+ }
52
+
53
+ /* The row of choices. It wraps rather than scrolls: on a 320px phone three
54
+ buttons do not fit side by side, and a row that overflows puts the way out
55
+ off the edge of the sheet. */
56
+ .actions {
57
+ display: flex;
58
+ flex-wrap: wrap;
59
+ align-items: center;
60
+ gap: var(--spacing-sm, 0.5rem);
61
+ padding-top: var(--spacing-sm, 0.5rem);
62
+ }
63
+
64
+ .primary {
65
+ @include button-primary;
66
+ }
67
+
68
+ .later {
69
+ @include button-secondary;
70
+ }
71
+
72
+ /* Pushed to the far end of the row, and styled as text: the permanent answer
73
+ should be available, findable, and never the thing a thumb lands on. */
74
+ .never {
75
+ @include button-base;
76
+
77
+ margin-left: auto;
78
+ background: none;
79
+ border-color: transparent;
80
+ color: var(--muted-color, #6b7280);
81
+ text-decoration: underline;
82
+ text-underline-offset: 0.2em;
83
+
84
+ &:hover:not(:disabled) {
85
+ background: none;
86
+ color: var(--text-color, #333);
87
+ }
88
+ }
89
+
90
+ /* Phones: one full-width button per line, in the order they are read, and the
91
+ quiet one loses its auto margin — `margin-left: auto` in a column pushes it
92
+ to the right edge of a stack, which is where nothing else is. */
93
+ @media (max-width: 640px) {
94
+ .actions {
95
+ flex-direction: column;
96
+ align-items: stretch;
97
+ }
98
+
99
+ .primary,
100
+ .later,
101
+ .never {
102
+ width: 100%;
103
+ margin-left: 0;
104
+ }
105
+ }
@@ -580,6 +580,44 @@
580
580
  }
581
581
  }
582
582
 
583
+ /* The passkey list. A stack of rows rather than a table: each row is a name,
584
+ a quiet line of context and the one control that acts on it, and at phone
585
+ width a table of three columns is a table that scrolls sideways. The row
586
+ wraps instead, so the Remove button drops under the name rather than off
587
+ the edge. */
588
+ .passkeyList {
589
+ display: flex;
590
+ flex-direction: column;
591
+ gap: var(--spacing-sm, 0.5rem);
592
+ width: 100%;
593
+ margin: 0;
594
+ padding: 0;
595
+ list-style: none;
596
+ }
597
+
598
+ .passkeyItem {
599
+ display: flex;
600
+ flex-wrap: wrap;
601
+ align-items: center;
602
+ gap: var(--spacing-sm, 0.5rem);
603
+ padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
604
+ border: 1px solid var(--border-color, #e1e1e1);
605
+ border-radius: var(--radius-sm, 8px);
606
+ }
607
+
608
+ .passkeyName {
609
+ font-weight: var(--font-weight-semibold, 600);
610
+ /* Takes the slack so the button sits at the far edge of the row. */
611
+ flex: 1 1 auto;
612
+ min-width: 0;
613
+ overflow-wrap: anywhere;
614
+ }
615
+
616
+ .passkeyMeta {
617
+ font-size: var(--font-size-xs, 0.75rem);
618
+ color: var(--muted-color, #6b7280);
619
+ }
620
+
583
621
  /* The setup walkthrough. It used to be a bordered, shadowed card inside the
584
622
  panel card, carrying a heading that repeated the heading above it. The
585
623
  section owns the title and the separation now, so this is only the steps. */
@@ -902,3 +940,122 @@
902
940
  color: rgba(var(--paragraph-color-rgb, 43, 43, 43), 0.65);
903
941
  }
904
942
  }
943
+
944
+ /* ============================================================================
945
+ * Tabs as a strip
946
+ * ----------------------------------------------------------------------------
947
+ * The rail above is the layout this screen has always had, and on a top-bar
948
+ * app it is still the one it gets. In a SIDEBAR app the page already has a
949
+ * vertical navigation column two centimetres to the left, and a second one
950
+ * beside it spends a fifth of the content width to show two words. Profile
951
+ * asks for the strip there (see profileLayout.js for the rule, Profile.jsx
952
+ * for where the classes below are applied) and for the same three shapes
953
+ * GenericIndex and GenericDetail use for it, so the three pages agree:
954
+ *
955
+ * `.crmtitle--flat` the page title as type on the page ground.
956
+ * `.grid__subrow--stacked` one column instead of rail + panel.
957
+ * `.grid__subnav--underline` the rail's card undone; TableFilter's
958
+ * `variant="underline"` draws what is inside.
959
+ *
960
+ * Appended rather than merged into the rules above, and every selector here
961
+ * carries a modifier class: with none of them applied — which is every
962
+ * `layout="full"` app, and any app that passes `tabLayout="rail"` — not one
963
+ * declaration in this block matches anything.
964
+ * ==========================================================================*/
965
+
966
+ /* `.crmtitle` is applied to the same element as `.grid__full`, so this has to
967
+ undo that card's surface, border, radius, shadow and 2px padding as well as
968
+ the title card's own. Matched to `.crmtitle` in GenericIndex.module.scss —
969
+ the same inset, the same type size — so the account screen sits on the same
970
+ baseline as every index page. */
971
+ .crmtitle--flat {
972
+ padding: 0 0.25rem 0.85rem;
973
+ margin: 8px 0 0;
974
+ background: none;
975
+ border: 0;
976
+ border-radius: 0;
977
+ box-shadow: none;
978
+
979
+ h1 {
980
+ font-size: 1.375rem;
981
+ font-weight: 700;
982
+ letter-spacing: -0.01em;
983
+ line-height: 1.25;
984
+ color: var(--heading-color, var(--primary-color, #1b3933));
985
+ }
986
+ }
987
+
988
+ /* Two grid tracks become one flow column: `grid-template-columns: none` is
989
+ what hands the rail's 14rem back to the panel rather than leaving a hole
990
+ beside it. */
991
+ .grid__subrow--stacked {
992
+ display: flex;
993
+ flex-direction: column;
994
+ flex-wrap: nowrap;
995
+ grid-template-columns: none;
996
+ align-items: stretch;
997
+ gap: 0;
998
+ }
999
+
1000
+ .grid__subrow--stacked > .grid__subnav,
1001
+ .grid__subrow--stacked > .grid__subcontent {
1002
+ flex: 0 0 auto;
1003
+ width: 100%;
1004
+ /* Without this the strip's min-content width floors the flex item, so a
1005
+ row of tabs widens the page instead of scrolling inside it. */
1006
+ min-width: 0;
1007
+ }
1008
+
1009
+ /* The strip and the card it labels are one object: no gap, and the card's top
1010
+ corners squared off under it. The `8px 0` margin `.grid__subcontent` carries
1011
+ for the rail layout would put a hairline gap between the two. */
1012
+ .grid__subrow--stacked > .grid__subcontent {
1013
+ margin: 0;
1014
+ border-top-left-radius: 0;
1015
+ border-top-right-radius: 0;
1016
+ }
1017
+
1018
+ .grid__subnav--underline {
1019
+ /* Not a card. */
1020
+ padding: 0;
1021
+ margin: 0;
1022
+ background: none;
1023
+ border: none;
1024
+ border-radius: 0;
1025
+ box-shadow: none;
1026
+ /* The one hairline under the whole strip; the selected tab's accent edge
1027
+ sits on top of it, and the content card below continues it. */
1028
+ border-bottom: 1px solid var(--border-color, #e1e1e1);
1029
+ display: block;
1030
+ min-width: 0;
1031
+ }
1032
+
1033
+ /* `.grid__subnav > div` above only zeroes a margin; the strip needs the
1034
+ wrapper to stay a plain block, which is what TableFilter's own stylesheet
1035
+ expects around its scroller. */
1036
+ .grid__subnav--underline > div {
1037
+ display: block;
1038
+ min-width: 0;
1039
+ margin-bottom: 0;
1040
+ padding: 0;
1041
+ background: none;
1042
+ }
1043
+
1044
+ /* The panel header named the tab — "Overview" under a rail whose selected
1045
+ item also said "Overview". Beside a strip that is the same word twice, half
1046
+ a centimetre apart, so the strip is the only place the tab is named.
1047
+
1048
+ Hidden rather than dropped from the markup: the rail layout renders this
1049
+ element and must keep rendering it, the heading is the same string the
1050
+ selected tab already publishes (with real tab semantics — see TableFilter's
1051
+ underline variant), and one class in one place is easier to be sure about
1052
+ than a branch inside a memoised renderer with sixteen dependencies. */
1053
+ .grid__subrow--stacked .gridtxt__header {
1054
+ display: none;
1055
+ }
1056
+
1057
+ /* With the header gone the sections would start hard against the card's top
1058
+ edge. The header's own vertical padding, given back to the body. */
1059
+ .grid__subrow--stacked .form {
1060
+ padding-top: var(--spacing-md, 1rem);
1061
+ }