@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
@@ -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;
@@ -765,6 +800,63 @@ body.tablet-mode .InovuaReactDataGrid__cell.cell-word-wrap > div {
765
800
  border-radius: 6px !important;
766
801
  background: var(--dgx-surface) !important;
767
802
  box-shadow: none !important;
803
+ /* The vendored theme's padding-top: 2px — inside a hard 28px box it just
804
+ pushed the text off-centre. */
805
+ padding-top: 0 !important;
806
+ }
807
+
808
+ /* A narrow column must TRUNCATE the text, not run it into the arrow.
809
+
810
+ The vendored theme declares the placeholder `white-space: nowrap` and gives
811
+ neither it nor the flex boxes above it any way to shrink: `__value` is
812
+ allowed to wrap (useless inside a hard 28px height — the second line is
813
+ just clipped) and the input wrapper keeps its content-size width. So in a
814
+ column narrower than the text, "ALL OPTIONS" kept its full width and the
815
+ toggle arrow was painted on top of its tail.
816
+
817
+ `min-width: 0` at each flex level is what lets the text box actually give
818
+ up width; the ellipsis then says so honestly. The arrow is pinned
819
+ unshrinkable on the other side — a dropdown with no visible handle stops
820
+ looking like a dropdown.
821
+
822
+ The vendored 27px min-heights on `__value`/`__tools` also go: inside the
823
+ 26px content box of the bordered 28px field they forced a 1px overflow that
824
+ read as the text sitting low. */
825
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
826
+ .inovua-react-toolkit-combo-box__value {
827
+ flex-wrap: nowrap !important;
828
+ min-width: 0 !important;
829
+ min-height: 0 !important;
830
+ height: 100% !important;
831
+ overflow: hidden !important;
832
+ }
833
+
834
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
835
+ .inovua-react-toolkit-combo-box__input__wrapper {
836
+ min-width: 0 !important;
837
+ overflow: hidden !important;
838
+ margin-bottom: 0 !important;
839
+ /* The vendored --ltr theme rule pads THIS wrapper 8px left as well as the
840
+ display value — stacked on the spans' own inset that made 16px. The
841
+ spans carry the one true inset; the wrapper carries none. */
842
+ padding: 0 !important;
843
+ }
844
+
845
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
846
+ .inovua-react-toolkit-combo-box__input__placeholder,
847
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
848
+ .inovua-react-toolkit-combo-box__value__display-value {
849
+ overflow: hidden !important;
850
+ text-overflow: ellipsis !important;
851
+ white-space: nowrap !important;
852
+ }
853
+
854
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
855
+ .inovua-react-toolkit-combo-box__tools {
856
+ flex: 0 0 auto !important;
857
+ min-height: 0 !important;
858
+ margin: 0 !important;
859
+ align-items: center !important;
768
860
  }
769
861
 
770
862
  .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
@@ -796,6 +888,34 @@ body.tablet-mode .InovuaReactDataGrid__cell.cell-word-wrap > div {
796
888
  padding-inline-end: 0 !important;
797
889
  }
798
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
+
799
919
  /* The keyboard highlight in a dropdown's option list — declared by the
800
920
  vendored theme, then painted with nothing.
801
921
 
@@ -1016,3 +1136,103 @@ body.tablet-mode .InovuaReactDataGrid__cell.cell-word-wrap > div {
1016
1136
  .visns-native-date-filter__input {
1017
1137
  font-size: 0.75rem !important;
1018
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
+ }
@@ -314,6 +314,26 @@ input[type]:not([type='search']):not([type='url']):not([type='hidden']):not(
314
314
  }
315
315
  }
316
316
 
317
+ /* THE DROPDOWN ARROW, restored. The rule further up gives `select` its
318
+ chevron and strips the native chrome (`appearance: none`), and then the
319
+ chained rule above this comment writes `background: white` — a SHORTHAND,
320
+ which resets background-image along with the colour. Net result shipped
321
+ for months: every native select rendered as a plain box visually identical
322
+ to a text input, with nothing saying "this opens".
323
+
324
+ This sits AFTER the chained rule so source order wins at equal
325
+ specificity, states only the longhands the shorthand clobbered, and
326
+ positions the chevron inside the field (the original hugged the raw right
327
+ edge) with the padding to keep long option text from running under it. */
328
+ select {
329
+ background-image: url(data:image/svg+xml;base64,PHN2ZyBpZD0iTGF5ZXJfMSIgZGF0YS1uYW1lPSJMYXllciAxIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0Ljk1IDEwIj48ZGVmcz48c3R5bGU+LmNscy0xe2ZpbGw6I2ZmZjt9LmNscy0ye2ZpbGw6IzQ0NDt9PC9zdHlsZT48L2RlZnM+PHRpdGxlPmFycm93czwvdGl0bGU+PHJlY3QgY2xhc3M9ImNscy0xIiB3aWR0aD0iNC45NSIgaGVpZ2h0PSIxMCIvPjxwb2x5Z29uIGNsYXNzPSJjbHMtMiIgcG9pbnRzPSIxLjQxIDQuNjcgMi40OCAzLjE4IDMuNTQgNC42NyAxLjQxIDQuNjciLz48cG9seWdvbiBjbGFzcz0iY2xzLTIiIHBvaW50cz0iMy41NCA1LjMzIDIuNDggNi44MiAxLjQxIDUuMzMgMy41NCA1LjMzIi8+PC9zdmc+);
330
+ background-repeat: no-repeat;
331
+ background-position: right 0.65rem center;
332
+ background-size: 7px 14px;
333
+ padding-right: 2rem;
334
+ cursor: pointer;
335
+ }
336
+
317
337
  /* A textarea is multi-line by definition, so it is the one control that opts
318
338
  out of the fixed height — it keeps the font and the horizontal padding, and
319
339
  takes the vertical padding that puts its FIRST row on the same line as the
@@ -119,6 +119,19 @@ const TicketConversation = ({
119
119
  <div className={styles.wrap}>
120
120
  {ticket && <TicketHeader ticket={ticket} counts={counts} />}
121
121
 
122
+ {/* The composer sits ABOVE the thread, and a posted reply is
123
+ PREPENDED: the server answers newest-first now, so the top of
124
+ the list is where the conversation currently is — writing
125
+ happens beside the message being answered, not three scrolls
126
+ below it under the original request. */}
127
+ {canReply && (
128
+ <Composer
129
+ base={base}
130
+ ticketId={ticketId}
131
+ onPosted={(entry) => setEntries((prev) => [entry, ...prev])}
132
+ />
133
+ )}
134
+
122
135
  {entries.length === 0 ? (
123
136
  <p className={styles.state}>
124
137
  Nothing has been said on this ticket yet.
@@ -130,14 +143,6 @@ const TicketConversation = ({
130
143
  ))}
131
144
  </ol>
132
145
  )}
133
-
134
- {canReply && (
135
- <Composer
136
- base={base}
137
- ticketId={ticketId}
138
- onPosted={(entry) => setEntries((prev) => [...prev, entry])}
139
- />
140
- )}
141
146
  </div>
142
147
  );
143
148
  };
@@ -42,9 +42,28 @@ export const confirmDialog = (options) => {
42
42
  data = null, // Optional data object from the selected row
43
43
  } = options;
44
44
 
45
- // Find Yes and No buttons if they exist
46
- const yesButton = buttons.find((btn) => btn.label === 'Yes');
47
- const noButton = buttons.find((btn) => btn.label === 'No');
45
+ /*
46
+ * WHICH BUTTON CONFIRMS IS NOT A LABEL LOOKUP ANY MORE.
47
+ *
48
+ * This used to find the confirm handler with `label === 'Yes'` and
49
+ * nothing else — so a dialog whose button said "Delete" rendered
50
+ * perfectly, showed a default "Yes" button, and ran NOTHING when it was
51
+ * pressed. Two real screens shipped in that state (ticket delete,
52
+ * customer delete): the reader confirmed a deletion and the app silently
53
+ * did not delete, which is the worst available failure — no error, no
54
+ * effect, no clue. App code even grew comments warning that every
55
+ * confirm button must be labelled "Yes".
56
+ *
57
+ * Now: "No" or "Cancel" (either case) is the cancel button; the FIRST
58
+ * button that is neither is the confirm, whatever it says — "Yes",
59
+ * "Delete", "Remove from this project". Its label is what the reader
60
+ * sees, which is how a destructive dialog should read anyway.
61
+ */
62
+ const isCancelLabel = (label) =>
63
+ typeof label === 'string' && ['no', 'cancel'].includes(label.toLowerCase());
64
+
65
+ const yesButton = buttons.find((btn) => !isCancelLabel(btn.label));
66
+ const noButton = buttons.find((btn) => isCancelLabel(btn.label));
48
67
 
49
68
  // Default button text
50
69
  const confirmButtonText = yesButton ? yesButton.label : 'Yes';