@visns-studio/visns-components 6.4.0 → 6.4.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
@@ -91,7 +91,7 @@
91
91
  "react-dom": "^17.0.0 || ^18.0.0"
92
92
  },
93
93
  "name": "@visns-studio/visns-components",
94
- "version": "6.4.0",
94
+ "version": "6.4.2",
95
95
  "description": "Various packages to assist in the development of our Custom Applications.",
96
96
  "main": "src/index.js",
97
97
  "files": [
@@ -3645,7 +3645,13 @@ const DataGrid = forwardRef(
3645
3645
  defaultFlex: 1,
3646
3646
  sortable: sortable,
3647
3647
  link: column.link ?? {},
3648
- minWidth: column.minWidth ?? undefined,
3648
+ // Floor every column: the datagrid's flex solver
3649
+ // hands the last unclamped columns the leftover
3650
+ // width VERBATIM, which goes negative once the
3651
+ // clamped columns exceed the container (narrow
3652
+ // screens) — columns then render a few px wide
3653
+ // with no horizontal scrollbar ever appearing.
3654
+ minWidth: column.minWidth ?? 80,
3649
3655
  maxWidth: column.maxWidth ?? undefined,
3650
3656
  textAlign: column.textAlign ?? undefined,
3651
3657
  textVerticalAlign:
@@ -440,6 +440,7 @@ function Navigation({
440
440
  // Close the mobile drawer when navigating; no-op for the
441
441
  // header layout where the drawer state is never opened.
442
442
  <Link to={n.url} onClick={() => setOpen(false)}>
443
+ {NavItemIcon ? <NavItemIcon size={18} /> : null}
443
444
  {n.label}
444
445
  </Link>
445
446
  ) : (
@@ -733,6 +734,14 @@ function Navigation({
733
734
  </nav>
734
735
  </div>
735
736
  </aside>
737
+ {isOpen ? (
738
+ <button
739
+ type="button"
740
+ className={styles['drawer-backdrop']}
741
+ aria-label="Close menu"
742
+ onClick={() => setOpen(false)}
743
+ />
744
+ ) : null}
736
745
  <main className={styles.content}>
737
746
  <div className={styles['content-header']}>
738
747
  {navConfig.search?.enable === true && (
@@ -13,6 +13,7 @@ function TableFilter({
13
13
  const [visibleChildren, setVisibleChildren] = useState({});
14
14
  const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
15
15
  const isFirstRender = useRef(true);
16
+ const listRef = useRef(null);
16
17
 
17
18
  // Initialize visible children for all parent categories and set checkbox function availability
18
19
  useEffect(() => {
@@ -500,6 +501,10 @@ function TableFilter({
500
501
  setMobileMenuOpen(false);
501
502
  };
502
503
 
504
+ // `data-active` below marks the selected top-level item. It is what the
505
+ // mobile tab strip scrolls into view on mount; `filterTable` destructures
506
+ // only the dataset keys it names, so the extra attribute is inert to the
507
+ // click handling and to every existing consumer.
503
508
  const renderContent = (d) => {
504
509
  // Determine if this is a parent category
505
510
  const isParent = d.isParent === true;
@@ -519,6 +524,7 @@ function TableFilter({
519
524
  data-filtertype="parent"
520
525
  data-url={d.url || ''}
521
526
  data-value={d.hasOwnProperty('value') ? d.value : ''}
527
+ data-active={d.show === true ? 'true' : undefined}
522
528
  onClick={filterTable}
523
529
  >
524
530
  {d.label}
@@ -534,6 +540,7 @@ function TableFilter({
534
540
  data-filtertype="parent"
535
541
  data-url={d.url || ''}
536
542
  data-value={d.hasOwnProperty('value') ? d.value : ''}
543
+ data-active={d.show === true ? 'true' : undefined}
537
544
  onClick={filterTable}
538
545
  className={parentClass}
539
546
  >
@@ -616,21 +623,68 @@ function TableFilter({
616
623
 
617
624
  const activeLabel = activeFilter ? activeFilter.label : 'Select Option';
618
625
 
626
+ // A rail whose items are a flat list of tabs renders on narrow viewports as
627
+ // a single-row strip that pans with a thumb, instead of collapsing behind a
628
+ // hamburger of its own — the app shell already owns one, and the collapse
629
+ // cost two taps per switch. A rail with expandable parents keeps the
630
+ // collapse: a nested submenu has no single row to pan.
631
+ //
632
+ // Nothing here runs above the strip's breakpoint (759px, below): the class
633
+ // is only styled inside that media query, and `.mobileToggle` is
634
+ // `display: none` at every width above it, so leaving it out of the tree
635
+ // for a flat rail is invisible on desktop and tablet.
636
+ const isFlatStrip = !(filters || []).some(
637
+ (filter) =>
638
+ filter &&
639
+ filter.isParent === true &&
640
+ Array.isArray(filter.children) &&
641
+ filter.children.length > 0
642
+ );
643
+
644
+ const activeId = activeFilter ? activeFilter.id : null;
645
+
646
+ // Bring the selected tab into view when the strip is wider than its box.
647
+ // Guarded on overflow so this never scrolls anything at widths where the
648
+ // rail is a plain vertical list.
649
+ useEffect(() => {
650
+ if (!isFlatStrip) return;
651
+
652
+ const list = listRef.current;
653
+
654
+ if (!list || typeof list.querySelector !== 'function') return;
655
+ if (list.scrollWidth <= list.clientWidth) return;
656
+
657
+ const active = list.querySelector('[data-active="true"]');
658
+
659
+ if (active && typeof active.scrollIntoView === 'function') {
660
+ active.scrollIntoView({ block: 'nearest', inline: 'nearest' });
661
+ }
662
+ }, [isFlatStrip, activeId]);
663
+
619
664
  return (
620
- <div className={styles.tableFilterContainer}>
621
- <div className={styles.mobileToggle} onClick={toggleMobileMenu}>
622
- <span className={styles.activeFilterLabel}>{activeLabel}</span>
623
- <span
624
- className={`${styles.mobileMenuIcon} ${
625
- mobileMenuOpen ? styles.open : ''
626
- }`}
627
- >
628
- <span></span>
629
- <span></span>
630
- <span></span>
631
- </span>
632
- </div>
665
+ <div
666
+ className={`${styles.tableFilterContainer} ${
667
+ isFlatStrip ? styles.flatFilter : ''
668
+ }`}
669
+ >
670
+ {!isFlatStrip && (
671
+ <div className={styles.mobileToggle} onClick={toggleMobileMenu}>
672
+ <span className={styles.activeFilterLabel}>
673
+ {activeLabel}
674
+ </span>
675
+ <span
676
+ className={`${styles.mobileMenuIcon} ${
677
+ mobileMenuOpen ? styles.open : ''
678
+ }`}
679
+ >
680
+ <span></span>
681
+ <span></span>
682
+ <span></span>
683
+ </span>
684
+ </div>
685
+ )}
633
686
  <ul
687
+ ref={listRef}
634
688
  className={`${styles.tableFilter} ${
635
689
  mobileMenuOpen ? styles.mobileOpen : ''
636
690
  }`}
@@ -282,8 +282,10 @@ export const renderDateColumn = ({
282
282
  name: column.id,
283
283
  filterEditor: filterEditor,
284
284
  filterEditorProps: filterEditorProps,
285
- defaultWidth: 120, // Set default width for date columns
286
- minWidth: 120, // Set minimum width for date columns
285
+ // Config-first: these sit after the commonProps spread, so a literal
286
+ // here silently discards an explicit column.minWidth from the app.
287
+ defaultWidth: column.defaultWidth ?? 120,
288
+ minWidth: column.minWidth ?? 120,
287
289
  render: ({ data }) => {
288
290
  if (
289
291
  data &&
@@ -343,8 +345,9 @@ export const renderDateTimeColumn = ({
343
345
  name: column.id,
344
346
  filterEditor: filterEditor,
345
347
  filterEditorProps: filterEditorProps,
346
- defaultWidth: 160, // Set default width for datetime columns
347
- minWidth: 160, // Set minimum width for datetime columns
348
+ // Config-first — see renderDateColumn.
349
+ defaultWidth: column.defaultWidth ?? 160,
350
+ minWidth: column.minWidth ?? 160,
348
351
  render: ({ data }) => {
349
352
  if (
350
353
  data &&
@@ -2530,8 +2533,9 @@ export const renderDateRangeColumn = ({
2530
2533
  name: `${column.id[0]}.${column.id[1]}.`,
2531
2534
  filterEditor: filterEditor,
2532
2535
  filterEditorProps: filterEditorProps,
2533
- defaultWidth: 200, // Set default width for daterange columns
2534
- minWidth: 200, // Set minimum width for daterange columns
2536
+ // Config-first — see renderDateColumn.
2537
+ defaultWidth: column.defaultWidth ?? 200,
2538
+ minWidth: column.minWidth ?? 200,
2535
2539
  render: ({ data }) => {
2536
2540
  let value = '';
2537
2541
 
@@ -952,6 +952,12 @@
952
952
  }
953
953
  }
954
954
 
955
+ /* Rendered only while the mobile drawer is open; the phone block below
956
+ displays it. Desktop never opens the drawer, but the guard costs nothing. */
957
+ .drawer-backdrop {
958
+ display: none;
959
+ }
960
+
955
961
  @media (max-width: 1024px) {
956
962
  .hwrap {
957
963
  grid-template-columns: auto 1fr auto;
@@ -970,7 +976,10 @@
970
976
  }
971
977
 
972
978
  .aside--cms {
973
- width: 100%;
979
+ /* A panel, not a full-screen sheet: the page stays visible behind
980
+ the backdrop, which gives tap-outside-to-close and stops a short
981
+ nav list reading as a vast empty void. */
982
+ width: min(84vw, 320px);
974
983
  /* The content header is sticky at z-index 995, so the drawer sits
975
984
  below it: start the drawer under the 50px header instead of
976
985
  letting the header paint over the drawer's top row. This also
@@ -981,12 +990,45 @@
981
990
  transition: all 0.55s cubic-bezier(0.25, 0.8, 0.25, 1);
982
991
  will-change: transform;
983
992
  z-index: 600;
993
+ box-shadow: 24px 0 48px rgba(0, 0, 0, 0.35);
984
994
 
985
995
  /* The header already anchors the screen; the drawer's own logo
986
996
  plate would render as a second 50px brand row. */
987
997
  .logo {
988
998
  display: none;
989
999
  }
1000
+
1001
+ /* Phone-sized touch targets: the desktop rail's compact rows are
1002
+ finger-hostile at arm's length on site. */
1003
+ .app-nav .nav-item {
1004
+ padding: 0.2rem 0.65rem;
1005
+
1006
+ a {
1007
+ padding: 0.85rem 1rem;
1008
+ font-size: 1rem;
1009
+ line-height: 1.3;
1010
+ }
1011
+
1012
+ svg {
1013
+ width: 20px;
1014
+ height: 20px;
1015
+ }
1016
+ }
1017
+ }
1018
+
1019
+ .drawer-backdrop {
1020
+ display: block;
1021
+ position: fixed;
1022
+ top: 50px;
1023
+ left: 0;
1024
+ right: 0;
1025
+ bottom: 0;
1026
+ background: rgba(0, 0, 0, 0.45);
1027
+ border: none;
1028
+ margin: 0;
1029
+ padding: 0;
1030
+ appearance: none;
1031
+ z-index: 599;
990
1032
  }
991
1033
 
992
1034
  .aside--cms--open {
@@ -298,4 +298,104 @@
298
298
  .collapsibleMenu {
299
299
  border-left: none;
300
300
  }
301
+
302
+ /* ---------------------------------------------------------------------
303
+ Flat rails: a one-row tab strip instead of the hamburger.
304
+
305
+ `.flatFilter` is set by TableFilter on any rail whose items have no
306
+ expandable children, and exists only inside this media query — above
307
+ 759px the class matches nothing and the rail is byte-for-byte the
308
+ vertical list it has always been.
309
+
310
+ The strip pans horizontally with a thumb and marks the selection with a
311
+ 3px accent edge rather than the full brand fill: on a phone the fill was
312
+ a solid band the width of the screen, which read as a header rather than
313
+ as one tab out of several.
314
+
315
+ These selectors are deliberately longer than the ones they override —
316
+ `.subactive` and `.activetab button` carry `!important`, so only a more
317
+ specific `!important` can take the fill back off. --------------------- */
318
+
319
+ .flatFilter .tableFilter,
320
+ .flatFilter .tableFilter.mobileOpen {
321
+ display: flex;
322
+ flex-wrap: nowrap;
323
+ align-items: stretch;
324
+ gap: var(--spacing-xs, 0.25rem);
325
+ max-height: none;
326
+ overflow-x: auto;
327
+ overflow-y: hidden;
328
+ /* Momentum panning on iOS. */
329
+ -webkit-overflow-scrolling: touch;
330
+ /* The scrollbar is noise on a two-tab strip; the overflowing tab is
331
+ the affordance. */
332
+ scrollbar-width: none;
333
+ -ms-overflow-style: none;
334
+ /* The dropdown drew itself a card. A strip sits on the panel it is
335
+ already inside. */
336
+ padding: 0;
337
+ border: none;
338
+ border-radius: 0;
339
+ margin-bottom: 0;
340
+ /* No baseline rule under the row: every surface this rail sits on
341
+ (`.grid__subnav` in Profile, GenericIndex and GenericDetail) already
342
+ draws its own border a few pixels below, and a second hairline
343
+ alongside it read as a mistake. The 3px edge under the selected tab
344
+ is the whole marker. */
345
+ box-shadow: none;
346
+ }
347
+
348
+ .flatFilter .tableFilter::-webkit-scrollbar {
349
+ display: none;
350
+ }
351
+
352
+ .flatFilter .tableFilter > li {
353
+ flex: 0 0 auto;
354
+ margin: 0;
355
+ border-bottom: none;
356
+ }
357
+
358
+ /* Inactive tabs are muted labels, not tinted blocks. `white-space: nowrap`
359
+ is what keeps the row one row: without it a two-word tab wraps and the
360
+ strip grows a second line instead of overflowing. */
361
+ .flatFilter .tableFilter > li > .link,
362
+ .flatFilter .tableFilter > li > .parentLink,
363
+ .flatFilter .tableFilter > li > button {
364
+ display: block;
365
+ white-space: nowrap;
366
+ padding: 0.75rem var(--spacing-md, 1rem);
367
+ border: none;
368
+ border-bottom: 3px solid transparent;
369
+ border-radius: 0;
370
+ background: none !important;
371
+ color: var(--paragraph-color, #6b7280) !important;
372
+ font-size: var(--font-size-sm, 0.8125rem);
373
+ font-weight: var(--font-weight-medium, 500);
374
+ cursor: pointer;
375
+ }
376
+
377
+ /* The selected tab. `--primary-color` first because it is what the rail
378
+ already fills the active item with everywhere else; `--accent-color`
379
+ covers apps that only declare that one, and the literal keeps the edge
380
+ visible in an app that declares neither. */
381
+ .flatFilter .tableFilter > li > .subactive,
382
+ .flatFilter .tableFilter > li.activetab > button {
383
+ background: none !important;
384
+ color: var(--heading-color, #111) !important;
385
+ font-weight: var(--font-weight-semibold, 600);
386
+ border-bottom-color: var(
387
+ --primary-color,
388
+ var(--accent-color, #0076c0)
389
+ );
390
+ }
391
+
392
+ /* Touch devices keep :hover stuck on the last tap, which on the old rule
393
+ tinted whichever tab was tapped last and made it compete with the
394
+ selected one. */
395
+ .flatFilter .tableFilter > li > .link:hover,
396
+ .flatFilter .tableFilter > li > .parentLink:hover,
397
+ .flatFilter .tableFilter > li > button:hover {
398
+ background: none !important;
399
+ color: var(--heading-color, #111) !important;
400
+ }
301
401
  }