@visns-studio/visns-components 6.24.1 → 6.24.2

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.2",
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
  }}
@@ -788,15 +788,188 @@ body.tablet-mode .InovuaReactDataGrid__cell.cell-word-wrap > div {
788
788
  theme selector the invisible rule uses, so it lands exactly where that rule
789
789
  lands and nowhere else.
790
790
 
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. */
791
+ `--primary-color` rather than one of the `--dgx-*` tokens above, because
792
+ this selector is the vendored theme's, unscoped to the grid, and so also
793
+ matches a combo box that genuinely is outside `.InovuaReactDataGrid` (the
794
+ toolkit's `relativeToViewport` overlay), where a `--dgx-*` token would not
795
+ resolve. Inside a grid filter — which is where the list actually mounts,
796
+ see the block below — either would have worked. */
794
797
  .inovua-react-toolkit-combo-box__list--theme-default-light
795
798
  .inovua-react-toolkit-combo-box__list__item--active {
796
799
  border-color: color-mix(in srgb, var(--primary-color, #1b3933) 55%, transparent) !important;
797
800
  background: color-mix(in srgb, var(--primary-color, #1b3933) 12%, transparent) !important;
798
801
  }
799
802
 
803
+ /* ==========================================================================
804
+ The dropdown filter's OPTION MENU
805
+ --------------------------------------------------------------------------
806
+ Two complaints, one rule set.
807
+
808
+ 1. The options were being cut off. "Concrete, Concrete Precast & Concrete
809
+ Pumping" arrived as "CONCRETE, CONCRETE PRECAS…" and "General Building +
810
+ Construction Management" as "GENERAL BUILDING + CONSTR…", which makes two
811
+ different trades indistinguishable in the list you are choosing from.
812
+
813
+ That is not a text-length problem, it is a width problem. The vendored
814
+ base stylesheet pins the menu to the FIELD:
815
+
816
+ .inovua-react-toolkit-combo-box__list:not(…--relative-to-viewport) {
817
+ left: -1px; right: -1px; min-width: 100%; position: absolute;
818
+ }
819
+
820
+ Both edges anchored to a ~104px column means the menu can only ever be
821
+ as wide as the column, and every item carries
822
+ `…__list__item--ellipsis { overflow:hidden; white-space:nowrap;
823
+ text-overflow:ellipsis }`, so the text is trimmed to fit. Measured in a
824
+ real grid: item scrollWidth 355px inside clientWidth 102px.
825
+
826
+ Releasing the right edge and sizing to `max-content` lets the menu grow
827
+ to its widest option; `min-width` keeps it at least as wide as the field
828
+ it drops out of, and the cap keeps a pathological option (a 300-character
829
+ description) from painting a menu across the whole screen. Options longer
830
+ than the cap still ellipsize — that is the cap doing its job, not the bug
831
+ above.
832
+
833
+ 2. The menu did not look like anything else in the library. It kept the
834
+ vendored theme's 14px text, 1px radius, hard `0 0 4px rgba(0,0,0,.4)`
835
+ shadow and indigo `rgba(121,134,203,…)` highlights, while the grid around
836
+ it is 13px `--dgx-ink` on 6-8px radii and the app's own dropdowns
837
+ (react-select, `.visns-select__menu` in global.css) are a hairline
838
+ surface with `primary 8%` on the focused option. So the values below are
839
+ lifted from those two places rather than invented: the option size sits
840
+ on the grid's own type scale, the highlights are the house primary, and
841
+ the surface is the same popover treatment.
842
+
843
+ Scoping. The menu is NOT portalled — verified in a rendered grid, its parent
844
+ chain is `…__list > .inovua-react-toolkit-combo-box.InovuaReactDataGrid__column-header__filter
845
+ > …__filter-wrapper > …__column-header > … > .InovuaReactDataGrid`. It is a
846
+ direct child of the combo, which is exactly why `>` off the grid's own
847
+ filter class is enough to reach it and nothing else: a combo box anywhere
848
+ else in the app (the pagination toolbar's page-size picker, for one) carries
849
+ no `InovuaReactDataGrid__column-header__filter` and is untouched.
850
+
851
+ Because it is inside the grid element, the `--dgx-*` tokens declared on
852
+ `.InovuaReactDataGrid` DO resolve here (measured: `--dgx-ink` computes to
853
+ rgb(43,43,43) on a node appended to the menu). They are still written with
854
+ their own fallbacks so the rules survive if a future grid ever renders the
855
+ list through the toolkit's `relativeToViewport` overlay.
856
+
857
+ Presentational only. Nothing here changes what is in the list, what a click
858
+ does, or how the filter reports its value.
859
+ ========================================================================== */
860
+
861
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
862
+ > .inovua-react-toolkit-combo-box__list {
863
+ /* The width fix. `right: auto` is the load-bearing line — while both
864
+ edges are pinned the box is stretched to the column and `width` is
865
+ ignored. */
866
+ right: auto !important;
867
+ width: max-content !important;
868
+ min-width: calc(100% + 2px) !important;
869
+
870
+ /* Three caps, whichever bites first.
871
+
872
+ `420px` is the design cap — past that a menu stops being a list and
873
+ starts being a wall of text. `90vw` is the small-screen cap.
874
+
875
+ `--vs-filter-menu-avail` is the one CSS cannot work out for itself: the
876
+ distance from this column to the right edge of the grid. The menu is
877
+ absolutely positioned INSIDE `.InovuaReactDataGrid__body`, which is
878
+ `overflow: hidden`, so a menu on a right-hand column that grew past that
879
+ edge would simply be sliced off — visibly worse than the ellipsis this
880
+ change removes. The number is measured and written onto the combo by
881
+ SelectFilterEditor as the menu opens (see the comment there); when it is
882
+ absent — a combo outside a grid, or the editor not in play — the cap
883
+ falls back to `100vw` and the other two do the work. */
884
+ max-width: min(420px, 90vw, var(--vs-filter-menu-avail, 100vw)) !important;
885
+
886
+ /* The same popover surface as `.visns-select__menu`, on the grid's radius. */
887
+ padding: 4px !important;
888
+ border: 1px solid var(--dgx-line, rgba(43, 43, 43, 0.12)) !important;
889
+ border-radius: 8px !important;
890
+ background: var(--dgx-surface, var(--background-color, #fff)) !important;
891
+ box-shadow: 0 12px 28px -12px rgba(0, 0, 0, 0.28) !important;
892
+ }
893
+
894
+ /* An RTL combo grows the other way, or it would run off the field it belongs
895
+ to. The toolkit marks the direction on the combo, not on the list. */
896
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box.inovua-react-toolkit-combo-box--rtl
897
+ > .inovua-react-toolkit-combo-box__list {
898
+ right: -1px !important;
899
+ left: auto !important;
900
+ }
901
+
902
+ /* One option.
903
+
904
+ `text-transform` and `letter-spacing` are declared rather than left alone
905
+ because the column header two rows up IS uppercase-with-tracking
906
+ (`…__column-header__content`), and an option list is data, not a label — it
907
+ must not inherit that treatment from a host stylesheet. Note this cannot
908
+ un-shout an option whose VALUE is stored in capitals; that is the record,
909
+ not the styling.
910
+
911
+ `border` is deliberately absent: the theme gives every item
912
+ `border: 1px solid transparent` and the keyboard-highlight rule above
913
+ colours it, so declaring a border here would silently kill arrow-key
914
+ navigation's only visual. The radius is set so that highlight is rounded. */
915
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
916
+ > .inovua-react-toolkit-combo-box__list
917
+ .inovua-react-toolkit-combo-box__list__item {
918
+ padding: 0.375rem 0.625rem !important;
919
+ border-radius: 6px !important;
920
+ color: var(--dgx-ink, var(--paragraph-color, #2b2b2b)) !important;
921
+ font-size: 0.8125rem !important;
922
+ font-weight: 400 !important;
923
+ line-height: 1.45 !important;
924
+ letter-spacing: normal !important;
925
+ text-transform: none !important;
926
+ }
927
+
928
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
929
+ > .inovua-react-toolkit-combo-box__list
930
+ .inovua-react-toolkit-combo-box__list__item:not(.inovua-react-toolkit-combo-box__list__item--disabled):hover {
931
+ background: color-mix(in srgb, var(--primary-color, #1b3933) 8%, transparent) !important;
932
+ }
933
+
934
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
935
+ > .inovua-react-toolkit-combo-box__list
936
+ .inovua-react-toolkit-combo-box__list__item--selected {
937
+ background: color-mix(in srgb, var(--primary-color, #1b3933) 16%, transparent) !important;
938
+ font-weight: 600 !important;
939
+ }
940
+
941
+ /* Group headings and the "no options" line are the menu's own chrome, so they
942
+ take the quiet label treatment the column headers use rather than looking
943
+ like options you could pick. */
944
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
945
+ > .inovua-react-toolkit-combo-box__list
946
+ .inovua-react-toolkit-combo-box__list__group,
947
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
948
+ > .inovua-react-toolkit-combo-box__list
949
+ .inovua-react-toolkit-combo-box__list__empty-text {
950
+ padding: 0.375rem 0.625rem !important;
951
+ color: var(--dgx-muted, rgba(43, 43, 43, 0.6)) !important;
952
+ font-size: 0.6875rem !important;
953
+ font-weight: 600 !important;
954
+ letter-spacing: 0.05em !important;
955
+ text-transform: uppercase !important;
956
+ }
957
+
958
+ /* The field itself was 14px while the text, numeric and date filters beside it
959
+ are 0.75rem (the rule further up). The combo's displayed value and
960
+ placeholder are SPANS, not the `<input>` that rule reaches, which is how one
961
+ control in the filter row ended up a size larger than its neighbours. */
962
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box,
963
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
964
+ .inovua-react-toolkit-combo-box__value__display-value,
965
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
966
+ .inovua-react-toolkit-combo-box__input__placeholder,
967
+ .InovuaReactDataGrid__column-header__filter.inovua-react-toolkit-combo-box
968
+ .inovua-react-toolkit-combo-box__value__tag__label {
969
+ font-size: 0.75rem !important;
970
+ letter-spacing: normal !important;
971
+ }
972
+
800
973
  /* The native date filter is the odd one out in the row.
801
974
 
802
975
  Measured on /tickets: every other filter editor is 28px tall with a 6px