@visns-studio/visns-components 6.24.1 → 6.24.3

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.
package/package.json CHANGED
@@ -93,7 +93,7 @@
93
93
  "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
94
94
  },
95
95
  "name": "@visns-studio/visns-components",
96
- "version": "6.24.1",
96
+ "version": "6.24.3",
97
97
  "description": "Various packages to assist in the development of our Custom Applications.",
98
98
  "main": "src/index.js",
99
99
  "files": [
@@ -1329,6 +1329,40 @@ export const renderFileColumn = ({
1329
1329
  handleDownload,
1330
1330
  styles,
1331
1331
  }) => {
1332
+ // `styles.tdaction` belongs on the WRAPPER, not on the glyph.
1333
+ //
1334
+ // The class is a 28px hit target — `display: inline-flex; width: 28px;
1335
+ // height: 28px` in DataGrid.module.scss, written so a row action is
1336
+ // something a finger can land on. Put it on an `<svg>` and those two
1337
+ // lengths do not draw a box around the icon, they ARE the icon: CSS
1338
+ // `width`/`height` outrank the `width="14"` / `height="14"` presentation
1339
+ // attributes lucide renders from `size`, so the declared 14px glyph was
1340
+ // painted at 28 — measured, not guessed — twice the size of the 13px row
1341
+ // text it sits in and twice the 18px the action column's own icons use.
1342
+ //
1343
+ // The action column never had the problem because its icons live inside
1344
+ // `.tdactions`, where `.tdactions span svg { width: 18px }` is the more
1345
+ // specific rule and wins. This column renders its icon on its own, so the
1346
+ // bare `.tdaction` was the only rule reaching it.
1347
+ //
1348
+ // Moving the class one level out is what the actions column already does:
1349
+ // the span becomes the 28px target (and picks up the hover wash and focus
1350
+ // ring that were being painted on the glyph), and the icon is free to be
1351
+ // the size it asks for. The span keeps its 10px right margin, so a row of
1352
+ // several files occupies exactly the width it did before.
1353
+ const icon = (tooltip) => (
1354
+ <Download
1355
+ data-tooltip-id="system-tooltip"
1356
+ data-tooltip-content={tooltip}
1357
+ strokeWidth={2}
1358
+ size={16}
1359
+ />
1360
+ );
1361
+
1362
+ // `inline-flex` comes from the class; declaring `display` here as well
1363
+ // would beat it from the style attribute and un-centre the glyph.
1364
+ const wrapperStyle = { marginRight: '10px' };
1365
+
1332
1366
  return {
1333
1367
  ...commonProps,
1334
1368
  name: column.id,
@@ -1341,48 +1375,30 @@ export const renderFileColumn = ({
1341
1375
  return data[column.id].map((file, index) => (
1342
1376
  <span
1343
1377
  key={index}
1378
+ className={styles.tdaction}
1344
1379
  onClick={(e) => {
1345
1380
  e.stopPropagation();
1346
1381
  e.preventDefault();
1347
1382
  handleDownload(file);
1348
1383
  }}
1349
- style={{
1350
- marginRight: '10px',
1351
- display: 'inline-block',
1352
- }}
1384
+ style={wrapperStyle}
1353
1385
  >
1354
- <Download
1355
- data-tooltip-id="system-tooltip"
1356
- data-tooltip-content={`Download File ${file.file_name}`}
1357
- strokeWidth={2}
1358
- size={14}
1359
- className={styles.tdaction}
1360
- />
1386
+ {icon(`Download File ${file.file_name}`)}
1361
1387
  </span>
1362
1388
  ));
1363
1389
  } else {
1364
1390
  // Return a single span for a single file with added margin
1365
1391
  return (
1366
1392
  <span
1393
+ className={styles.tdaction}
1367
1394
  onClick={(e) => {
1368
1395
  e.stopPropagation();
1369
1396
  e.preventDefault();
1370
1397
  handleDownload(data[column.id]);
1371
1398
  }}
1372
- style={{
1373
- marginRight: '10px',
1374
- display: 'inline-block',
1375
- }}
1399
+ style={wrapperStyle}
1376
1400
  >
1377
- <Download
1378
- data-tooltip-id="system-tooltip"
1379
- data-tooltip-content={`Download File ${
1380
- data[column.id].file_name
1381
- }`}
1382
- strokeWidth={2}
1383
- size={14}
1384
- className={styles.tdaction}
1385
- />
1401
+ {icon(`Download File ${data[column.id].file_name}`)}
1386
1402
  </span>
1387
1403
  );
1388
1404
  }
@@ -189,6 +189,68 @@ export const openedByGesture = (
189
189
  return !!node && !!node.contains && node.contains(gesture.target);
190
190
  };
191
191
 
192
+ /**
193
+ * Breathing room left between a widened menu and the edge that would clip it.
194
+ */
195
+ const MENU_GUTTER_PX = 8;
196
+
197
+ /**
198
+ * How much room the option menu has to grow into before the grid clips it, in
199
+ * pixels — written onto the combo as `--vs-filter-menu-avail` for
200
+ * `global-datagrid.css` to cap the menu's `max-width` with.
201
+ *
202
+ * The menu is a direct child of the combo, absolutely positioned, and the
203
+ * nearest ancestor that clips is `.InovuaReactDataGrid__body` (`overflow:
204
+ * hidden`, so the grid can virtualise). The stylesheet now widens the menu to
205
+ * fit its longest option instead of ellipsising it into the column's width —
206
+ * but CSS cannot ask "how far is it from this column to the right edge of the
207
+ * grid?", so a menu on a right-hand column would grow straight into that clip
208
+ * and come out sliced. This measures the answer.
209
+ *
210
+ * Purely presentational: it sets one custom property and touches nothing else.
211
+ * The option list, the value and every event are untouched, and if this never
212
+ * runs the stylesheet's own 420px / 90vw caps still apply.
213
+ *
214
+ * Measured on the COMBO, not the menu, and before the menu mounts, so the
215
+ * first painted frame is already the right width. Recomputed on every open, so
216
+ * a resized grid or a dragged column is picked up with nothing to invalidate.
217
+ */
218
+ export const measureMenuRoom = (node) => {
219
+ if (!node || typeof node.getBoundingClientRect !== 'function') {
220
+ return null;
221
+ }
222
+
223
+ const clip =
224
+ typeof node.closest === 'function'
225
+ ? node.closest('.InovuaReactDataGrid__body') ||
226
+ node.closest('.InovuaReactDataGrid')
227
+ : null;
228
+
229
+ const right = clip
230
+ ? clip.getBoundingClientRect().right
231
+ : typeof window !== 'undefined'
232
+ ? window.innerWidth
233
+ : 0;
234
+
235
+ const box = node.getBoundingClientRect();
236
+
237
+ // Never narrower than the field itself: a combo scrolled half out of view
238
+ // would otherwise be told it has no room at all. The stylesheet's
239
+ // `min-width` would fight that and win, but an honest floor is cheaper
240
+ // than relying on which cap happens to be stronger.
241
+ return Math.max(right - box.left - MENU_GUTTER_PX, box.width);
242
+ };
243
+
244
+ const applyMenuRoom = (node) => {
245
+ const room = measureMenuRoom(node);
246
+
247
+ if (room == null || !node || !node.style) {
248
+ return;
249
+ }
250
+
251
+ node.style.setProperty('--vs-filter-menu-avail', `${Math.round(room)}px`);
252
+ };
253
+
192
254
  /**
193
255
  * "Is something actually filtered here?" — `null` is the `select` filter
194
256
  * type's emptyValue, `''` is what DataGrid's dependent-filter reset writes,
@@ -360,6 +422,23 @@ const SelectFilterEditor = forwardRef((props, ref) => {
360
422
  editorProps.onExpandedChange(expanded);
361
423
  }
362
424
 
425
+ // Unconditional, and before the clear gate: every open gets
426
+ // a menu sized to the room it has, whether or not this
427
+ // particular open also clears the filter.
428
+ if (expanded) {
429
+ const instance = innerRef.current;
430
+ const combo =
431
+ instance &&
432
+ instance.getInputRef &&
433
+ instance.getInputRef();
434
+
435
+ applyMenuRoom(
436
+ combo && combo.getComboNode
437
+ ? combo.getComboNode()
438
+ : null
439
+ );
440
+ }
441
+
363
442
  clearRef.current(expanded);
364
443
  },
365
444
  }}
@@ -772,6 +772,28 @@ body.tablet-mode .InovuaReactDataGrid__cell.cell-word-wrap > div {
772
772
  border: 0 !important;
773
773
  border-radius: 0 !important;
774
774
  background: none !important;
775
+ /* The `filter input { padding: 0 0.5rem }` rule above reaches this inner
776
+ input too. On a text filter that padding is the text's inset; here the
777
+ input is the 1px typing sliver rendered BEFORE the placeholder span, so
778
+ 8px a side inflated an invisible element to ~17px and shoved the
779
+ "ALL …" text that far off the field's left edge. The visible text gets
780
+ its inset below instead. */
781
+ padding: 0 !important;
782
+ height: auto !important;
783
+ }
784
+
785
+ /* One inset for whichever span is doing the talking — the placeholder when
786
+ the filter is empty, the display value once something is picked. 6px here
787
+ plus the value area's own 2px lines the text up with the 8px inset of the
788
+ text and date filters beside it (the vendored theme gave the display value
789
+ 8px and the placeholder nothing, so the two states didn't even line up with
790
+ each other). Logical properties so RTL mirrors for free. */
791
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
792
+ .inovua-react-toolkit-combo-box__value__display-value,
793
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
794
+ .inovua-react-toolkit-combo-box__input__placeholder {
795
+ padding-inline-start: 6px !important;
796
+ padding-inline-end: 0 !important;
775
797
  }
776
798
 
777
799
  /* The keyboard highlight in a dropdown's option list — declared by the
@@ -788,15 +810,188 @@ body.tablet-mode .InovuaReactDataGrid__cell.cell-word-wrap > div {
788
810
  theme selector the invisible rule uses, so it lands exactly where that rule
789
811
  lands and nowhere else.
790
812
 
791
- `--primary-color` rather than one of the `--dgx-*` tokens above: those are
792
- defined on `.InovuaReactDataGrid`, and the option list is rendered outside
793
- the grid element, where they would not resolve. */
813
+ `--primary-color` rather than one of the `--dgx-*` tokens above, because
814
+ this selector is the vendored theme's, unscoped to the grid, and so also
815
+ matches a combo box that genuinely is outside `.InovuaReactDataGrid` (the
816
+ toolkit's `relativeToViewport` overlay), where a `--dgx-*` token would not
817
+ resolve. Inside a grid filter — which is where the list actually mounts,
818
+ see the block below — either would have worked. */
794
819
  .inovua-react-toolkit-combo-box__list--theme-default-light
795
820
  .inovua-react-toolkit-combo-box__list__item--active {
796
821
  border-color: color-mix(in srgb, var(--primary-color, #1b3933) 55%, transparent) !important;
797
822
  background: color-mix(in srgb, var(--primary-color, #1b3933) 12%, transparent) !important;
798
823
  }
799
824
 
825
+ /* ==========================================================================
826
+ The dropdown filter's OPTION MENU
827
+ --------------------------------------------------------------------------
828
+ Two complaints, one rule set.
829
+
830
+ 1. The options were being cut off. "Concrete, Concrete Precast & Concrete
831
+ Pumping" arrived as "CONCRETE, CONCRETE PRECAS…" and "General Building +
832
+ Construction Management" as "GENERAL BUILDING + CONSTR…", which makes two
833
+ different trades indistinguishable in the list you are choosing from.
834
+
835
+ That is not a text-length problem, it is a width problem. The vendored
836
+ base stylesheet pins the menu to the FIELD:
837
+
838
+ .inovua-react-toolkit-combo-box__list:not(…--relative-to-viewport) {
839
+ left: -1px; right: -1px; min-width: 100%; position: absolute;
840
+ }
841
+
842
+ Both edges anchored to a ~104px column means the menu can only ever be
843
+ as wide as the column, and every item carries
844
+ `…__list__item--ellipsis { overflow:hidden; white-space:nowrap;
845
+ text-overflow:ellipsis }`, so the text is trimmed to fit. Measured in a
846
+ real grid: item scrollWidth 355px inside clientWidth 102px.
847
+
848
+ Releasing the right edge and sizing to `max-content` lets the menu grow
849
+ to its widest option; `min-width` keeps it at least as wide as the field
850
+ it drops out of, and the cap keeps a pathological option (a 300-character
851
+ description) from painting a menu across the whole screen. Options longer
852
+ than the cap still ellipsize — that is the cap doing its job, not the bug
853
+ above.
854
+
855
+ 2. The menu did not look like anything else in the library. It kept the
856
+ vendored theme's 14px text, 1px radius, hard `0 0 4px rgba(0,0,0,.4)`
857
+ shadow and indigo `rgba(121,134,203,…)` highlights, while the grid around
858
+ it is 13px `--dgx-ink` on 6-8px radii and the app's own dropdowns
859
+ (react-select, `.visns-select__menu` in global.css) are a hairline
860
+ surface with `primary 8%` on the focused option. So the values below are
861
+ lifted from those two places rather than invented: the option size sits
862
+ on the grid's own type scale, the highlights are the house primary, and
863
+ the surface is the same popover treatment.
864
+
865
+ Scoping. The menu is NOT portalled — verified in a rendered grid, its parent
866
+ chain is `…__list > .inovua-react-toolkit-combo-box.InovuaReactDataGrid__column-header__filter
867
+ > …__filter-wrapper > …__column-header > … > .InovuaReactDataGrid`. It is a
868
+ direct child of the combo, which is exactly why `>` off the grid's own
869
+ filter class is enough to reach it and nothing else: a combo box anywhere
870
+ else in the app (the pagination toolbar's page-size picker, for one) carries
871
+ no `InovuaReactDataGrid__column-header__filter` and is untouched.
872
+
873
+ Because it is inside the grid element, the `--dgx-*` tokens declared on
874
+ `.InovuaReactDataGrid` DO resolve here (measured: `--dgx-ink` computes to
875
+ rgb(43,43,43) on a node appended to the menu). They are still written with
876
+ their own fallbacks so the rules survive if a future grid ever renders the
877
+ list through the toolkit's `relativeToViewport` overlay.
878
+
879
+ Presentational only. Nothing here changes what is in the list, what a click
880
+ does, or how the filter reports its value.
881
+ ========================================================================== */
882
+
883
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
884
+ > .inovua-react-toolkit-combo-box__list {
885
+ /* The width fix. `right: auto` is the load-bearing line — while both
886
+ edges are pinned the box is stretched to the column and `width` is
887
+ ignored. */
888
+ right: auto !important;
889
+ width: max-content !important;
890
+ min-width: calc(100% + 2px) !important;
891
+
892
+ /* Three caps, whichever bites first.
893
+
894
+ `420px` is the design cap — past that a menu stops being a list and
895
+ starts being a wall of text. `90vw` is the small-screen cap.
896
+
897
+ `--vs-filter-menu-avail` is the one CSS cannot work out for itself: the
898
+ distance from this column to the right edge of the grid. The menu is
899
+ absolutely positioned INSIDE `.InovuaReactDataGrid__body`, which is
900
+ `overflow: hidden`, so a menu on a right-hand column that grew past that
901
+ edge would simply be sliced off — visibly worse than the ellipsis this
902
+ change removes. The number is measured and written onto the combo by
903
+ SelectFilterEditor as the menu opens (see the comment there); when it is
904
+ absent — a combo outside a grid, or the editor not in play — the cap
905
+ falls back to `100vw` and the other two do the work. */
906
+ max-width: min(420px, 90vw, var(--vs-filter-menu-avail, 100vw)) !important;
907
+
908
+ /* The same popover surface as `.visns-select__menu`, on the grid's radius. */
909
+ padding: 4px !important;
910
+ border: 1px solid var(--dgx-line, rgba(43, 43, 43, 0.12)) !important;
911
+ border-radius: 8px !important;
912
+ background: var(--dgx-surface, var(--background-color, #fff)) !important;
913
+ box-shadow: 0 12px 28px -12px rgba(0, 0, 0, 0.28) !important;
914
+ }
915
+
916
+ /* An RTL combo grows the other way, or it would run off the field it belongs
917
+ to. The toolkit marks the direction on the combo, not on the list. */
918
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box.inovua-react-toolkit-combo-box--rtl
919
+ > .inovua-react-toolkit-combo-box__list {
920
+ right: -1px !important;
921
+ left: auto !important;
922
+ }
923
+
924
+ /* One option.
925
+
926
+ `text-transform` and `letter-spacing` are declared rather than left alone
927
+ because the column header two rows up IS uppercase-with-tracking
928
+ (`…__column-header__content`), and an option list is data, not a label — it
929
+ must not inherit that treatment from a host stylesheet. Note this cannot
930
+ un-shout an option whose VALUE is stored in capitals; that is the record,
931
+ not the styling.
932
+
933
+ `border` is deliberately absent: the theme gives every item
934
+ `border: 1px solid transparent` and the keyboard-highlight rule above
935
+ colours it, so declaring a border here would silently kill arrow-key
936
+ navigation's only visual. The radius is set so that highlight is rounded. */
937
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
938
+ > .inovua-react-toolkit-combo-box__list
939
+ .inovua-react-toolkit-combo-box__list__item {
940
+ padding: 0.375rem 0.625rem !important;
941
+ border-radius: 6px !important;
942
+ color: var(--dgx-ink, var(--paragraph-color, #2b2b2b)) !important;
943
+ font-size: 0.8125rem !important;
944
+ font-weight: 400 !important;
945
+ line-height: 1.45 !important;
946
+ letter-spacing: normal !important;
947
+ text-transform: none !important;
948
+ }
949
+
950
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
951
+ > .inovua-react-toolkit-combo-box__list
952
+ .inovua-react-toolkit-combo-box__list__item:not(.inovua-react-toolkit-combo-box__list__item--disabled):hover {
953
+ background: color-mix(in srgb, var(--primary-color, #1b3933) 8%, transparent) !important;
954
+ }
955
+
956
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
957
+ > .inovua-react-toolkit-combo-box__list
958
+ .inovua-react-toolkit-combo-box__list__item--selected {
959
+ background: color-mix(in srgb, var(--primary-color, #1b3933) 16%, transparent) !important;
960
+ font-weight: 600 !important;
961
+ }
962
+
963
+ /* Group headings and the "no options" line are the menu's own chrome, so they
964
+ take the quiet label treatment the column headers use rather than looking
965
+ like options you could pick. */
966
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
967
+ > .inovua-react-toolkit-combo-box__list
968
+ .inovua-react-toolkit-combo-box__list__group,
969
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
970
+ > .inovua-react-toolkit-combo-box__list
971
+ .inovua-react-toolkit-combo-box__list__empty-text {
972
+ padding: 0.375rem 0.625rem !important;
973
+ color: var(--dgx-muted, rgba(43, 43, 43, 0.6)) !important;
974
+ font-size: 0.6875rem !important;
975
+ font-weight: 600 !important;
976
+ letter-spacing: 0.05em !important;
977
+ text-transform: uppercase !important;
978
+ }
979
+
980
+ /* The field itself was 14px while the text, numeric and date filters beside it
981
+ are 0.75rem (the rule further up). The combo's displayed value and
982
+ placeholder are SPANS, not the `<input>` that rule reaches, which is how one
983
+ control in the filter row ended up a size larger than its neighbours. */
984
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box,
985
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
986
+ .inovua-react-toolkit-combo-box__value__display-value,
987
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
988
+ .inovua-react-toolkit-combo-box__input__placeholder,
989
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
990
+ .inovua-react-toolkit-combo-box__value__tag__label {
991
+ font-size: 0.75rem !important;
992
+ letter-spacing: normal !important;
993
+ }
994
+
800
995
  /* The native date filter is the odd one out in the row.
801
996
 
802
997
  Measured on /tickets: every other filter editor is 28px tall with a 6px