@visns-studio/visns-components 6.3.7 → 6.3.9

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.3.7",
94
+ "version": "6.3.9",
95
95
  "description": "Various packages to assist in the development of our Custom Applications.",
96
96
  "main": "src/index.js",
97
97
  "files": [
@@ -75,6 +75,13 @@ import {
75
75
  AlertCircle,
76
76
  CheckCheck,
77
77
  Plus,
78
+ // treeGroupBy parent rows: ChevronDown is the collapse control in BOTH
79
+ // states — it is rotated rather than swapped for ChevronRight, so the
80
+ // glyph turns between them instead of popping, and the direction it
81
+ // points is the state. ListPlus is the "add another to this group"
82
+ // affordance, which a bare Plus does not say on its own.
83
+ ChevronDown,
84
+ ListPlus,
78
85
  } from 'lucide-react';
79
86
  import styles from './styles/DataGrid.module.scss';
80
87
 
@@ -659,11 +666,11 @@ const DataGrid = forwardRef(
659
666
  }
660
667
 
661
668
  // Opt-in grouped display (ajaxSetting.treeGroupBy {key,
662
- // column, parentFields, countLabel, collapsible,
663
- // defaultCollapsed, icons}): consecutive rows sharing a
664
- // non-empty `key` value get a synthetic summary parent
665
- // row inserted above them. `parentFields` are lifted from
666
- // the first row onto the parent and blanked on the
669
+ // column, parentFields, parentFieldAliases, countLabel,
670
+ // collapsible, defaultCollapsed, icons}): consecutive rows
671
+ // sharing a non-empty `key` value get a synthetic summary
672
+ // parent row inserted above them. `parentFields` are lifted
673
+ // from the first row onto the parent and blanked on the
667
674
  // children, so shared details display once; children get
668
675
  // an "↳" prefix in the display column. Single rows stay
669
676
  // plain — unlike the grid's own groupBy, which forces a
@@ -678,6 +685,7 @@ const DataGrid = forwardRef(
678
685
  key: treeKey,
679
686
  column: treeDisplayColumn,
680
687
  parentFields = [],
688
+ parentFieldAliases = {},
681
689
  countLabel,
682
690
  collapsible,
683
691
  defaultCollapsed,
@@ -722,6 +730,35 @@ const DataGrid = forwardRef(
722
730
  });
723
731
  });
724
732
 
733
+ // `parentFieldAliases` ({columnField:
734
+ // sourceField}): the parent shows a DIFFERENT
735
+ // field in a column than its children do, so
736
+ // one column can carry the per-member value on
737
+ // members and the group's summary on the
738
+ // parent instead of spending a second column
739
+ // on the summary alone.
740
+ //
741
+ // Unlike `parentFields` this deliberately does
742
+ // NOT blank the column on the children — the
743
+ // members' own values are the reason the
744
+ // column exists. (Listing the same field in
745
+ // both would blank it on the children and
746
+ // defeat that, so pick one.)
747
+ //
748
+ // Distinct from `countLabel` too, which
749
+ // templates a summary from the child COUNT and
750
+ // so can only produce one fixed wording. An
751
+ // alias reads a field the backend computed,
752
+ // which is what it takes for the summary to
753
+ // vary with the data ("2 Headers" for one kind
754
+ // of group, "2 Stations" for another).
755
+ Object.entries(parentFieldAliases).forEach(
756
+ ([columnField, sourceField]) => {
757
+ parent[columnField] =
758
+ children[0]?.[sourceField];
759
+ }
760
+ );
761
+
725
762
  // Mark children for styling (getRowStyle) and
726
763
  // prefix their display value so nesting reads
727
764
  children.forEach((child) => {
@@ -1796,6 +1833,10 @@ const DataGrid = forwardRef(
1796
1833
  return CheckCheck;
1797
1834
  case 'plus':
1798
1835
  return Plus;
1836
+ // A separate id rather than a re-point of `plus`: every other
1837
+ // project's `plus` icon must keep the glyph it has always had.
1838
+ case 'listPlus':
1839
+ return ListPlus;
1799
1840
  case 'edit':
1800
1841
  return Edit;
1801
1842
  case 'envelope':
@@ -4110,9 +4151,125 @@ const DataGrid = forwardRef(
4110
4151
  };
4111
4152
  };
4112
4153
 
4154
+ /**
4155
+ * The group's collapse control.
4156
+ *
4157
+ * One glyph in both states: ChevronDown, turned a quarter
4158
+ * to point right when the group is shut. That is the
4159
+ * idiom every tree in every file manager uses, and it
4160
+ * animates — a "+"/"−" pair swaps two unrelated marks and
4161
+ * says nothing about direction. `relocated` picks the
4162
+ * class set, since the controls-column variant is sized
4163
+ * for a fingertip and must not inherit the inline one's
4164
+ * legacy dimensions.
4165
+ *
4166
+ * aria-expanded carries the state for assistive tech, so
4167
+ * the meaning does not depend on seeing the rotation.
4168
+ */
4169
+ const renderTreeCollapseControl = (
4170
+ groupValue,
4171
+ collapsed,
4172
+ relocated
4173
+ ) => (
4174
+ <button
4175
+ type="button"
4176
+ className={
4177
+ relocated
4178
+ ? `tree-control-btn ${styles.treeControlBtn} ${styles.treeControlChevron}`
4179
+ : `group-expand-btn ${styles.groupExpandBtn}`
4180
+ }
4181
+ aria-expanded={!collapsed}
4182
+ aria-label={
4183
+ collapsed ? 'Expand group' : 'Collapse group'
4184
+ }
4185
+ onClick={(e) => {
4186
+ e.stopPropagation();
4187
+ toggleTreeGroup(groupValue);
4188
+ }}
4189
+ data-tooltip-id="system-tooltip"
4190
+ data-tooltip-content={
4191
+ collapsed ? 'Expand group' : 'Collapse group'
4192
+ }
4193
+ >
4194
+ {relocated ? (
4195
+ <ChevronDown
4196
+ className={
4197
+ collapsed
4198
+ ? styles.treeChevronCollapsed
4199
+ : styles.treeChevronExpanded
4200
+ }
4201
+ strokeWidth={2.25}
4202
+ size={18}
4203
+ aria-hidden="true"
4204
+ />
4205
+ ) : collapsed ? (
4206
+ '+'
4207
+ ) : (
4208
+ '−'
4209
+ )}
4210
+ </button>
4211
+ );
4212
+
4213
+ /** The group's treeGroupBy.icons, in either placement. */
4214
+ const renderTreeGroupIcons = (groupValue, relocated) =>
4215
+ (ajaxSetting?.treeGroupBy?.icons || []).map(
4216
+ (iconConfig, index) => {
4217
+ if (
4218
+ !shouldShowGroupAction(
4219
+ iconConfig,
4220
+ groupValue
4221
+ )
4222
+ ) {
4223
+ return null;
4224
+ }
4225
+
4226
+ const IconComponent = getGroupIconComponent(
4227
+ iconConfig.id
4228
+ );
4229
+ const colour = iconConfig.colour || 'success';
4230
+
4231
+ return (
4232
+ <button
4233
+ type="button"
4234
+ key={`tree-action-${index}`}
4235
+ className={
4236
+ relocated
4237
+ ? `tree-control-btn tree-control-${iconConfig.id}-btn ${styles.treeControlBtn} ${styles[colour] || ''}`
4238
+ : `group-action-btn ${styles.groupActionBtn} ${styles[colour] || ''}`
4239
+ }
4240
+ aria-label={iconConfig.label}
4241
+ onClick={(e) => {
4242
+ e.stopPropagation();
4243
+ handleGroupAction(
4244
+ groupValue,
4245
+ iconConfig
4246
+ );
4247
+ }}
4248
+ data-tooltip-id="system-tooltip"
4249
+ data-tooltip-content={iconConfig.label}
4250
+ >
4251
+ <IconComponent
4252
+ strokeWidth={2}
4253
+ size={relocated ? 18 : 14}
4254
+ aria-hidden="true"
4255
+ />
4256
+ </button>
4257
+ );
4258
+ }
4259
+ );
4260
+
4113
4261
  // The tree parent's display column doubles as the group's
4114
4262
  // header line: expand/collapse plus any treeGroupBy.icons
4115
4263
  // whose show/showAll conditions the group's rows satisfy.
4264
+ //
4265
+ // `controlsColumn` (opt-in) moves those controls into
4266
+ // ANOTHER column's cell on parent rows, leaving the label
4267
+ // cell to the label alone. Parent rows blank most
4268
+ // interactive columns (TREE_PARENT_BLOCKED_TYPES), so a
4269
+ // grid usually has an empty cell going spare while the
4270
+ // label is squeezed between two buttons — this spends that
4271
+ // space instead. Unset (the default) keeps the controls
4272
+ // inline exactly as they have always rendered.
4116
4273
  const decorateTreeParentActions = (
4117
4274
  gridColumn,
4118
4275
  columnDef
@@ -4125,12 +4282,29 @@ const DataGrid = forwardRef(
4125
4282
  if (
4126
4283
  !tree?.key ||
4127
4284
  !tree.column ||
4128
- tree.column !== columnId ||
4129
4285
  !(tree.collapsible || tree.icons?.length)
4130
4286
  ) {
4131
4287
  return gridColumn;
4132
4288
  }
4133
4289
 
4290
+ // Pointing controlsColumn at the label column is the
4291
+ // inline layout, so treat it as unset rather than
4292
+ // rendering the controls into the cell twice.
4293
+ const controlsColumn =
4294
+ tree.controlsColumn &&
4295
+ tree.controlsColumn !== tree.column
4296
+ ? tree.controlsColumn
4297
+ : null;
4298
+
4299
+ const isLabelColumn = tree.column === columnId;
4300
+ const isControlsColumn =
4301
+ controlsColumn !== null &&
4302
+ controlsColumn === columnId;
4303
+
4304
+ if (!isLabelColumn && !isControlsColumn) {
4305
+ return gridColumn;
4306
+ }
4307
+
4134
4308
  const originalRender = gridColumn.render;
4135
4309
 
4136
4310
  return {
@@ -4147,28 +4321,44 @@ const DataGrid = forwardRef(
4147
4321
  const groupValue = data[tree.key];
4148
4322
  const collapsed = data.__treeCollapsed === true;
4149
4323
 
4324
+ // Relocated placement: this cell carries the
4325
+ // controls and nothing else.
4326
+ if (isControlsColumn) {
4327
+ return (
4328
+ <div
4329
+ className={
4330
+ styles.treeParentControls
4331
+ }
4332
+ onClick={(e) =>
4333
+ e.stopPropagation()
4334
+ }
4335
+ >
4336
+ {tree.collapsible &&
4337
+ renderTreeCollapseControl(
4338
+ groupValue,
4339
+ collapsed,
4340
+ true
4341
+ )}
4342
+ {renderTreeGroupIcons(
4343
+ groupValue,
4344
+ true
4345
+ )}
4346
+ </div>
4347
+ );
4348
+ }
4349
+
4150
4350
  return (
4151
4351
  <div
4152
4352
  className={styles.treeParentHeader}
4153
4353
  onClick={(e) => e.stopPropagation()}
4154
4354
  >
4155
- {tree.collapsible && (
4156
- <button
4157
- className={`group-expand-btn ${styles.groupExpandBtn}`}
4158
- onClick={(e) => {
4159
- e.stopPropagation();
4160
- toggleTreeGroup(groupValue);
4161
- }}
4162
- data-tooltip-id="system-tooltip"
4163
- data-tooltip-content={
4164
- collapsed
4165
- ? 'Expand group'
4166
- : 'Collapse group'
4167
- }
4168
- >
4169
- {collapsed ? '+' : '−'}
4170
- </button>
4171
- )}
4355
+ {!controlsColumn &&
4356
+ tree.collapsible &&
4357
+ renderTreeCollapseControl(
4358
+ groupValue,
4359
+ collapsed,
4360
+ false
4361
+ )}
4172
4362
  <strong
4173
4363
  className={styles.treeParentLabel}
4174
4364
  >
@@ -4177,53 +4367,11 @@ const DataGrid = forwardRef(
4177
4367
  ? ` (${data.__treeChildCount})`
4178
4368
  : ''}
4179
4369
  </strong>
4180
- {(tree.icons || []).map(
4181
- (iconConfig, index) => {
4182
- if (
4183
- !shouldShowGroupAction(
4184
- iconConfig,
4185
- groupValue
4186
- )
4187
- ) {
4188
- return null;
4189
- }
4190
-
4191
- const IconComponent =
4192
- getGroupIconComponent(
4193
- iconConfig.id
4194
- );
4195
-
4196
- return (
4197
- <button
4198
- key={`tree-action-${index}`}
4199
- className={`group-action-btn ${
4200
- styles.groupActionBtn
4201
- } ${
4202
- styles[
4203
- iconConfig.colour ||
4204
- 'success'
4205
- ] || ''
4206
- }`}
4207
- onClick={(e) => {
4208
- e.stopPropagation();
4209
- handleGroupAction(
4210
- groupValue,
4211
- iconConfig
4212
- );
4213
- }}
4214
- data-tooltip-id="system-tooltip"
4215
- data-tooltip-content={
4216
- iconConfig.label
4217
- }
4218
- >
4219
- <IconComponent
4220
- strokeWidth={2}
4221
- size={14}
4222
- />
4223
- </button>
4224
- );
4225
- }
4226
- )}
4370
+ {!controlsColumn &&
4371
+ renderTreeGroupIcons(
4372
+ groupValue,
4373
+ false
4374
+ )}
4227
4375
  </div>
4228
4376
  );
4229
4377
  },
@@ -1402,3 +1402,180 @@
1402
1402
  opacity: var(--grid-row-complete-opacity, 0.7);
1403
1403
  }
1404
1404
  }
1405
+
1406
+ /* =========================================================================
1407
+ treeGroupBy relocated group controls (`treeGroupBy.controlsColumn`)
1408
+
1409
+ When the controls move out of the label cell they stop being decoration
1410
+ squeezed beside a tag and become the row's primary affordance, so they are
1411
+ sized as targets rather than as glyphs. They also get class names of their
1412
+ own rather than reusing `.groupExpandBtn` / `.groupActionBtn`: consuming
1413
+ projects have `!important` rules pinned to those (hartway clamps them to
1414
+ 38px in tablet mode), and inheriting a size tuned for the cramped inline
1415
+ layout is exactly what this placement exists to escape.
1416
+
1417
+ Meaning never rests on colour: each control carries a glyph, the collapse
1418
+ state is carried by the chevron's rotation AND by aria-expanded, and the
1419
+ colour classes below are the same decorative palette the inline buttons
1420
+ use. A greyscale screen loses nothing.
1421
+
1422
+ Density hooks, per the `--nav-*` / `--grid-legend-*` convention above:
1423
+
1424
+ --grid-tree-control-size control box, pointer input. default: 32px
1425
+ --grid-tree-control-size-tablet control box under body.tablet-mode.
1426
+ default: 44px — the tap-target floor for
1427
+ a gloved hand on the plant floor, and the
1428
+ reason this placement is worth having.
1429
+ --grid-tree-control-gap space between controls. default: 6px
1430
+ ========================================================================= */
1431
+
1432
+ .treeParentControls {
1433
+ display: flex;
1434
+ align-items: center;
1435
+ justify-content: flex-start;
1436
+ gap: var(--grid-tree-control-gap, 6px);
1437
+ width: 100%;
1438
+ height: 100%;
1439
+ }
1440
+
1441
+ .treeControlBtn {
1442
+ flex-shrink: 0;
1443
+ display: inline-flex;
1444
+ align-items: center;
1445
+ justify-content: center;
1446
+ box-sizing: border-box;
1447
+ width: var(--grid-tree-control-size, 32px);
1448
+ height: var(--grid-tree-control-size, 32px);
1449
+ min-width: var(--grid-tree-control-size, 32px);
1450
+ min-height: var(--grid-tree-control-size, 32px);
1451
+ padding: 0;
1452
+ border: none;
1453
+ border-radius: var(--radius-sm, 6px);
1454
+ background: var(--primary-color, #3b82f6);
1455
+ color: white;
1456
+ cursor: pointer;
1457
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
1458
+ transition: background 0.2s ease, box-shadow 0.2s ease,
1459
+ transform 0.1s ease;
1460
+
1461
+ &:hover {
1462
+ box-shadow: 0 3px 6px rgba(0, 0, 0, 0.18);
1463
+ }
1464
+
1465
+ &:active {
1466
+ transform: scale(0.94);
1467
+ }
1468
+
1469
+ // A keyboard user has to be able to see which control is theirs; the grid
1470
+ // suppresses its own outline inside cells, so state it here.
1471
+ &:focus-visible {
1472
+ outline: 2px solid var(--primary-color, #3b82f6);
1473
+ outline-offset: 2px;
1474
+ }
1475
+
1476
+ // Colour variants, matching `.groupActionBtn`'s set so an icon config
1477
+ // reads the same in either placement.
1478
+ &.success {
1479
+ background: var(--success-color, #10b981);
1480
+ &:hover {
1481
+ background: var(--success-hover-color, #059669);
1482
+ }
1483
+ }
1484
+
1485
+ &.warning {
1486
+ background: var(--warning-color, #f59e0b);
1487
+ &:hover {
1488
+ background: var(--warning-hover-color, #d97706);
1489
+ }
1490
+ }
1491
+
1492
+ &.info {
1493
+ background: var(--info-color, #3b82f6);
1494
+ &:hover {
1495
+ background: var(--info-hover-color, #2563eb);
1496
+ }
1497
+ }
1498
+
1499
+ &.purple {
1500
+ background: var(--purple-color, #8b5cf6);
1501
+ &:hover {
1502
+ background: var(--purple-hover-color, #7c3aed);
1503
+ }
1504
+ }
1505
+
1506
+ &.pink {
1507
+ background: var(--pink-color, #ec4899);
1508
+ &:hover {
1509
+ background: var(--pink-hover-color, #db2777);
1510
+ }
1511
+ }
1512
+ }
1513
+
1514
+ /* The collapse control is deliberately quieter than the action buttons: it
1515
+ changes what you can see, not what the plant has done, so it reads as
1516
+ chrome rather than as a fourth coloured button competing with them. */
1517
+ .treeControlChevron {
1518
+ background: rgba(var(--paragraph-color-rgb, 34, 30, 51), 0.08);
1519
+ color: var(--paragraph-color, #374151);
1520
+ box-shadow: none;
1521
+
1522
+ &:hover {
1523
+ background: rgba(var(--paragraph-color-rgb, 34, 30, 51), 0.16);
1524
+ box-shadow: none;
1525
+ }
1526
+ }
1527
+
1528
+ .treeChevronExpanded,
1529
+ .treeChevronCollapsed {
1530
+ transition: transform 0.18s ease;
1531
+ }
1532
+
1533
+ .treeChevronExpanded {
1534
+ transform: rotate(0deg);
1535
+ }
1536
+
1537
+ /* Shut: the chevron points at the row it would open, the same way every
1538
+ file tree in the world points at a closed folder. */
1539
+ .treeChevronCollapsed {
1540
+ transform: rotate(-90deg);
1541
+ }
1542
+
1543
+ @media (prefers-reduced-motion: reduce) {
1544
+ .treeChevronExpanded,
1545
+ .treeChevronCollapsed {
1546
+ transition: none;
1547
+ }
1548
+ }
1549
+
1550
+ /* Tablet mode is a bench at arm's length worked with gloves on, so the
1551
+ controls step up to the 44px tap-target floor. Scoped to body.tablet-mode,
1552
+ the same signal the package already styles against in global-datagrid.css.
1553
+ The grid's tablet rows are 52px, so 44px still clears the row edges. */
1554
+ :global(body.tablet-mode) {
1555
+ .treeControlBtn {
1556
+ width: var(--grid-tree-control-size-tablet, 44px);
1557
+ height: var(--grid-tree-control-size-tablet, 44px);
1558
+ min-width: var(--grid-tree-control-size-tablet, 44px);
1559
+ min-height: var(--grid-tree-control-size-tablet, 44px);
1560
+ border-radius: var(--radius-md, 8px);
1561
+ }
1562
+
1563
+ .treeParentControls {
1564
+ gap: var(--grid-tree-control-gap, 8px);
1565
+
1566
+ /* A tablet row is a fixed 52px and the grid pads its cells 6px top
1567
+ and bottom, leaving a 40px content box — 4px short of the floor
1568
+ this placement exists to clear. This strip IS the whole cell's
1569
+ content and its buttons carry their own spacing, so it reclaims
1570
+ that padding rather than letting the control shrink to fit.
1571
+ Negative margin rather than overflow: the 44px control then sits
1572
+ inside the row's own 52px border box, so a cell that hides its
1573
+ overflow has nothing to clip.
1574
+
1575
+ --grid-tree-control-cell-inset cell padding to reclaim.
1576
+ default: 6px */
1577
+ margin-block: calc(
1578
+ -1 * var(--grid-tree-control-cell-inset, 6px)
1579
+ );
1580
+ }
1581
+ }