@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 +1 -1
- package/src/components/DataGrid.jsx +7 -1
- package/src/components/Navigation.jsx +9 -0
- package/src/components/TableFilter.jsx +67 -13
- package/src/components/columns/ColumnRenderers.jsx +10 -6
- package/src/components/styles/Navigation.module.scss +43 -1
- package/src/components/styles/TableFilter.module.scss +100 -0
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.
|
|
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
|
-
|
|
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
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
<span
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
286
|
-
|
|
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
|
-
|
|
347
|
-
|
|
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
|
-
|
|
2534
|
-
|
|
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
|
-
|
|
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
|
}
|