@visns-studio/visns-components 6.24.4 → 6.26.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 (51) 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/callQueue/CallQueueDiagnostics.jsx +1043 -0
  8. package/src/components/callQueue/CallQueuePop.jsx +713 -90
  9. package/src/components/callQueue/CallQueueSettings.jsx +308 -146
  10. package/src/components/callQueue/callPopStatus.js +236 -0
  11. package/src/components/callQueue/callQueueHelpers.js +284 -2
  12. package/src/components/columns/ColumnRenderers.jsx +3 -46
  13. package/src/components/columns/StackedRow.jsx +186 -0
  14. package/src/components/controls/DataGridSearch.jsx +110 -2
  15. package/src/components/controls/DataGridSortSheet.jsx +155 -0
  16. package/src/components/generic/GenericAuth.jsx +50 -18
  17. package/src/components/generic/GenericDashboard.jsx +20 -1
  18. package/src/components/generic/GenericDetail.jsx +446 -259
  19. package/src/components/mapboxSearchBox.js +640 -0
  20. package/src/components/navBadges.js +63 -1
  21. package/src/components/navDrawer.js +147 -0
  22. package/src/components/sms/SmsThreadPanel.jsx +34 -6
  23. package/src/components/sms/smsHelpers.js +15 -0
  24. package/src/components/styles/CallQueueDiagnostics.module.scss +398 -0
  25. package/src/components/styles/CallQueuePop.module.scss +29 -0
  26. package/src/components/styles/CallQueueSettings.module.scss +93 -0
  27. package/src/components/styles/ClientAuth.module.scss +39 -0
  28. package/src/components/styles/DataGrid.module.scss +158 -5
  29. package/src/components/styles/Field.module.scss +52 -1
  30. package/src/components/styles/Form.module.scss +82 -0
  31. package/src/components/styles/GenericClientPortal.module.scss +72 -20
  32. package/src/components/styles/GenericDashboard.module.scss +50 -0
  33. package/src/components/styles/GenericDetail.module.scss +63 -1
  34. package/src/components/styles/GenericDynamic.module.scss +23 -0
  35. package/src/components/styles/GenericFormBuilder.module.scss +11 -0
  36. package/src/components/styles/GenericIndex.module.scss +6 -1
  37. package/src/components/styles/Navigation.module.scss +460 -7
  38. package/src/components/styles/Sms.module.scss +92 -0
  39. package/src/components/styles/StackedRow.module.scss +182 -0
  40. package/src/components/styles/TicketConversation.module.scss +76 -0
  41. package/src/components/styles/Vault.module.scss +192 -0
  42. package/src/components/styles/density.css +10 -0
  43. package/src/components/styles/global-datagrid.css +163 -0
  44. package/src/components/styles/global.css +20 -0
  45. package/src/components/tickets/TicketConversation.jsx +13 -8
  46. package/src/components/utils/ConfirmDialog.js +22 -3
  47. package/src/components/utils/cardLayout.js +666 -0
  48. package/src/components/utils/contactChannels.js +130 -0
  49. package/src/components/utils/editPlacement.js +95 -0
  50. package/src/components/utils/useDensity.js +303 -7
  51. package/src/index.js +42 -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
+ }
@@ -103,6 +103,41 @@ body.tablet-mode .InovuaReactDataGrid__cell.cell-word-wrap > div {
103
103
  -webkit-line-clamp: 2 !important;
104
104
  }
105
105
 
106
+ /* The same three rules again for a COARSE PRIMARY POINTER, which now switches
107
+ the grid into its fixed-row-height geometry on its own — see the long note
108
+ in utils/useDensity.js. A phone or a tablet gets the roomy rows and the 44px
109
+ targets with no view config at all; the `body.tablet-mode` class above still
110
+ works and now means "treat this view as touch even on a mouse".
111
+ *
112
+ * Restated rather than folded into a shared selector list, because this is
113
+ * plain CSS with no mixins and a `:is()` across a class and a media query is
114
+ * not a thing that exists. Keep the two halves in step — they are one rule.
115
+ * `pointer` reports the PRIMARY device, so a touchscreen laptop answers `fine`
116
+ * and never reaches any of this. */
117
+ @media (pointer: coarse) {
118
+ .InovuaReactDataGrid__cell.cell-word-wrap,
119
+ .InovuaReactDataGrid__cell.cell-word-wrap > *,
120
+ .InovuaReactDataGrid__cell.cell-word-wrap div,
121
+ .InovuaReactDataGrid__cell.cell-word-wrap span {
122
+ overflow: hidden !important;
123
+ max-height: 100% !important;
124
+ }
125
+
126
+ .InovuaReactDataGrid__cell.cell-word-wrap:not(
127
+ .InovuaReactDataGrid__cell--no-padding
128
+ ) {
129
+ height: 100% !important;
130
+ align-items: center !important;
131
+ padding: var(--grid-cell-padding, 6px 8px) !important;
132
+ }
133
+
134
+ .InovuaReactDataGrid__cell.cell-word-wrap > div {
135
+ display: -webkit-box !important;
136
+ -webkit-box-orient: vertical !important;
137
+ -webkit-line-clamp: 2 !important;
138
+ }
139
+ }
140
+
106
141
  /* Default cell behavior - no word wrap */
107
142
  .InovuaReactDataGrid__cell:not(.cell-word-wrap) {
108
143
  white-space: nowrap !important;
@@ -853,6 +888,34 @@ body.tablet-mode .InovuaReactDataGrid__cell.cell-word-wrap > div {
853
888
  padding-inline-end: 0 !important;
854
889
  }
855
890
 
891
+ /* The filter row under a finger — as far as it can honestly go, which is not
892
+ 44px.
893
+ *
894
+ * The datagrid measures its own header height in JS from the theme, which pins
895
+ * the filter cell to `min-height: 32px`. A 44px control inside that cell is
896
+ * CLIPPED, not accommodated: the grid does not re-measure because a stylesheet
897
+ * grew one of its children. So the touch step here is 32px — the full height
898
+ * of the cell the theme already reserves, up from 28 — which at least makes
899
+ * the whole cell the target instead of a 28px box floating inside it.
900
+ *
901
+ * The real 44px targets on a touch device are the pager (DataGrid.module.scss)
902
+ * and the row action buttons. Filtering a grid on a phone is a typing task
903
+ * with a keyboard already covering half the screen; it is not the thing this
904
+ * pass had to rescue. Raising it properly means changing the grid theme's
905
+ * header height, which moves every desktop grid too. */
906
+ @media (pointer: coarse) {
907
+ .InovuaReactDataGrid__column-header__filter input,
908
+ .InovuaReactDataGrid__column-header__filter select {
909
+ height: 32px !important;
910
+ }
911
+
912
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box {
913
+ height: 32px !important;
914
+ min-height: 32px !important;
915
+ max-height: 32px !important;
916
+ }
917
+ }
918
+
856
919
  /* The keyboard highlight in a dropdown's option list — declared by the
857
920
  vendored theme, then painted with nothing.
858
921
 
@@ -1073,3 +1136,103 @@ body.tablet-mode .InovuaReactDataGrid__cell.cell-word-wrap > div {
1073
1136
  .visns-native-date-filter__input {
1074
1137
  font-size: 0.75rem !important;
1075
1138
  }
1139
+
1140
+ /* ==========================================================================
1141
+ NARROW VIEWPORTS — the pager, which used to be sliced off at the edge
1142
+ --------------------------------------------------------------------------
1143
+ Guarded on `(max-width: 640px)` — the same width `utils/useDensity.js`
1144
+ exports as NARROW_VIEWPORT_QUERY and raises the column floor at. Nothing in
1145
+ this block is reachable above it, so every desktop grid is untouched.
1146
+
1147
+ The pager is not the library's own markup: `pagination` on the datagrid
1148
+ renders the TOOLKIT's toolbar (`.inovua-react-pagination-toolbar`, no grid
1149
+ prefix), and that component measures itself in JS and puts its contents in
1150
+ an internal scroll container with little arrows at each end. On a phone its
1151
+ contents are 714px wide inside a 348px window, so what a reader actually
1152
+ gets is "|< < Page [1] of 236 > >| | R" — the "Results per page" label
1153
+ sheared off mid-word at the screen edge, with the rest behind a scroll
1154
+ affordance nobody recognises as one.
1155
+
1156
+ So the toolbar is made to WRAP instead of scroll. Wrapping keeps every
1157
+ control — the page size combo included — where hiding half of them would
1158
+ have been the easier fix and would have taken the record total off the one
1159
+ screen that cannot show the stat strip and the grid at the same time.
1160
+
1161
+ `!important` throughout because the widths and heights being overridden are
1162
+ INLINE, written by the scroll container's own measuring pass. The arrows go
1163
+ with the scrolling: an overflow affordance for content that no longer
1164
+ overflows is a button that does nothing.
1165
+ ========================================================================== */
1166
+ @media (max-width: 640px) {
1167
+ .inovua-react-pagination-toolbar {
1168
+ height: auto !important;
1169
+ min-height: 0 !important;
1170
+ }
1171
+
1172
+ .inovua-react-pagination-toolbar
1173
+ .inovua-react-toolkit-toolbar__scroll-container,
1174
+ .inovua-react-pagination-toolbar .inovua-react-scroll-container__wrapper,
1175
+ .inovua-react-pagination-toolbar .inovua-react-scroll-container__scroller,
1176
+ .inovua-react-pagination-toolbar .inovua-react-scroll-container__view {
1177
+ width: 100% !important;
1178
+ max-width: 100% !important;
1179
+ height: auto !important;
1180
+ max-height: none !important;
1181
+ overflow: visible !important;
1182
+ }
1183
+
1184
+ .inovua-react-pagination-toolbar
1185
+ .inovua-react-toolkit-toolbar__inner-wrapper {
1186
+ flex-wrap: wrap !important;
1187
+ width: 100% !important;
1188
+ row-gap: 0.4rem;
1189
+ }
1190
+
1191
+ /* The two regions and the spacer between them: the spacer is a flex
1192
+ `1 1 auto` that would otherwise push the record count onto a line of its
1193
+ own with a screen's worth of air in front of it. */
1194
+ .inovua-react-pagination-toolbar__region {
1195
+ flex-wrap: wrap !important;
1196
+ width: auto !important;
1197
+ min-width: 0 !important;
1198
+ }
1199
+
1200
+ .inovua-react-pagination-toolbar__spacer {
1201
+ display: none !important;
1202
+ }
1203
+
1204
+ /* The scroll arrows, now that nothing scrolls. */
1205
+ .inovua-react-pagination-toolbar .inovua-react-toolkit-toolbar__arrow {
1206
+ display: none !important;
1207
+ }
1208
+
1209
+ /* ----------------------------------------------------------------------
1210
+ CARD MODE — the cell the stacked row is drawn in.
1211
+
1212
+ `datagrid-card-mode` is on the grid and `datagrid-card-cell` is on the
1213
+ one synthetic column; both are set in DataGrid.jsx. The cell class is
1214
+ what does the work, because THE CHECKBOX COLUMN IS A CELL TOO and still
1215
+ wants the grid's own padding and its centred alignment — a rule scoped
1216
+ to the grid alone would strip both and drop the tick against the edge.
1217
+
1218
+ Everything here is `!important` for the same reason the rules it
1219
+ overrides are: those are `!important` themselves, a few hundred lines
1220
+ up, and were written before this block existed.
1221
+ ---------------------------------------------------------------------- */
1222
+ .datagrid-card-mode .InovuaReactDataGrid__cell.datagrid-card-cell {
1223
+ /* The card owns its own gutters — StackedRow's vertical padding is
1224
+ half the row-height arithmetic in `utils/cardLayout.js`, and two
1225
+ sets of it is how the last line ends up under the row below. */
1226
+ padding: 0 var(--spacing-sm, 0.5rem) !important;
1227
+ /* So `height: 100%` on the row inside resolves against the cell
1228
+ rather than against its own content. */
1229
+ align-items: stretch !important;
1230
+ /* The nowrap/ellipsis clamp on every non-wrapping cell would apply to
1231
+ the card as a whole; each LINE inside sets its own, which is what
1232
+ has to win. */
1233
+ white-space: normal !important;
1234
+ text-overflow: clip !important;
1235
+ /* A divider down the right of a full-width row is a line to nowhere. */
1236
+ border-right: 0 !important;
1237
+ }
1238
+ }