@visns-studio/visns-components 6.3.8 → 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.8",
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
 
@@ -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':
@@ -4140,9 +4151,125 @@ const DataGrid = forwardRef(
4140
4151
  };
4141
4152
  };
4142
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
+
4143
4261
  // The tree parent's display column doubles as the group's
4144
4262
  // header line: expand/collapse plus any treeGroupBy.icons
4145
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.
4146
4273
  const decorateTreeParentActions = (
4147
4274
  gridColumn,
4148
4275
  columnDef
@@ -4155,12 +4282,29 @@ const DataGrid = forwardRef(
4155
4282
  if (
4156
4283
  !tree?.key ||
4157
4284
  !tree.column ||
4158
- tree.column !== columnId ||
4159
4285
  !(tree.collapsible || tree.icons?.length)
4160
4286
  ) {
4161
4287
  return gridColumn;
4162
4288
  }
4163
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
+
4164
4308
  const originalRender = gridColumn.render;
4165
4309
 
4166
4310
  return {
@@ -4177,28 +4321,44 @@ const DataGrid = forwardRef(
4177
4321
  const groupValue = data[tree.key];
4178
4322
  const collapsed = data.__treeCollapsed === true;
4179
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
+
4180
4350
  return (
4181
4351
  <div
4182
4352
  className={styles.treeParentHeader}
4183
4353
  onClick={(e) => e.stopPropagation()}
4184
4354
  >
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
- )}
4355
+ {!controlsColumn &&
4356
+ tree.collapsible &&
4357
+ renderTreeCollapseControl(
4358
+ groupValue,
4359
+ collapsed,
4360
+ false
4361
+ )}
4202
4362
  <strong
4203
4363
  className={styles.treeParentLabel}
4204
4364
  >
@@ -4207,53 +4367,11 @@ const DataGrid = forwardRef(
4207
4367
  ? ` (${data.__treeChildCount})`
4208
4368
  : ''}
4209
4369
  </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
- )}
4370
+ {!controlsColumn &&
4371
+ renderTreeGroupIcons(
4372
+ groupValue,
4373
+ false
4374
+ )}
4257
4375
  </div>
4258
4376
  );
4259
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
+ }