@visns-studio/visns-components 6.24.3 → 6.25.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 (43) hide show
  1. package/package.json +4 -2
  2. package/src/components/Autocomplete.jsx +189 -119
  3. package/src/components/DataGrid.jsx +472 -31
  4. package/src/components/Navigation.jsx +475 -51
  5. package/src/components/auth/ClientAuthFrame.jsx +5 -0
  6. package/src/components/auth/ClientAuthScreen.jsx +29 -0
  7. package/src/components/columns/ColumnRenderers.jsx +3 -46
  8. package/src/components/columns/StackedRow.jsx +186 -0
  9. package/src/components/controls/DataGridSearch.jsx +110 -2
  10. package/src/components/controls/DataGridSortSheet.jsx +155 -0
  11. package/src/components/generic/GenericAuth.jsx +50 -18
  12. package/src/components/generic/GenericDashboard.jsx +20 -1
  13. package/src/components/generic/GenericDetail.jsx +446 -259
  14. package/src/components/mapboxSearchBox.js +640 -0
  15. package/src/components/navBadges.js +63 -1
  16. package/src/components/navDrawer.js +147 -0
  17. package/src/components/sms/SmsThreadPanel.jsx +34 -6
  18. package/src/components/sms/smsHelpers.js +15 -0
  19. package/src/components/styles/ClientAuth.module.scss +39 -0
  20. package/src/components/styles/DataGrid.module.scss +158 -5
  21. package/src/components/styles/Field.module.scss +52 -1
  22. package/src/components/styles/Form.module.scss +82 -0
  23. package/src/components/styles/GenericClientPortal.module.scss +72 -20
  24. package/src/components/styles/GenericDashboard.module.scss +50 -0
  25. package/src/components/styles/GenericDetail.module.scss +63 -1
  26. package/src/components/styles/GenericDynamic.module.scss +23 -0
  27. package/src/components/styles/GenericFormBuilder.module.scss +11 -0
  28. package/src/components/styles/GenericIndex.module.scss +6 -1
  29. package/src/components/styles/Navigation.module.scss +460 -7
  30. package/src/components/styles/Sms.module.scss +92 -0
  31. package/src/components/styles/StackedRow.module.scss +182 -0
  32. package/src/components/styles/TicketConversation.module.scss +76 -0
  33. package/src/components/styles/Vault.module.scss +192 -0
  34. package/src/components/styles/density.css +10 -0
  35. package/src/components/styles/global-datagrid.css +220 -0
  36. package/src/components/styles/global.css +20 -0
  37. package/src/components/tickets/TicketConversation.jsx +13 -8
  38. package/src/components/utils/ConfirmDialog.js +22 -3
  39. package/src/components/utils/cardLayout.js +666 -0
  40. package/src/components/utils/contactChannels.js +130 -0
  41. package/src/components/utils/editPlacement.js +95 -0
  42. package/src/components/utils/useDensity.js +303 -7
  43. package/src/index.js +32 -0
@@ -7,8 +7,46 @@
7
7
  display: block;
8
8
  }
9
9
 
10
+ /* ============================================================================
11
+ * THE FORM GRID — four tracks, and the two it gives up when there is no room
12
+ * ----------------------------------------------------------------------------
13
+ * The grid was a flat `1fr 1fr 1fr 1fr` with nothing underneath it, so a
14
+ * `size: "quarter"` field held one of four tracks at EVERY width: about 80px
15
+ * on a 390px phone, which is a label that cannot be read over an input that
16
+ * cannot be typed in. Two tracks at 768 and one at 640 is the collapse.
17
+ *
18
+ * WHY A CONTAINER QUERY AND NOT A MEDIA QUERY. The window's width is not the
19
+ * question. The same `<Form>` renders in a full-page panel, in a 560px sheet,
20
+ * in a modal, and in a narrow side rail beside a grid — and a form in a 420px
21
+ * rail on a 2560px monitor has exactly the problem a form on a phone has. A
22
+ * media query answers "how big is the glass"; this layout needs "how much room
23
+ * do I actually have", and only the container knows that. The library already
24
+ * reaches for this where the same argument holds (Integrations' `.fields`,
25
+ * global-datagrid's native date filter), so it is the house pattern rather
26
+ * than a new dependency. Nothing here pins a browser floor that would forbid
27
+ * it; the same three engines that ship `color-mix()` — used freely across this
28
+ * library — ship container queries.
29
+ *
30
+ * ONE CONSEQUENCE WORTH KNOWING, because it is not obvious from the one line
31
+ * that causes it: `container-type: inline-size` also applies LAYOUT
32
+ * containment, which makes this element the containing block for any
33
+ * `position: fixed` descendant. Inside a form that is the colour picker's
34
+ * click-away scrim (Field.jsx's `cover`), which now covers the form rather
35
+ * than the whole window — clicking another field still dismisses the picker,
36
+ * clicking the page header no longer does. The scrim is the only fixed thing
37
+ * that renders in here; `.polActions` in GenericDynamic is a SIBLING of the
38
+ * container, not a child, so it is untouched.
39
+ *
40
+ * The item classes that answer these queries live in Field.module.scss
41
+ * (`.halfItem` / `.qtrItem`, written by Field.jsx from `settings.size`) —
42
+ * a different module, but the query is unnamed, so it resolves against the
43
+ * nearest ancestor container whichever file declared it. Keep the two ends in
44
+ * step: the breakpoints below and the ones in Field.module.scss are one rule
45
+ * written in two files because the class names are hashed per module.
46
+ * ==========================================================================*/
10
47
  .formcontainer {
11
48
  width: 100%;
49
+ container-type: inline-size;
12
50
 
13
51
  form {
14
52
  width: 100%;
@@ -24,6 +62,50 @@
24
62
  }
25
63
  }
26
64
 
65
+ /* Two tracks. A half-width field is then the full row and a quarter is half of
66
+ one, which is the same proportion the four-track layout gave them — the
67
+ shape survives, it just has less of it.
68
+ *
69
+ * WHERE THESE TWO NUMBERS BITE, since they are container widths and not window
70
+ * widths and the two do not land in the same places. StandardModal's sizes are
71
+ * 420 / 560 / 800 and `.modal__content` spends 1rem a side, so:
72
+ *
73
+ * large (768 content) — exactly on this boundary, and `max-width` is
74
+ * inclusive, so a large dialog draws TWO tracks on a
75
+ * desktop where it used to draw four. Quarters go from
76
+ * ~180px to ~370px, which is roomier rather than
77
+ * wrong, but it is a visible change and this is where
78
+ * to look when somebody asks about it.
79
+ * medium (528 content) — one track.
80
+ * small (388 content) — one track.
81
+ * a 22rem side rail — one track.
82
+ *
83
+ * If a form is wanted back at four tracks in a large dialog, the fix is this
84
+ * number and only this number — but it has to move in Field.module.scss and
85
+ * GenericDynamic.module.scss with it. */
86
+ @container (max-width: 768px) {
87
+ .formcontainer form {
88
+ grid-template-columns: 1fr 1fr;
89
+ }
90
+
91
+ .halfBuilderItem {
92
+ grid-column: 1 / -1;
93
+ }
94
+ }
95
+
96
+ /* One track. Below this a two-up row is two ~150px fields, which is where a
97
+ date input starts clipping its own year. */
98
+ @container (max-width: 640px) {
99
+ .formcontainer form {
100
+ grid-template-columns: 1fr;
101
+ }
102
+
103
+ .qtrBuilderItem,
104
+ .halfBuilderItem {
105
+ grid-column: 1 / -1;
106
+ }
107
+ }
108
+
27
109
  .formItem {
28
110
  width: 100%;
29
111
  position: relative;
@@ -25,71 +25,123 @@
25
25
  }
26
26
  }
27
27
 
28
+ // ============================================================================
29
+ // Header chrome — tokenised, NOT restyled.
30
+ // ----------------------------------------------------------------------------
31
+ // Every `--client-portal-*` property below falls back to the value this file
32
+ // carried before, so a host that defines none of them renders byte-identically
33
+ // to the version that shipped. They exist because the header is the one piece
34
+ // of a client portal that has to look like the company whose portal it is, and
35
+ // the markup for it lives in this library rather than in the host app — so
36
+ // without a hook the only ways to brand it are forking the component or an
37
+ // attribute selector against a generated class name, and both of those break
38
+ // the next time this file is touched.
39
+ //
40
+ // Nothing here reads a host token directly. A host brands the header by
41
+ // defining these properties (typically on `:root`), and can undo it by not.
42
+ // ============================================================================
43
+
28
44
  .portalHeader {
29
45
  display: flex;
30
46
  justify-content: space-between;
31
47
  align-items: center;
32
- padding: 0.5rem 1.25rem;
33
- background-color: var(--tertiary-color, white);
34
- box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
48
+ gap: var(--client-portal-header-gap, 1rem);
49
+ padding: var(--client-portal-header-padding, 0.5rem 1.25rem);
50
+ background: var(--client-portal-header-bg, var(--tertiary-color, white));
51
+ color: var(--client-portal-header-ink, inherit);
52
+ border-bottom: var(--client-portal-header-border, 0 solid transparent);
53
+ box-shadow: var(--client-portal-header-shadow, 0 1px 3px rgba(0, 0, 0, 0.05));
35
54
  z-index: 10;
36
- height: 64px; // Increased height to accommodate larger logo
55
+ // Increased height to accommodate larger logo
56
+ height: var(--client-portal-header-height, 64px);
57
+ flex: none;
37
58
  }
38
59
 
39
60
  .logoContainer {
40
61
  display: flex;
41
62
  align-items: center;
42
- margin-left: -1.25rem;
63
+ margin-left: var(--client-portal-logo-offset, -1.25rem);
43
64
  padding-left: 0.5rem;
65
+ min-width: 0;
44
66
  }
45
67
 
46
68
  .logo {
47
- height: 52px;
48
- max-width: 250px;
69
+ height: var(--client-portal-logo-height, 52px);
70
+ max-width: var(--client-portal-logo-max-width, 250px);
49
71
  object-fit: contain;
72
+ // `none` unless the host asks otherwise. A dark header needs the reverse of
73
+ // a logo drawn for a white one, and the host cannot swap the file from
74
+ // here — `brightness(0) invert(1)` is the usual answer.
75
+ filter: var(--client-portal-logo-filter, none);
50
76
  }
51
77
 
52
78
  .userInfo {
53
79
  display: flex;
54
80
  align-items: center;
55
- gap: 0.75rem;
81
+ gap: var(--client-portal-user-gap, 0.75rem);
82
+ min-width: 0;
56
83
  }
57
84
 
58
85
  .welcomeText {
59
- font-size: 0.8rem;
60
- color: var(--paragraph-color, #555);
86
+ font-size: var(--client-portal-welcome-size, 0.8rem);
87
+ color: var(--client-portal-header-ink, var(--paragraph-color, #555));
88
+ white-space: nowrap;
89
+ overflow: hidden;
90
+ text-overflow: ellipsis;
61
91
  }
62
92
 
63
93
  .logoutButton {
64
94
  display: flex;
65
95
  align-items: center;
66
96
  justify-content: center;
67
- width: 28px;
68
- height: 28px;
97
+ flex: none;
98
+ width: var(--client-portal-logout-size, 28px);
99
+ height: var(--client-portal-logout-size, 28px);
69
100
  padding: 0;
70
101
  background-color: transparent;
71
- border: 1px solid rgba(var(--paragraph-color-rgb, 0, 0, 0), 0.15);
72
- border-radius: var(--radius, 4px);
73
- color: var(--paragraph-color, #555);
102
+ border: 1px solid
103
+ var(
104
+ --client-portal-header-line,
105
+ rgba(var(--paragraph-color-rgb, 0, 0, 0), 0.15)
106
+ );
107
+ border-radius: var(--client-portal-logout-radius, var(--radius, 4px));
108
+ color: var(--client-portal-header-ink, var(--paragraph-color, #555));
74
109
  cursor: pointer;
75
110
  transition: all 0.2s ease;
76
111
 
77
112
  &:hover {
78
- background-color: rgba(var(--primary-rgb, 0, 123, 255), 0.05);
79
- border-color: var(--primary-color, #0f4229);
80
- color: var(--primary-color, #0f4229);
113
+ background-color: var(
114
+ --client-portal-logout-hover-bg,
115
+ rgba(var(--primary-rgb, 0, 123, 255), 0.05)
116
+ );
117
+ border-color: var(
118
+ --client-portal-logout-hover-line,
119
+ var(--primary-color, #0f4229)
120
+ );
121
+ color: var(
122
+ --client-portal-logout-hover-ink,
123
+ var(--primary-color, #0f4229)
124
+ );
125
+ }
126
+
127
+ &:focus-visible {
128
+ outline: 2px solid
129
+ var(--client-portal-focus-ring, var(--primary-color, #0f4229));
130
+ outline-offset: 2px;
81
131
  }
82
132
  }
83
133
 
84
134
  .portalContent {
85
135
  display: flex;
86
136
  flex: 1;
87
- height: calc(100vh - 64px); // Subtract header height
137
+ // Subtract header height the same property the header is sized from, so
138
+ // a host that changes one cannot leave the two disagreeing.
139
+ height: calc(100vh - var(--client-portal-header-height, 64px));
88
140
  }
89
141
 
90
142
  .mainContent {
91
143
  flex: 1;
92
- padding: 1.25rem;
144
+ padding: var(--client-portal-content-padding, 1.25rem);
93
145
  overflow-y: auto;
94
146
  color: var(--paragraph-color, #333);
95
147
  }
@@ -756,3 +756,53 @@
756
756
  }
757
757
 
758
758
  .modal__content { padding: 1.25rem; }
759
+
760
+ // ============================================================================
761
+ // The card, on a phone
762
+ // ----------------------------------------------------------------------------
763
+ // LAST IN THE FILE ON PURPOSE. Every declaration below overrides one written
764
+ // earlier at the same specificity — `.widgetNo` is a single class here and a
765
+ // single class there — so the same block sitting up beside the `grid__*` span
766
+ // collapse it belongs with would be beaten by the rules it exists to override
767
+ // and do nothing at all. Media queries carry no weight of their own.
768
+ //
769
+ // What changes and why: below 700px every card is one column wide, and the
770
+ // reserved heights in this sheet exist so that cards SIDE BY SIDE line their
771
+ // labels, values and buttons up with one another. Nothing sits beside a card
772
+ // at this width. A stat tile is a label, a number and a button — three lines
773
+ // — and it was reserving 180px for them: 40 of padding, 24 of gaps, 24 held
774
+ // open under a 17px label and 48 held open under a 32px number. That is one
775
+ // and a half cards to a 402px screen, on the surface whose whole job is to
776
+ // answer three questions at a glance.
777
+ //
778
+ // Nothing here shrinks the number itself. The value is the content; the empty
779
+ // space around it was the problem.
780
+ // ============================================================================
781
+
782
+ @media (max-width: 700px) {
783
+ // The token's own default rather than a flat value, so a project that has
784
+ // set --dash-card-padding keeps its figure at every width; only the
785
+ // shipped default tightens.
786
+ .grid {
787
+ --dash-pad: var(--dash-card-padding, 0.9rem);
788
+ }
789
+
790
+ .widgetItem {
791
+ gap: 0.5rem;
792
+ }
793
+
794
+ // Both floors go: each element is still exactly as tall as its content,
795
+ // and there is no neighbouring card to hold a line for.
796
+ .widgetTitle {
797
+ min-height: 0;
798
+ }
799
+
800
+ .widgetNo,
801
+ .widgetNo-alt {
802
+ min-height: 0;
803
+ }
804
+
805
+ .widgetLink {
806
+ padding-top: 0.25rem;
807
+ }
808
+ }
@@ -1207,10 +1207,15 @@
1207
1207
  z-index: 1000;
1208
1208
  }
1209
1209
 
1210
+ /* See DataGrid.module.scss's `.modalWide` for the whole note. Short version:
1211
+ `min-width` outranks both `width` and `max-width`, so a flat 600px floor put
1212
+ 210px of the dialog — its right edge and its close button — off the side of
1213
+ a 390px phone. 90vw is the width this rule already asks for, so the floor
1214
+ can never outrank the line above it. */
1210
1215
  .modalWide {
1211
1216
  width: 90vw;
1212
1217
  max-width: var(--detail-max-width, 1400px);
1213
- min-width: 600px;
1218
+ min-width: min(600px, 90vw);
1214
1219
  }
1215
1220
 
1216
1221
  .modal {
@@ -1384,6 +1389,63 @@
1384
1389
  0 12px 24px -12px color-mix(in srgb, var(--primary-color, #1b3933) 35%, transparent);
1385
1390
  }
1386
1391
 
1392
+ /* -- the same cluster, in the title row ----------------------------------- */
1393
+
1394
+ /* `editPlacement: "header"` (see utils/editPlacement.js) hangs the SAME
1395
+ cluster off the breadcrumb row instead of pinning it to the viewport. It is
1396
+ a modifier rather than an edit to `.polActions` on purpose: the floating bar
1397
+ is still the default on every other detail page in every other application,
1398
+ and the two `:has(> :nth-child(2))` rules above are the whole of its
1399
+ treatment.
1400
+
1401
+ Which is exactly what has to come off here. A pill with a blurred backdrop
1402
+ is how a control says "I am floating over the page"; sitting in a header bar
1403
+ it reads as a second bar drawn inside the first, and the elevation a lone
1404
+ button carries reads as a button that has come loose. In the title row the
1405
+ ground is the header, so the cluster is nothing but its buttons — `.btn`
1406
+ itself is untouched, so they are the same buttons as everywhere else.
1407
+
1408
+ Both selectors are doubled (`.polActions.polActionsHeader`) to out-specify
1409
+ the two rules above, which `:has()` lifts to two classes' worth. Source
1410
+ order after them is not enough on its own. */
1411
+ .polActions.polActionsHeader {
1412
+ position: static;
1413
+ inset: auto;
1414
+ z-index: auto;
1415
+ padding: 0;
1416
+ border: 0;
1417
+ border-radius: 0;
1418
+ background: none;
1419
+ backdrop-filter: none;
1420
+ box-shadow: none;
1421
+ flex-wrap: wrap;
1422
+ justify-content: flex-end;
1423
+ }
1424
+
1425
+ .polActions.polActionsHeader > * {
1426
+ box-shadow: none;
1427
+ }
1428
+
1429
+ /* The right-hand end of the title row: the total, a host's `actions`, and —
1430
+ under this placement — the cluster. `margin-left: auto` does nothing while
1431
+ the row fits (the bar is already `justify-content: space-between`); it is
1432
+ what keeps the group on the right once it has WRAPPED onto its own line. */
1433
+ .titleActions {
1434
+ margin-left: auto;
1435
+ }
1436
+
1437
+ /* Wrapping is opt-in, and only for a header that actually received a cluster.
1438
+ A record title is long ("Verkada Camera & Access Control" under a client
1439
+ name) and a row of buttons is not compressible, so without this the title
1440
+ is squeezed to a word per line to hold a shape that stopped fitting. The
1441
+ buttons take their own line instead. Left alone for every page that has not
1442
+ opted in, so no existing header can start wrapping. */
1443
+ .crmtitle:has(.polActionsHeader),
1444
+ .crmtitle--alternate:has(.polActionsHeader) {
1445
+ flex-wrap: wrap;
1446
+ row-gap: var(--spacing-sm, 0.5rem);
1447
+ }
1448
+
1387
1449
  /* -- states --------------------------------------------------------------- */
1388
1450
 
1389
1451
  .noDataMessage {
@@ -176,8 +176,17 @@
176
176
  font-size: 1rem;
177
177
  }
178
178
 
179
+ /* A third copy of the four-track form grid — this one wraps Field.jsx directly
180
+ (see GenericDynamic.jsx), so it has to declare the container too, or the
181
+ collapse written in Field.module.scss finds nothing to measure and the
182
+ fields stay four-up on a phone. `container-type` is the whole of what is
183
+ added here; the reasoning is over Form.module.scss's `.formcontainer`.
184
+ `.polActions` in this module is `position: fixed`, but it is a SIBLING of
185
+ this element rather than a descendant, so the layout containment that comes
186
+ with `container-type` does not move it. */
179
187
  .formcontainer {
180
188
  width: 100%;
189
+ container-type: inline-size;
181
190
 
182
191
  form {
183
192
  width: 100%;
@@ -188,6 +197,20 @@
188
197
  }
189
198
  }
190
199
 
200
+ /* The track list, narrowed. What a "half" and a "quarter" mean against the
201
+ narrower list is re-stated in Field.module.scss, which owns those classes. */
202
+ @container (max-width: 768px) {
203
+ .formcontainer form {
204
+ grid-template-columns: 1fr 1fr;
205
+ }
206
+ }
207
+
208
+ @container (max-width: 640px) {
209
+ .formcontainer form {
210
+ grid-template-columns: 1fr;
211
+ }
212
+ }
213
+
191
214
  .formItem {
192
215
  width: 100%;
193
216
  position: relative;
@@ -2929,6 +2929,17 @@ select:not(:placeholder-shown) + .fi__span {
2929
2929
  opacity: 1;
2930
2930
  }
2931
2931
 
2932
+ /* No pointer means no hover, and a hidden control that is only revealed by an
2933
+ event the device cannot generate is not hidden, it is GONE — on a tablet
2934
+ these rows had no edit and no delete at all. `focus-within` above already
2935
+ covers the keyboard; this covers the finger. The same pattern, and the same
2936
+ sentence, as `_associationManager.scss`'s `.actions`. */
2937
+ @media (hover: none), (pointer: coarse) {
2938
+ .cActions {
2939
+ opacity: 1;
2940
+ }
2941
+ }
2942
+
2932
2943
  .cDelete:hover {
2933
2944
  color: var(--secondary-color) !important;
2934
2945
  background: rgba(var(--secondary-rgb, 60, 191, 125), 0.1) !important;
@@ -1208,10 +1208,15 @@
1208
1208
  z-index: 1000;
1209
1209
  }
1210
1210
 
1211
+ /* See DataGrid.module.scss's `.modalWide` for the whole note. Short version:
1212
+ `min-width` outranks both `width` and `max-width`, so a flat 600px floor put
1213
+ 210px of the dialog — its right edge and its close button — off the side of
1214
+ a 390px phone. 90vw is the width this rule already asks for, so the floor
1215
+ can never outrank the line above it. */
1211
1216
  .modalWide {
1212
1217
  width: 90vw;
1213
1218
  max-width: var(--detail-max-width, 1400px);
1214
- min-width: 600px;
1219
+ min-width: min(600px, 90vw);
1215
1220
  }
1216
1221
 
1217
1222
  .modal {