@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
@@ -0,0 +1,182 @@
1
+ /**
2
+ * StackedRow — one grid row, stacked, for a screen too narrow to lay columns
3
+ * out side by side.
4
+ *
5
+ * ROWS, NOT CARDS. The vocabulary the mapper uses is a card's (title, subtitle,
6
+ * meta, badge) but nothing here is a Material card: no elevation, no inner
7
+ * border, no rounded panel, no margin between rows. The row keeps the grid's
8
+ * own zebra striping and its own divider, because a card per record is four
9
+ * times the height for the same four facts and a phone has the least height of
10
+ * anything we ship to.
11
+ *
12
+ * EVERY SIZE IS rem, AND THAT IS LOAD-BEARING. `styles/density.css` takes the
13
+ * root font-size to 112.5% under `data-density="large"`, so every line, gap and
14
+ * pad below grows by 1.125 with no rule in this file mentioning density. The px
15
+ * table in `utils/cardLayout.js` that fixes the ROW's height is deliberately a
16
+ * shade larger than 1.125× at every entry, so the computed height is always at
17
+ * or above what these rules need — over-allocating leaves a hairline of slack,
18
+ * under-allocating clips the last line, and only one of those is noticed.
19
+ *
20
+ * NO HOVER RULES ANYWHERE IN THIS FILE. It renders on a coarse pointer, where
21
+ * hover is either absent or sticky, and an action that appears on hover is an
22
+ * action a finger cannot find.
23
+ */
24
+
25
+ .card {
26
+ display: flex;
27
+ gap: var(--spacing-sm, 0.5rem);
28
+ align-items: center;
29
+ box-sizing: border-box;
30
+
31
+ /* The cell is sized by the grid's fixed rowHeight; the row fills it and
32
+ clips rather than pushing it. Nothing inside may change the height —
33
+ a blank field collapses its line's CONTENT and leaves the row exactly
34
+ as tall as its neighbours. */
35
+ width: 100%;
36
+ height: 100%;
37
+ padding: 0.625rem 0;
38
+ overflow: hidden;
39
+ }
40
+
41
+ /* `min-width: 0` is the whole reason the ellipses work: a flex child's default
42
+ `min-width: auto` is its content's intrinsic width, so a long client name
43
+ would push the rail off the screen instead of being clipped. */
44
+ .main {
45
+ display: flex;
46
+ flex: 1 1 auto;
47
+ flex-direction: column;
48
+ gap: 0.125rem;
49
+ min-width: 0;
50
+ }
51
+
52
+ .titleRow {
53
+ display: flex;
54
+ gap: 0.375rem;
55
+ align-items: baseline;
56
+ min-width: 0;
57
+ }
58
+
59
+ /* The record's own number, where it has one: a quote number, a ticket number.
60
+ Mono because it is read digit by digit and compared, not scanned. */
61
+ .reference {
62
+ flex: 0 0 auto;
63
+ padding: 0 0.25rem;
64
+ color: var(--muted-color, #6b7280);
65
+ font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
66
+ font-size: var(--font-size-xs, 0.75rem);
67
+ line-height: 1.375rem;
68
+ background: var(--border-color, #e1e1e1);
69
+ border-radius: var(--radius-sm, 4px);
70
+ }
71
+
72
+ .title,
73
+ .subtitle,
74
+ .line {
75
+ /* Fixed height, so nothing may take a second line. Repeated on each rather
76
+ than shared through a parent because the renderers put their own
77
+ elements inside these and a rule on the parent does not reach them. */
78
+ overflow: hidden;
79
+ min-width: 0;
80
+ white-space: nowrap;
81
+ text-overflow: ellipsis;
82
+ }
83
+
84
+ .title {
85
+ color: var(--heading-color, #1b3933);
86
+ font-size: 0.9375rem;
87
+ font-weight: var(--font-weight-semibold, 600);
88
+ line-height: 1.375rem;
89
+ }
90
+
91
+ .subtitle {
92
+ color: var(--muted-color, #6b7280);
93
+ font-size: var(--font-size-sm, 0.8125rem);
94
+ line-height: 1.125rem;
95
+ }
96
+
97
+ .line {
98
+ display: flex;
99
+ gap: 0.375rem;
100
+ align-items: baseline;
101
+ color: var(--paragraph-color, #443c2b);
102
+ font-size: var(--font-size-sm, 0.8125rem);
103
+ line-height: 1.125rem;
104
+ }
105
+
106
+ .lineLabel {
107
+ flex: 0 0 auto;
108
+ color: var(--muted-color, #6b7280);
109
+ }
110
+
111
+ .lineValue {
112
+ overflow: hidden;
113
+ min-width: 0;
114
+ white-space: nowrap;
115
+ text-overflow: ellipsis;
116
+ }
117
+
118
+ /* The click-to-call and click-to-mail anchors — the reason this treatment
119
+ exists. Accent-coloured because they are the only thing on the row that is
120
+ not the row's own link, and underlined so that reads as a decision rather
121
+ than as a colour scheme. */
122
+ .channel {
123
+ overflow: hidden;
124
+ color: var(--primary-color, #1b3933);
125
+ text-decoration: underline;
126
+ text-underline-offset: 0.125rem;
127
+ white-space: nowrap;
128
+ text-overflow: ellipsis;
129
+ }
130
+
131
+ /* Right-hand rail: the state chip over the row's action icons.
132
+ `auto` width, never flexed — the main column is what gives way. */
133
+ .rail {
134
+ display: flex;
135
+ flex: 0 0 auto;
136
+ flex-direction: column;
137
+ gap: 0.25rem;
138
+ align-items: flex-end;
139
+ justify-content: center;
140
+ min-height: 2.75rem;
141
+ max-width: 45%;
142
+ }
143
+
144
+ .badge {
145
+ overflow: hidden;
146
+ max-width: 100%;
147
+ color: var(--paragraph-color, #443c2b);
148
+ font-size: var(--font-size-xs, 0.75rem);
149
+ white-space: nowrap;
150
+ text-overflow: ellipsis;
151
+ }
152
+
153
+ /* Always drawn, never on hover. The icons inside are the grid's own
154
+ `renderRowAction` / `renderSetting` output and already stopPropagation, so
155
+ the row's link and the rail's controls cannot both fire. */
156
+ .actions {
157
+ display: flex;
158
+ gap: 0.125rem;
159
+ align-items: center;
160
+ justify-content: flex-end;
161
+ }
162
+
163
+ /* THE HIT AREA, not the glyph.
164
+ *
165
+ * `.tdaction` in DataGrid.module.scss is a 28px square, and in the table that
166
+ * is the whole target — the action COLUMN reserves 44px per icon of width, but
167
+ * nothing widens the icon itself. 28px is under every touch guideline there
168
+ * is, and on a card the icons sit at the end of a row whose whole body is a
169
+ * link, so a near miss does not do nothing: it opens the record.
170
+ *
171
+ * So the WRAPPER is grown instead of the icon. Both of the grid's action
172
+ * entry points put their `onClick` on the wrapper — `.rowActionTrigger` for an
173
+ * inline row action, the `setting-*` span for a standard one — so the target
174
+ * grows and nothing about the drawn icon changes. Scoped to this module, so
175
+ * the table's own action column is untouched. */
176
+ .actions > * {
177
+ display: inline-flex;
178
+ align-items: center;
179
+ justify-content: center;
180
+ min-width: 2.75rem;
181
+ min-height: 2.75rem;
182
+ }
@@ -407,6 +407,82 @@
407
407
  .when { margin-left: 0; width: 100%; }
408
408
  }
409
409
 
410
+ /* -- the phone ------------------------------------------------------------- */
411
+
412
+ /*
413
+ * At 390px the 759 block above is doing the right thing and not enough of it.
414
+ * It fixes the header — one column, the timestamp on its own line — and leaves
415
+ * the two boxes the page is actually FOR at their desktop insets.
416
+ *
417
+ * Count the gutters an entry's text sits behind on a 390px screen: the CRM's
418
+ * page padding, then TicketWorkspace's `.main` card at 1rem a side, then this
419
+ * module's `.entry` at another 1rem. That is roughly 96px of chrome, a quarter
420
+ * of the glass, spent on nested padding around a line of email — and the
421
+ * composer pays it twice over, because its textarea then has its own 0.7rem
422
+ * inside that. Nothing overflows; it is simply the wrong split between frame
423
+ * and content at a width where content is scarce.
424
+ *
425
+ * So the two inner boxes give up half their side padding. The card edges still
426
+ * read as edges — they are drawn with a border, not with space — and the text
427
+ * gets about 28px of its width back per side.
428
+ *
429
+ * READ-AND-TRIAGE IS THE BAR for this pass: the thread has to be readable and
430
+ * the composer has to be usable. There is deliberately NO pinned action bar —
431
+ * a sticky Send over a thread this narrow costs a line of the conversation
432
+ * permanently, and the composer is already at the end of the page where the
433
+ * thumb is.
434
+ */
435
+ @media (max-width: 480px) {
436
+ .entry,
437
+ .composer {
438
+ padding: 0.7rem;
439
+ }
440
+
441
+ /* The message's own scroll cap. 24rem is 384px — on an 844px-tall phone,
442
+ most of what can be seen at once, so a long forwarded chain fills the
443
+ screen and the reply under it is a scroll away with no sign it is there.
444
+ 16rem still shows a real message and keeps the next entry in view. */
445
+ .body,
446
+ .bodyHtml {
447
+ max-height: 16rem;
448
+ }
449
+
450
+ /* Full-bleed Send, which is the phone convention for the one action a
451
+ screen exists to perform — and the only place in this library where a
452
+ full-width primary is right rather than a banner (see the note over
453
+ Form.module.scss's `.buttonContainer` for the desktop argument against
454
+ it). Nothing shares the row with it. */
455
+ .composerActions {
456
+ justify-content: stretch;
457
+ }
458
+
459
+ .send {
460
+ width: 100%;
461
+ }
462
+ }
463
+
464
+ /* 34px is a mouse target. Under a finger the Send button sits directly under a
465
+ textarea the reader has just been typing in, which is the worst place on the
466
+ page to miss — the miss lands in the textarea and looks like nothing
467
+ happened. 44px is the floor. Applied on the pointer rather than on a width,
468
+ because a tablet with a keyboard case is 1024px wide and still driven by a
469
+ thumb. */
470
+ @media (pointer: coarse) {
471
+ .send {
472
+ min-height: 44px;
473
+ height: auto;
474
+ }
475
+
476
+ /* The image-unblock and quote-collapse chips in `.bodyBar` are 0.68rem
477
+ text in a pill; give them a real box rather than a line of type to aim
478
+ at. They stay small — they are qualifiers on a message, not actions on
479
+ the ticket — but they stop being a 16px-tall target. */
480
+ .bodyAction {
481
+ min-height: 32px;
482
+ padding: 0.25rem 0.6rem;
483
+ }
484
+ }
485
+
410
486
  /* -- composer -------------------------------------------------------------- */
411
487
 
412
488
  .composer {
@@ -1106,6 +1106,66 @@ input.checkControl {
1106
1106
  font-size: 0.75rem; // 12px at a 16px root
1107
1107
  }
1108
1108
 
1109
+ // ---------------------------------------------------------------------------
1110
+ // Mounted in the sidebar rail, and in the mobile drawer
1111
+ // ---------------------------------------------------------------------------
1112
+ // `actionsPlacement: 'sidebar'` — and, below 1024px, the header layout's
1113
+ // drawer — draws the account actions as full-width rows instead of chips. The
1114
+ // block that holds them advertises itself with `data-nav-actions="sidebar"`
1115
+ // (Navigation.jsx / Navigation.module.scss `.rail-actions`), which is all this
1116
+ // file needs to know. Notification.module.scss and Sms.module.scss carry the
1117
+ // same block; a chip and a row are two different things in three places.
1118
+ //
1119
+ // The row's box — full width, the rail's padding, left-aligned, and the LABEL
1120
+ // beside the icon — all come from `.rail-actions`. The trigger publishes no
1121
+ // label of its own, so the row publishes one as `--nav-action-label` and the
1122
+ // rail hangs it off the trigger's `::after`. Only what this file would
1123
+ // otherwise force back is restated here.
1124
+ [data-nav-actions='sidebar'] {
1125
+ // The chip's lift and scale read as a stray button in a list of rows.
1126
+ // The focus ring is left alone — it is the only keyboard affordance the
1127
+ // row has.
1128
+ .trigger {
1129
+ &:hover,
1130
+ &:focus,
1131
+ &:active {
1132
+ transform: none;
1133
+ }
1134
+
1135
+ &:hover {
1136
+ background-color: rgba(var(--tertiary-color-rgb, 255, 255, 255), 0.05);
1137
+ color: var(--secondary-color);
1138
+ }
1139
+ }
1140
+
1141
+ // `top: 100%` drops the panel below the row. These rows are pinned to the
1142
+ // FOOT of the rail, so that is below the foot of the viewport. Beside the
1143
+ // rail, bottom-aligned with the row that opened it.
1144
+ .popoverWrapper {
1145
+ top: auto;
1146
+ right: auto;
1147
+ bottom: 0;
1148
+ left: calc(100% + 10px);
1149
+ margin-top: 0;
1150
+ }
1151
+
1152
+ @media (max-width: 1024px) {
1153
+ // Inside the drawer there is nothing to the right, so the panel opens
1154
+ // upward from the row.
1155
+ .popoverWrapper {
1156
+ left: 0;
1157
+ bottom: calc(100% + 8px);
1158
+ }
1159
+
1160
+ // Keeps the panel inside a phone's viewport: 348px of rows plus a
1161
+ // head, a search bar and a footer is taller than the room above the
1162
+ // drawer's footer.
1163
+ .rows {
1164
+ max-height: 40vh;
1165
+ }
1166
+ }
1167
+ }
1168
+
1109
1169
  // ---------------------------------------------------------------------------
1110
1170
  // Generator
1111
1171
  // ---------------------------------------------------------------------------
@@ -2006,6 +2066,138 @@ input.checkControl {
2006
2066
  }
2007
2067
  }
2008
2068
 
2069
+ // ---------------------------------------------------------------------------
2070
+ // The phone: six columns become two lines
2071
+ // ---------------------------------------------------------------------------
2072
+ //
2073
+ // The table sheds a column at a time down to 900px, which is the right answer
2074
+ // for a tablet and runs out at a phone: what is left below 640 is Title,
2075
+ // Username, Visibility, 2FA and five action buttons across roughly 350px of
2076
+ // glass. Every one of those tracks is then narrower than the word in its own
2077
+ // header, so the table stops being a table — it is six columns of hyphenated
2078
+ // fragments over a horizontal scrollbar.
2079
+ //
2080
+ // So each row becomes a small card of two lines: the title (with the host and
2081
+ // the client already sitting under it from the 1200px step) on the first, and
2082
+ // everything that qualifies it — username, visibility, the live 2FA code — on
2083
+ // the second, followed by the actions. This is SharePointGrid's `620` idea
2084
+ // carried one step past where columns can go: that grid drops a column at each
2085
+ // width until only the name and the date are left, and this is what happens
2086
+ // when even two tracks will not fit.
2087
+ //
2088
+ // THE SORT CONTROLS SURVIVE, which is the part that is easy to get wrong. The
2089
+ // obvious `thead { display: none }` takes the only sort control on the screen
2090
+ // with it — there is no sort menu in the toolbar to fall back on. Instead the
2091
+ // header row collapses to a wrapped strip of just the two sortable headings
2092
+ // (`SORTS` in VaultManager.jsx is Title and Username, and they are the first
2093
+ // two `th`), which reads as the row of chips it now looks like. The four
2094
+ // non-sortable headings are labels for cells that no longer sit under them, so
2095
+ // those do go.
2096
+ @media (max-width: 640px) {
2097
+ // The header strip. `display: block` on the `tr` and `inline-block` on the
2098
+ // `th` rather than flex, because a table row is not a flex container in
2099
+ // every engine without also restating `display` on the `table` and the
2100
+ // `tbody`, and doing that throws away the browser's row model for nothing.
2101
+ .table thead tr {
2102
+ display: block;
2103
+ padding: 0.35rem 0.7rem;
2104
+ // The rule under the header moves from the cells to the row, because
2105
+ // the cells are now a wrapped strip and a hairline under each of them
2106
+ // draws two short underlines instead of one line.
2107
+ border-bottom: 1px solid var(--v-line);
2108
+ background: var(--v-surface);
2109
+ }
2110
+
2111
+ .table thead th {
2112
+ display: inline-block;
2113
+ padding: 0.25rem 0.5rem 0.25rem 0;
2114
+ border-bottom: 0;
2115
+ }
2116
+
2117
+ // Everything after the two sortable columns. `nth-child` and not a class,
2118
+ // because these are the toolkit-free `<th>`s the component writes inline
2119
+ // and giving four of them a class each to hide them all is noise.
2120
+ .table thead th:nth-child(n + 3) {
2121
+ display: none;
2122
+ }
2123
+
2124
+ // A row is a card. The border that separated rows becomes the card's own
2125
+ // bottom edge, which is the same hairline in the same place.
2126
+ .table tbody tr {
2127
+ display: block;
2128
+ padding: 0.6rem 0.7rem;
2129
+ }
2130
+
2131
+ .table tbody td {
2132
+ display: inline;
2133
+ padding: 0;
2134
+ vertical-align: baseline;
2135
+ }
2136
+
2137
+ // Line one: the title block, which already carries the host and the client
2138
+ // underneath it from the 1200px step.
2139
+ .table tbody td:first-child {
2140
+ display: block;
2141
+ margin-bottom: 0.35rem;
2142
+ }
2143
+
2144
+ // Line two: username, visibility and the 2FA chip, spaced by a real gap
2145
+ // rather than by the padding a table cell would have had. Wrapped in a
2146
+ // flow of inline boxes so an em dash placeholder costs nothing.
2147
+ .table tbody td:nth-child(2),
2148
+ .table tbody td:nth-child(4),
2149
+ .table tbody td:nth-child(5) {
2150
+ margin-right: 0.6rem;
2151
+ color: var(--v-muted);
2152
+ font-size: 0.8rem;
2153
+ }
2154
+
2155
+ // The actions get a line of their own: five 29px buttons plus their gaps is
2156
+ // 165px, which is half the width of the glass and would push the facts
2157
+ // beside them down to nothing.
2158
+ .table tbody td.actionsCell {
2159
+ display: block;
2160
+ margin-top: 0.5rem;
2161
+ }
2162
+
2163
+ // Fitted mode pins every row to a single 58px band so the page size is
2164
+ // arithmetic (see `.tableWrapFitted` above). A two-line card does not fit
2165
+ // in 58px and would be clipped mid-row, so the pin comes off here and the
2166
+ // list scrolls instead. The pager still works; it just no longer gets a
2167
+ // whole number of rows, which is a trade a phone was never going to win.
2168
+ .tableWrapFitted {
2169
+ overflow-y: auto;
2170
+
2171
+ tbody tr,
2172
+ tbody td {
2173
+ height: auto;
2174
+ max-height: none;
2175
+ white-space: normal;
2176
+ }
2177
+ }
2178
+ }
2179
+
2180
+ // The ⌘K quick-search popover's rows, at the same width and for the same
2181
+ // reason. `.rows > .row` and not `.row`, because the geometry above is stated
2182
+ // at 0,2,0 precisely so a host cannot outrank it — and a 0,1,0 override in
2183
+ // here would not outrank it either.
2184
+ @media (max-width: 640px) {
2185
+ .rows > .row {
2186
+ grid-template-columns: minmax(0, 1fr);
2187
+ gap: 0.4rem;
2188
+ }
2189
+
2190
+ // Hard left on the second line rather than flush right against an edge the
2191
+ // title no longer reaches: the cluster now starts under the title it
2192
+ // belongs to, which is what keeps the pairing readable once they are two
2193
+ // lines rather than one.
2194
+ .rowActions {
2195
+ justify-self: start;
2196
+ margin-left: 0;
2197
+ flex-wrap: wrap;
2198
+ }
2199
+ }
2200
+
2009
2201
  // ---------------------------------------------------------------------------
2010
2202
  // Access log
2011
2203
  // ---------------------------------------------------------------------------
@@ -83,3 +83,13 @@
83
83
  :root[data-density='large'] body.tablet-mode {
84
84
  --grid-checkbox-col-width: 80px;
85
85
  }
86
+
87
+ /* And again for a coarse primary pointer, which switches the grid into the
88
+ same tablet geometry with no view config — see utils/useDensity.js, which
89
+ ORs the two signals for the JS half. Restated rather than shared: this is
90
+ plain CSS, so a class and a media query cannot be one selector. */
91
+ @media (pointer: coarse) {
92
+ :root[data-density='large'] body {
93
+ --grid-checkbox-col-width: 80px;
94
+ }
95
+ }