@visns-studio/visns-components 6.3.8 → 6.3.10

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.8",
94
+ "version": "6.3.10",
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
 
@@ -1826,6 +1833,10 @@ const DataGrid = forwardRef(
1826
1833
  return CheckCheck;
1827
1834
  case 'plus':
1828
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;
1829
1840
  case 'edit':
1830
1841
  return Edit;
1831
1842
  case 'envelope':
@@ -3738,9 +3749,29 @@ const DataGrid = forwardRef(
3738
3749
  entries.filter(isRowActionConfig);
3739
3750
  // Per-row conditions may hide row actions at
3740
3751
  // runtime, but the column must fit the maximum
3741
- const width = actionColumnWidthFor(
3742
- actionSettings.length + rowActions.length
3743
- );
3752
+ const actionCount =
3753
+ actionSettings.length + rowActions.length;
3754
+ const computedWidth =
3755
+ actionColumnWidthFor(actionCount);
3756
+ // An explicit config maxWidth may narrow the
3757
+ // column below the 100px floor (a single-icon
3758
+ // column doesn't need it), but never below
3759
+ // what the icons themselves need — tap targets
3760
+ // stay intact in tablet mode.
3761
+ const iconsNeed =
3762
+ actionCount *
3763
+ (isTabletMode ? 44 : 34) +
3764
+ 16;
3765
+ const width =
3766
+ column.maxWidth != null
3767
+ ? Math.max(
3768
+ iconsNeed,
3769
+ Math.min(
3770
+ column.maxWidth,
3771
+ computedWidth
3772
+ )
3773
+ )
3774
+ : computedWidth;
3744
3775
  return {
3745
3776
  ...commonProps,
3746
3777
  name: Array.isArray(column.id)
@@ -4140,9 +4171,125 @@ const DataGrid = forwardRef(
4140
4171
  };
4141
4172
  };
4142
4173
 
4174
+ /**
4175
+ * The group's collapse control.
4176
+ *
4177
+ * One glyph in both states: ChevronDown, turned a quarter
4178
+ * to point right when the group is shut. That is the
4179
+ * idiom every tree in every file manager uses, and it
4180
+ * animates — a "+"/"−" pair swaps two unrelated marks and
4181
+ * says nothing about direction. `relocated` picks the
4182
+ * class set, since the controls-column variant is sized
4183
+ * for a fingertip and must not inherit the inline one's
4184
+ * legacy dimensions.
4185
+ *
4186
+ * aria-expanded carries the state for assistive tech, so
4187
+ * the meaning does not depend on seeing the rotation.
4188
+ */
4189
+ const renderTreeCollapseControl = (
4190
+ groupValue,
4191
+ collapsed,
4192
+ relocated
4193
+ ) => (
4194
+ <button
4195
+ type="button"
4196
+ className={
4197
+ relocated
4198
+ ? `tree-control-btn ${styles.treeControlBtn} ${styles.treeControlChevron}`
4199
+ : `group-expand-btn ${styles.groupExpandBtn}`
4200
+ }
4201
+ aria-expanded={!collapsed}
4202
+ aria-label={
4203
+ collapsed ? 'Expand group' : 'Collapse group'
4204
+ }
4205
+ onClick={(e) => {
4206
+ e.stopPropagation();
4207
+ toggleTreeGroup(groupValue);
4208
+ }}
4209
+ data-tooltip-id="system-tooltip"
4210
+ data-tooltip-content={
4211
+ collapsed ? 'Expand group' : 'Collapse group'
4212
+ }
4213
+ >
4214
+ {relocated ? (
4215
+ <ChevronDown
4216
+ className={
4217
+ collapsed
4218
+ ? styles.treeChevronCollapsed
4219
+ : styles.treeChevronExpanded
4220
+ }
4221
+ strokeWidth={2.25}
4222
+ size={18}
4223
+ aria-hidden="true"
4224
+ />
4225
+ ) : collapsed ? (
4226
+ '+'
4227
+ ) : (
4228
+ '−'
4229
+ )}
4230
+ </button>
4231
+ );
4232
+
4233
+ /** The group's treeGroupBy.icons, in either placement. */
4234
+ const renderTreeGroupIcons = (groupValue, relocated) =>
4235
+ (ajaxSetting?.treeGroupBy?.icons || []).map(
4236
+ (iconConfig, index) => {
4237
+ if (
4238
+ !shouldShowGroupAction(
4239
+ iconConfig,
4240
+ groupValue
4241
+ )
4242
+ ) {
4243
+ return null;
4244
+ }
4245
+
4246
+ const IconComponent = getGroupIconComponent(
4247
+ iconConfig.id
4248
+ );
4249
+ const colour = iconConfig.colour || 'success';
4250
+
4251
+ return (
4252
+ <button
4253
+ type="button"
4254
+ key={`tree-action-${index}`}
4255
+ className={
4256
+ relocated
4257
+ ? `tree-control-btn tree-control-${iconConfig.id}-btn ${styles.treeControlBtn} ${styles[colour] || ''}`
4258
+ : `group-action-btn ${styles.groupActionBtn} ${styles[colour] || ''}`
4259
+ }
4260
+ aria-label={iconConfig.label}
4261
+ onClick={(e) => {
4262
+ e.stopPropagation();
4263
+ handleGroupAction(
4264
+ groupValue,
4265
+ iconConfig
4266
+ );
4267
+ }}
4268
+ data-tooltip-id="system-tooltip"
4269
+ data-tooltip-content={iconConfig.label}
4270
+ >
4271
+ <IconComponent
4272
+ strokeWidth={2}
4273
+ size={relocated ? 18 : 14}
4274
+ aria-hidden="true"
4275
+ />
4276
+ </button>
4277
+ );
4278
+ }
4279
+ );
4280
+
4143
4281
  // The tree parent's display column doubles as the group's
4144
4282
  // header line: expand/collapse plus any treeGroupBy.icons
4145
4283
  // whose show/showAll conditions the group's rows satisfy.
4284
+ //
4285
+ // `controlsColumn` (opt-in) moves those controls into
4286
+ // ANOTHER column's cell on parent rows, leaving the label
4287
+ // cell to the label alone. Parent rows blank most
4288
+ // interactive columns (TREE_PARENT_BLOCKED_TYPES), so a
4289
+ // grid usually has an empty cell going spare while the
4290
+ // label is squeezed between two buttons — this spends that
4291
+ // space instead. Unset (the default) keeps the controls
4292
+ // inline exactly as they have always rendered.
4146
4293
  const decorateTreeParentActions = (
4147
4294
  gridColumn,
4148
4295
  columnDef
@@ -4155,12 +4302,29 @@ const DataGrid = forwardRef(
4155
4302
  if (
4156
4303
  !tree?.key ||
4157
4304
  !tree.column ||
4158
- tree.column !== columnId ||
4159
4305
  !(tree.collapsible || tree.icons?.length)
4160
4306
  ) {
4161
4307
  return gridColumn;
4162
4308
  }
4163
4309
 
4310
+ // Pointing controlsColumn at the label column is the
4311
+ // inline layout, so treat it as unset rather than
4312
+ // rendering the controls into the cell twice.
4313
+ const controlsColumn =
4314
+ tree.controlsColumn &&
4315
+ tree.controlsColumn !== tree.column
4316
+ ? tree.controlsColumn
4317
+ : null;
4318
+
4319
+ const isLabelColumn = tree.column === columnId;
4320
+ const isControlsColumn =
4321
+ controlsColumn !== null &&
4322
+ controlsColumn === columnId;
4323
+
4324
+ if (!isLabelColumn && !isControlsColumn) {
4325
+ return gridColumn;
4326
+ }
4327
+
4164
4328
  const originalRender = gridColumn.render;
4165
4329
 
4166
4330
  return {
@@ -4177,28 +4341,44 @@ const DataGrid = forwardRef(
4177
4341
  const groupValue = data[tree.key];
4178
4342
  const collapsed = data.__treeCollapsed === true;
4179
4343
 
4344
+ // Relocated placement: this cell carries the
4345
+ // controls and nothing else.
4346
+ if (isControlsColumn) {
4347
+ return (
4348
+ <div
4349
+ className={
4350
+ styles.treeParentControls
4351
+ }
4352
+ onClick={(e) =>
4353
+ e.stopPropagation()
4354
+ }
4355
+ >
4356
+ {tree.collapsible &&
4357
+ renderTreeCollapseControl(
4358
+ groupValue,
4359
+ collapsed,
4360
+ true
4361
+ )}
4362
+ {renderTreeGroupIcons(
4363
+ groupValue,
4364
+ true
4365
+ )}
4366
+ </div>
4367
+ );
4368
+ }
4369
+
4180
4370
  return (
4181
4371
  <div
4182
4372
  className={styles.treeParentHeader}
4183
4373
  onClick={(e) => e.stopPropagation()}
4184
4374
  >
4185
- {tree.collapsible && (
4186
- <button
4187
- className={`group-expand-btn ${styles.groupExpandBtn}`}
4188
- onClick={(e) => {
4189
- e.stopPropagation();
4190
- toggleTreeGroup(groupValue);
4191
- }}
4192
- data-tooltip-id="system-tooltip"
4193
- data-tooltip-content={
4194
- collapsed
4195
- ? 'Expand group'
4196
- : 'Collapse group'
4197
- }
4198
- >
4199
- {collapsed ? '+' : '−'}
4200
- </button>
4201
- )}
4375
+ {!controlsColumn &&
4376
+ tree.collapsible &&
4377
+ renderTreeCollapseControl(
4378
+ groupValue,
4379
+ collapsed,
4380
+ false
4381
+ )}
4202
4382
  <strong
4203
4383
  className={styles.treeParentLabel}
4204
4384
  >
@@ -4207,53 +4387,11 @@ const DataGrid = forwardRef(
4207
4387
  ? ` (${data.__treeChildCount})`
4208
4388
  : ''}
4209
4389
  </strong>
4210
- {(tree.icons || []).map(
4211
- (iconConfig, index) => {
4212
- if (
4213
- !shouldShowGroupAction(
4214
- iconConfig,
4215
- groupValue
4216
- )
4217
- ) {
4218
- return null;
4219
- }
4220
-
4221
- const IconComponent =
4222
- getGroupIconComponent(
4223
- iconConfig.id
4224
- );
4225
-
4226
- return (
4227
- <button
4228
- key={`tree-action-${index}`}
4229
- className={`group-action-btn ${
4230
- styles.groupActionBtn
4231
- } ${
4232
- styles[
4233
- iconConfig.colour ||
4234
- 'success'
4235
- ] || ''
4236
- }`}
4237
- onClick={(e) => {
4238
- e.stopPropagation();
4239
- handleGroupAction(
4240
- groupValue,
4241
- iconConfig
4242
- );
4243
- }}
4244
- data-tooltip-id="system-tooltip"
4245
- data-tooltip-content={
4246
- iconConfig.label
4247
- }
4248
- >
4249
- <IconComponent
4250
- strokeWidth={2}
4251
- size={14}
4252
- />
4253
- </button>
4254
- );
4255
- }
4256
- )}
4390
+ {!controlsColumn &&
4391
+ renderTreeGroupIcons(
4392
+ groupValue,
4393
+ false
4394
+ )}
4257
4395
  </div>
4258
4396
  );
4259
4397
  },
@@ -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
+ }