@visns-studio/visns-components 6.3.3 → 6.3.5
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/Field.jsx +15 -0
- package/src/components/Form.jsx +218 -3
- package/src/components/MultiCheckbox.jsx +13 -3
- package/src/components/generic/GenericDetail.jsx +16 -0
- package/src/components/styles/Form.module.scss +73 -31
- package/src/components/styles/GenericDetail.module.scss +93 -17
- package/src/components/styles/GenericIndex.module.scss +31 -14
- package/src/components/styles/TableFilter.module.scss +96 -62
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.
|
|
94
|
+
"version": "6.3.5",
|
|
95
95
|
"description": "Various packages to assist in the development of our Custom Applications.",
|
|
96
96
|
"main": "src/index.js",
|
|
97
97
|
"files": [
|
package/src/components/Field.jsx
CHANGED
|
@@ -2182,6 +2182,11 @@ function Field({
|
|
|
2182
2182
|
isEditable={settings.isEditable ?? false}
|
|
2183
2183
|
creatableConfig={settings.creatableConfig || {}}
|
|
2184
2184
|
editableConfig={settings.editableConfig || {}}
|
|
2185
|
+
// The checkbox renderer previously ignored the field's
|
|
2186
|
+
// read-only state, leaving `readOnlyWhen` configs
|
|
2187
|
+
// claiming a lock the UI didn't enforce — the same gap
|
|
2188
|
+
// the radio renderers had.
|
|
2189
|
+
disabled={isFieldReadOnly}
|
|
2185
2190
|
style={style}
|
|
2186
2191
|
/>
|
|
2187
2192
|
);
|
|
@@ -2306,6 +2311,11 @@ function Field({
|
|
|
2306
2311
|
data-name={settings.id}
|
|
2307
2312
|
type="radio"
|
|
2308
2313
|
className={`${styles.fi__customcheckbox} radio-input`}
|
|
2314
|
+
// Radio renderers previously ignored the
|
|
2315
|
+
// field's read-only state, leaving
|
|
2316
|
+
// `readOnlyWhen` configs claiming a lock
|
|
2317
|
+
// the UI didn't enforce.
|
|
2318
|
+
disabled={isFieldReadOnly}
|
|
2309
2319
|
value={option.id}
|
|
2310
2320
|
checked={
|
|
2311
2321
|
inputValue !== undefined &&
|
|
@@ -2336,6 +2346,11 @@ function Field({
|
|
|
2336
2346
|
data-name={settings.id}
|
|
2337
2347
|
type="radio"
|
|
2338
2348
|
className={`${styles.fi__customcheckbox} radio-input`}
|
|
2349
|
+
// Radio renderers previously ignored the
|
|
2350
|
+
// field's read-only state, leaving
|
|
2351
|
+
// `readOnlyWhen` configs claiming a lock
|
|
2352
|
+
// the UI didn't enforce.
|
|
2353
|
+
disabled={isFieldReadOnly}
|
|
2339
2354
|
value={option.id}
|
|
2340
2355
|
checked={
|
|
2341
2356
|
inputValue !== undefined &&
|
package/src/components/Form.jsx
CHANGED
|
@@ -91,6 +91,56 @@ const evaluateDisableWhen = (rule, data) => {
|
|
|
91
91
|
}
|
|
92
92
|
};
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Field visibility rules (`show`).
|
|
96
|
+
*
|
|
97
|
+
* A field carries `show: [{ id: '<other field id>', <condition> }]`, meaning
|
|
98
|
+
* "when MY value satisfies this condition, reveal the named field; otherwise
|
|
99
|
+
* hide it". With `self: true` the roles swap: the condition is read from the
|
|
100
|
+
* NAMED field's value and applied to the field carrying the rule.
|
|
101
|
+
*
|
|
102
|
+
* Supported conditions:
|
|
103
|
+
*
|
|
104
|
+
* { value: [...] } the value equals one of the listed literals. What
|
|
105
|
+
* toggles ({ value: [true] }) and radios
|
|
106
|
+
* ({ value: [1, 2] }) use.
|
|
107
|
+
*
|
|
108
|
+
* { minSelected: n } the value is an array holding at least n entries.
|
|
109
|
+
* For the multi-* types (multi-checkbox-ajax,
|
|
110
|
+
* multi-dropdown-ajax, …) whose value is a list of
|
|
111
|
+
* selected options — e.g. reveal a pair of per-item
|
|
112
|
+
* quantity inputs only once two boxes are ticked.
|
|
113
|
+
*
|
|
114
|
+
* Rules are evaluated when the record loads (the effect keyed on
|
|
115
|
+
* `fetchTrigger`) and again on every change to the field that carries them,
|
|
116
|
+
* so a revealed field cannot linger after the value that revealed it goes
|
|
117
|
+
* away. See also `required_rely` (required-ness that follows another field's
|
|
118
|
+
* value) and `sumEquals` (a group of numbers that must total a target).
|
|
119
|
+
*/
|
|
120
|
+
const hasShowCondition = (condition) =>
|
|
121
|
+
!!condition &&
|
|
122
|
+
(condition.minSelected !== undefined ||
|
|
123
|
+
(Array.isArray(condition.value) && condition.value.length > 0));
|
|
124
|
+
|
|
125
|
+
const matchesShowCondition = (condition, value) => {
|
|
126
|
+
if (!condition) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (condition.minSelected !== undefined) {
|
|
131
|
+
return (
|
|
132
|
+
Array.isArray(value) &&
|
|
133
|
+
value.length >= Number(condition.minSelected)
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (Array.isArray(condition.value)) {
|
|
138
|
+
return condition.value.includes(value);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return false;
|
|
142
|
+
};
|
|
143
|
+
|
|
94
144
|
function Form({
|
|
95
145
|
ajaxSetting,
|
|
96
146
|
api,
|
|
@@ -115,6 +165,12 @@ function Form({
|
|
|
115
165
|
const [inputClass, setInputClass] = useState({});
|
|
116
166
|
const [tableData, setTableData] = useState({ columns: [], dataSource: [] });
|
|
117
167
|
const [tableProps, setTableProps] = useState({});
|
|
168
|
+
// A `save.return.type: 'table'` form (the report filter rows in
|
|
169
|
+
// GenericDetail) renders nothing at all when the query comes back empty,
|
|
170
|
+
// so "no rows matched" and "you have not run it yet" looked identical: an
|
|
171
|
+
// unchanged blank panel under a "generated successfully" toast. This
|
|
172
|
+
// separates the two so the empty result can say so.
|
|
173
|
+
const [hasQueried, setHasQueried] = useState(false);
|
|
118
174
|
const [uploadProgress, setUploadProgress] = useState(0);
|
|
119
175
|
|
|
120
176
|
// Group selection state management
|
|
@@ -459,18 +515,71 @@ function Form({
|
|
|
459
515
|
});
|
|
460
516
|
};
|
|
461
517
|
|
|
518
|
+
/**
|
|
519
|
+
* Re-evaluate this field's `show` rules against the value it has just
|
|
520
|
+
* been given. The other two change handlers do this from the DOM
|
|
521
|
+
* node's `data-show`, which a select / multi-checkbox never emits, so
|
|
522
|
+
* without this a rule on one of those types would only ever run at
|
|
523
|
+
* load time — the reveal would not follow the user's clicks.
|
|
524
|
+
*/
|
|
525
|
+
const applyShowConditions = (newValue) => {
|
|
526
|
+
const sourceField = formSettings.fields.find((f) => f.id === id);
|
|
527
|
+
const conditions = sourceField?.show || [];
|
|
528
|
+
|
|
529
|
+
if (conditions.length === 0) {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const _fields = [...formSettings.fields];
|
|
534
|
+
let changed = false;
|
|
535
|
+
|
|
536
|
+
conditions.forEach((showObject) => {
|
|
537
|
+
if (!showObject?.id || !hasShowCondition(showObject)) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const targetIndex = _fields.findIndex(
|
|
542
|
+
(f) => f.id === showObject.id
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
if (targetIndex < 0) {
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const shouldShow = matchesShowCondition(showObject, newValue);
|
|
550
|
+
const target = showObject.self
|
|
551
|
+
? _fields.find((f) => f.id === id)
|
|
552
|
+
: _fields[targetIndex];
|
|
553
|
+
|
|
554
|
+
if (target) {
|
|
555
|
+
target.hide = !shouldShow;
|
|
556
|
+
changed = true;
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
if (changed && updateForm) {
|
|
561
|
+
updateForm((prevState) => ({
|
|
562
|
+
...prevState,
|
|
563
|
+
fields: _fields,
|
|
564
|
+
}));
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
|
|
462
568
|
// // Switch cases to handle different actions
|
|
463
569
|
switch (action.action) {
|
|
464
570
|
case 'select-option':
|
|
465
571
|
case 'create-option': // New case to handle create-option
|
|
466
572
|
case 'deselect-option': // Handle unchecking tags
|
|
467
573
|
handleSelectOption();
|
|
574
|
+
applyShowConditions(inputValue);
|
|
468
575
|
break;
|
|
469
576
|
case 'clear':
|
|
470
577
|
updateFormData({ [id]: '' });
|
|
578
|
+
applyShowConditions([]);
|
|
471
579
|
break;
|
|
472
580
|
case 'remove-value':
|
|
473
581
|
updateFormData({ [id]: inputValue });
|
|
582
|
+
applyShowConditions(inputValue);
|
|
474
583
|
break;
|
|
475
584
|
}
|
|
476
585
|
};
|
|
@@ -1297,6 +1406,83 @@ function Form({
|
|
|
1297
1406
|
_inputClass[item.id] = styles.inputError;
|
|
1298
1407
|
}
|
|
1299
1408
|
}
|
|
1409
|
+
|
|
1410
|
+
/**
|
|
1411
|
+
* `sumEquals`: a set of numeric fields that must add
|
|
1412
|
+
* up to a target.
|
|
1413
|
+
*
|
|
1414
|
+
* sumEquals: {
|
|
1415
|
+
* fields: ['qty_a', 'qty_b'],
|
|
1416
|
+
* equals: 'qty_total', // field id, or a number
|
|
1417
|
+
* message: '…' // optional override
|
|
1418
|
+
* }
|
|
1419
|
+
*
|
|
1420
|
+
* Declared once, on any one of the fields in the
|
|
1421
|
+
* group. Every named field is marked in error so the
|
|
1422
|
+
* user sees which inputs to reconcile, not just a
|
|
1423
|
+
* message.
|
|
1424
|
+
*
|
|
1425
|
+
* The rule is skipped while its own field is hidden,
|
|
1426
|
+
* so a group that only appears in a revealed section
|
|
1427
|
+
* (see the `show` documentation above) cannot block an
|
|
1428
|
+
* ordinary save. While visible it also stands in for
|
|
1429
|
+
* `required` on the whole group: a blank member is as
|
|
1430
|
+
* wrong as a wrong total.
|
|
1431
|
+
*/
|
|
1432
|
+
if (item.sumEquals && item.hide !== true) {
|
|
1433
|
+
const rule = item.sumEquals;
|
|
1434
|
+
const groupIds = Array.isArray(rule.fields)
|
|
1435
|
+
? rule.fields
|
|
1436
|
+
: [];
|
|
1437
|
+
|
|
1438
|
+
if (groupIds.length > 0) {
|
|
1439
|
+
const labelFor = (fieldId) =>
|
|
1440
|
+
fields.find((f) => f.id === fieldId)
|
|
1441
|
+
?.label || fieldId;
|
|
1442
|
+
const groupLabels = groupIds
|
|
1443
|
+
.map(labelFor)
|
|
1444
|
+
.join(' and ');
|
|
1445
|
+
const groupValues = groupIds.map(
|
|
1446
|
+
(fieldId) => formData[fieldId]
|
|
1447
|
+
);
|
|
1448
|
+
const incomplete = groupValues.some(
|
|
1449
|
+
(v) =>
|
|
1450
|
+
v === null ||
|
|
1451
|
+
v === undefined ||
|
|
1452
|
+
v === '' ||
|
|
1453
|
+
isNaN(Number(v))
|
|
1454
|
+
);
|
|
1455
|
+
|
|
1456
|
+
const target =
|
|
1457
|
+
typeof rule.equals === 'number'
|
|
1458
|
+
? rule.equals
|
|
1459
|
+
: Number(formData[rule.equals]);
|
|
1460
|
+
|
|
1461
|
+
const markGroup = () =>
|
|
1462
|
+
groupIds.forEach((fieldId) => {
|
|
1463
|
+
_inputClass[fieldId] =
|
|
1464
|
+
styles.inputError;
|
|
1465
|
+
});
|
|
1466
|
+
|
|
1467
|
+
if (incomplete) {
|
|
1468
|
+
validation += `${groupLabels} are required.<br/>`;
|
|
1469
|
+
markGroup();
|
|
1470
|
+
} else if (!isNaN(target)) {
|
|
1471
|
+
const sum = groupValues.reduce(
|
|
1472
|
+
(total, v) => total + Number(v),
|
|
1473
|
+
0
|
|
1474
|
+
);
|
|
1475
|
+
|
|
1476
|
+
if (sum !== target) {
|
|
1477
|
+
validation +=
|
|
1478
|
+
(rule.message ||
|
|
1479
|
+
`${groupLabels} must add up to ${target} - they add up to ${sum}.`) +
|
|
1480
|
+
'<br/>';
|
|
1481
|
+
markGroup();
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1300
1486
|
});
|
|
1301
1487
|
|
|
1302
1488
|
setInputClass(_inputClass);
|
|
@@ -2091,6 +2277,11 @@ function Form({
|
|
|
2091
2277
|
);
|
|
2092
2278
|
}
|
|
2093
2279
|
|
|
2280
|
+
// Set before the branch: an empty result is
|
|
2281
|
+
// still a result, and that is the case the
|
|
2282
|
+
// empty state exists for.
|
|
2283
|
+
setHasQueried(true);
|
|
2284
|
+
|
|
2094
2285
|
if (
|
|
2095
2286
|
res.data[formSettings.save.return.param]
|
|
2096
2287
|
?.length > 0
|
|
@@ -2665,9 +2856,9 @@ function Form({
|
|
|
2665
2856
|
const showConditions = field.show || [];
|
|
2666
2857
|
|
|
2667
2858
|
showConditions.forEach((showObject) => {
|
|
2668
|
-
const { id,
|
|
2859
|
+
const { id, self } = showObject;
|
|
2669
2860
|
|
|
2670
|
-
if (id != '' &&
|
|
2861
|
+
if (id != '' && hasShowCondition(showObject)) {
|
|
2671
2862
|
const targetField = formSettings.fields.find(
|
|
2672
2863
|
(f) => f.id === id
|
|
2673
2864
|
);
|
|
@@ -2684,7 +2875,10 @@ function Form({
|
|
|
2684
2875
|
valueChecker !== undefined &&
|
|
2685
2876
|
valueChecker !== null
|
|
2686
2877
|
) {
|
|
2687
|
-
const shouldShow =
|
|
2878
|
+
const shouldShow = matchesShowCondition(
|
|
2879
|
+
showObject,
|
|
2880
|
+
valueChecker
|
|
2881
|
+
);
|
|
2688
2882
|
|
|
2689
2883
|
if (self) {
|
|
2690
2884
|
field.hide = !shouldShow;
|
|
@@ -3300,6 +3494,27 @@ function Form({
|
|
|
3300
3494
|
/>
|
|
3301
3495
|
</div>
|
|
3302
3496
|
)}
|
|
3497
|
+
|
|
3498
|
+
{/* Empty result. Only ever after a run — before that the
|
|
3499
|
+
panel is a filter row waiting to be used, and telling
|
|
3500
|
+
the user there is nothing there would be wrong. Copy is
|
|
3501
|
+
overridable per report via `save.return.empty`. */}
|
|
3502
|
+
{hasQueried &&
|
|
3503
|
+
formSettings?.save?.return?.type === 'table' &&
|
|
3504
|
+
!(tableData?.dataSource?.length > 0) && (
|
|
3505
|
+
<div
|
|
3506
|
+
className={`${styles.formItem} ${styles.fwItem} ${styles.emptyResult}`}
|
|
3507
|
+
>
|
|
3508
|
+
<p>
|
|
3509
|
+
{formSettings.save.return.empty?.title ||
|
|
3510
|
+
'No records matched'}
|
|
3511
|
+
</p>
|
|
3512
|
+
<span>
|
|
3513
|
+
{formSettings.save.return.empty?.message ||
|
|
3514
|
+
'Nothing came back for these filters. Widen them and run the report again.'}
|
|
3515
|
+
</span>
|
|
3516
|
+
</div>
|
|
3517
|
+
)}
|
|
3303
3518
|
</form>
|
|
3304
3519
|
<div className={styles.polActions}>
|
|
3305
3520
|
{formSettings.hasOwnProperty('customButton') &&
|
|
@@ -16,6 +16,11 @@ function MultiCheckbox({
|
|
|
16
16
|
isEditable = false,
|
|
17
17
|
creatableConfig = {},
|
|
18
18
|
editableConfig = {},
|
|
19
|
+
// The checkbox renderer used to ignore the field's read-only state, so a
|
|
20
|
+
// `readOnlyWhen` config claimed a lock the UI never enforced (the same
|
|
21
|
+
// gap the radio renderers had). Disabling the boxes also takes the
|
|
22
|
+
// create/edit affordances away — they mutate the option list.
|
|
23
|
+
disabled = false,
|
|
19
24
|
}) {
|
|
20
25
|
const [checkboxOptions, setCheckboxOptions] = useState([]);
|
|
21
26
|
const [selectedValues, setSelectedValues] = useState([]);
|
|
@@ -75,8 +80,12 @@ function MultiCheckbox({
|
|
|
75
80
|
const filteredOptions = checkboxOptions;
|
|
76
81
|
|
|
77
82
|
const handleCheckboxChange = (option, isChecked) => {
|
|
83
|
+
if (disabled) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
78
87
|
let newSelected;
|
|
79
|
-
|
|
88
|
+
|
|
80
89
|
if (isChecked) {
|
|
81
90
|
// Add to selection
|
|
82
91
|
newSelected = [...selectedValues, option];
|
|
@@ -186,7 +195,7 @@ function MultiCheckbox({
|
|
|
186
195
|
<div className={`${styles.multiCheckbox} ${className || ''}`} style={style}>
|
|
187
196
|
|
|
188
197
|
{/* Create new tag section */}
|
|
189
|
-
{isCreatable && (
|
|
198
|
+
{isCreatable && !disabled && (
|
|
190
199
|
<div className={styles.createSection}>
|
|
191
200
|
{!isCreating ? (
|
|
192
201
|
<button
|
|
@@ -294,9 +303,10 @@ function MultiCheckbox({
|
|
|
294
303
|
checked={isSelected(option.id)}
|
|
295
304
|
onChange={(e) => handleCheckboxChange(option, e.target.checked)}
|
|
296
305
|
className={styles.checkbox}
|
|
306
|
+
disabled={disabled}
|
|
297
307
|
/>
|
|
298
308
|
<span className={styles.labelText}>{option.label}</span>
|
|
299
|
-
{isEditable && (
|
|
309
|
+
{isEditable && !disabled && (
|
|
300
310
|
<button
|
|
301
311
|
type="button"
|
|
302
312
|
onClick={(e) => {
|
|
@@ -2679,6 +2679,22 @@ function GenericDetail({
|
|
|
2679
2679
|
>
|
|
2680
2680
|
<span>{formConfig.title}</span>
|
|
2681
2681
|
</div>
|
|
2682
|
+
{/* Optional one-line subtitle. A
|
|
2683
|
+
report tab is a name and a set
|
|
2684
|
+
of controls with nothing saying
|
|
2685
|
+
what it returns; a tab that has
|
|
2686
|
+
something to say can now say
|
|
2687
|
+
it, and one that has not is
|
|
2688
|
+
unchanged. */}
|
|
2689
|
+
{formConfig.description && (
|
|
2690
|
+
<p
|
|
2691
|
+
className={
|
|
2692
|
+
styles.gridtxt__description
|
|
2693
|
+
}
|
|
2694
|
+
>
|
|
2695
|
+
{formConfig.description}
|
|
2696
|
+
</p>
|
|
2697
|
+
)}
|
|
2682
2698
|
<Form
|
|
2683
2699
|
key={`form-${activeTabConfig.id}`}
|
|
2684
2700
|
columnId={routeParams[urlParam]}
|
|
@@ -335,43 +335,54 @@ input[type='file'] {
|
|
|
335
335
|
|
|
336
336
|
|
|
337
337
|
|
|
338
|
+
/* The form's primary action (Save / View Report / Refresh).
|
|
339
|
+
|
|
340
|
+
This was a bespoke button — 1.25em type on 0.65rem padding, its own radius
|
|
341
|
+
token, its own transition curve, and a hover that swapped to pale blue on
|
|
342
|
+
blue. Sat next to `.btn` (the shared primary) in the same action bar, so the
|
|
343
|
+
two controls that are meant to read as a pair were a different height, a
|
|
344
|
+
different type size and a different hover. It is the same button as every
|
|
345
|
+
other primary in the library; it just needed to say so. */
|
|
338
346
|
.saveBtn {
|
|
339
|
-
|
|
340
|
-
display: inline-block;
|
|
341
|
-
position: relative;
|
|
342
|
-
padding: 0.65rem 1rem;
|
|
343
|
-
cursor: pointer;
|
|
344
|
-
font-size: 1rem;
|
|
345
|
-
color: var(--tertiary-color);
|
|
346
|
-
text-decoration: none;
|
|
347
|
-
overflow: hidden;
|
|
348
|
-
background: var(--primary-color);
|
|
349
|
-
border: 1px solid rgba(var(--primary-color--rgb), 1.1);
|
|
350
|
-
border-radius: var(--br);
|
|
351
|
-
outline: none;
|
|
352
|
-
transition: all 0.2s cubic-bezier(0.85, 0, 0.15, 1) 0s;
|
|
353
|
-
font-size: 1.25em;
|
|
354
|
-
|
|
355
|
-
&:hover {
|
|
356
|
-
color: var(--primary-color);
|
|
357
|
-
background: var(--highlight-color);
|
|
358
|
-
border: 1px solid rgba(var(--highlight-rgb), 1.05);
|
|
359
|
-
}
|
|
347
|
+
@include button-primary;
|
|
360
348
|
}
|
|
361
349
|
|
|
350
|
+
/* The inline form's action bar.
|
|
351
|
+
|
|
352
|
+
Was `position: fixed; bottom: 15px; right: 20px` with `float: left`
|
|
353
|
+
children: the buttons left the form entirely and parked in the corner of
|
|
354
|
+
the *viewport*. On a short form (the report filter rows in GenericDetail —
|
|
355
|
+
one row of dropdowns) that put "View Report" nowhere near the filters it
|
|
356
|
+
runs; on a form that returns a results grid it floated on top of the grid's
|
|
357
|
+
last rows and its horizontal scrollbar.
|
|
358
|
+
|
|
359
|
+
It is a footer to the form it belongs to, so it lives at the end of the
|
|
360
|
+
form. `sticky` keeps what the fixed positioning was actually for — the
|
|
361
|
+
action stays reachable while a long form or a 550px results grid is
|
|
362
|
+
scrolled — without detaching it from its panel. */
|
|
362
363
|
.polActions {
|
|
363
|
-
position:
|
|
364
|
-
bottom:
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
364
|
+
position: sticky;
|
|
365
|
+
bottom: 0;
|
|
366
|
+
z-index: 5;
|
|
367
|
+
display: flex;
|
|
368
|
+
align-items: center;
|
|
369
|
+
justify-content: flex-end;
|
|
370
|
+
gap: var(--spacing-sm, 0.5rem);
|
|
371
|
+
width: 100%;
|
|
372
|
+
margin-top: var(--spacing-lg, 1.5rem);
|
|
373
|
+
padding: var(--spacing-sm, 0.5rem) 0;
|
|
374
|
+
/* Opaque, or the form scrolls through the bar while it is stuck. */
|
|
375
|
+
background: var(--surface-color, #fff);
|
|
376
|
+
border-top: 1px solid var(--border-color, #e1e1e1);
|
|
368
377
|
}
|
|
369
378
|
|
|
370
|
-
.
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
379
|
+
/* One primary per bar. Download / Sort / Save & Exit are alternatives to the
|
|
380
|
+
main action, not competitors: identical geometry, no fill. Previously every
|
|
381
|
+
button in here was a filled brand-blue block, so a filter row offering
|
|
382
|
+
"Download Report" and "View Report" gave no clue which one was the thing
|
|
383
|
+
you came to do. */
|
|
384
|
+
.polActions .btn {
|
|
385
|
+
@include button-secondary;
|
|
375
386
|
}
|
|
376
387
|
|
|
377
388
|
/* For in-place editing mode */
|
|
@@ -421,6 +432,37 @@ input[type='file'] {
|
|
|
421
432
|
display: none;
|
|
422
433
|
}
|
|
423
434
|
|
|
435
|
+
/* Empty result for `save.return.type: 'table'` forms. Deliberately not a
|
|
436
|
+
card and not an alert: an empty report is an ordinary outcome, so it is a
|
|
437
|
+
quiet block of centred text on the panel it would have filled, drawn with
|
|
438
|
+
the same hairline the rest of the page separates things with. */
|
|
439
|
+
.emptyResult {
|
|
440
|
+
display: flex;
|
|
441
|
+
flex-direction: column;
|
|
442
|
+
align-items: center;
|
|
443
|
+
justify-content: center;
|
|
444
|
+
gap: var(--spacing-xs, 0.25rem);
|
|
445
|
+
padding: var(--spacing-2xl, 3rem) var(--spacing-md, 1rem);
|
|
446
|
+
text-align: center;
|
|
447
|
+
border: 1px dashed var(--border-color, #e1e1e1);
|
|
448
|
+
border-radius: var(--radius-md, 10px);
|
|
449
|
+
background: var(--alternate-color, transparent);
|
|
450
|
+
|
|
451
|
+
p {
|
|
452
|
+
margin: 0;
|
|
453
|
+
font-size: var(--font-size-md, 0.9375rem);
|
|
454
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
455
|
+
color: var(--heading-color, inherit);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
span {
|
|
459
|
+
max-width: 42ch;
|
|
460
|
+
font-size: var(--font-size-sm, 0.8125rem);
|
|
461
|
+
line-height: 1.5;
|
|
462
|
+
color: var(--muted-color, #6e7276);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
424
466
|
.buttonContainer {
|
|
425
467
|
display: flex;
|
|
426
468
|
justify-content: center;
|
|
@@ -86,15 +86,31 @@
|
|
|
86
86
|
gap: 0.5rem;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/* The tab rail. Identical spec to `.grid__subnav` in GenericIndex.module.scss
|
|
90
|
+
— the two pages share one rail, so keep them in step.
|
|
91
|
+
|
|
92
|
+
It was a white panel under a 50px brand-tinted blur, holding one tinted blue
|
|
93
|
+
pill per tab. Every item therefore carried the same weight as the selected
|
|
94
|
+
one, and a seven-tab rail (the reports page) read as a stack of seven
|
|
95
|
+
buttons rather than as navigation with one thing chosen. 6.3's answer
|
|
96
|
+
everywhere else on the page is a flat surface, a hairline and quiet ink, so
|
|
97
|
+
that is what the rail gets: unselected tabs are plain text on the panel, and
|
|
98
|
+
the brand blue is spent once, on the tab you are actually on. */
|
|
89
99
|
.grid__subnav {
|
|
100
|
+
/* `flex: 1` (basis 0) against `.grid__subcontent`'s `flex: 0 1 80%` is
|
|
101
|
+
left exactly as it was: `.grid__subrow` is `flex-wrap: wrap`, so a
|
|
102
|
+
flex-basis here large enough to overflow the row would wrap the content
|
|
103
|
+
panel onto its own line rather than shrink it. Only the treatment
|
|
104
|
+
changes below. */
|
|
90
105
|
flex: 1;
|
|
91
|
-
background:
|
|
106
|
+
background: var(--surface-color, #fff);
|
|
107
|
+
border: 1px solid var(--border-color, #e1e1e1);
|
|
92
108
|
border-radius: var(--br);
|
|
93
109
|
box-sizing: border-box;
|
|
94
|
-
padding:
|
|
110
|
+
padding: var(--spacing-xs, 0.25rem);
|
|
95
111
|
margin: 8px 0;
|
|
96
112
|
height: auto;
|
|
97
|
-
box-shadow:
|
|
113
|
+
box-shadow: none;
|
|
98
114
|
}
|
|
99
115
|
|
|
100
116
|
.grid__subnav > ul {
|
|
@@ -108,32 +124,54 @@
|
|
|
108
124
|
|
|
109
125
|
.grid__subnav > ul li {
|
|
110
126
|
width: 100%;
|
|
111
|
-
margin-bottom:
|
|
127
|
+
margin-bottom: var(--spacing-xs, 0.25rem);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.grid__subnav > ul li:last-child {
|
|
131
|
+
margin-bottom: 0;
|
|
112
132
|
}
|
|
113
133
|
|
|
114
134
|
.grid__subnav > ul li a {
|
|
115
135
|
width: 100%;
|
|
116
|
-
|
|
136
|
+
/* 0.35rem was a 5px-tall hit area for a control people click all day. */
|
|
137
|
+
padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
|
|
117
138
|
box-sizing: border-box;
|
|
118
139
|
display: block;
|
|
119
140
|
text-decoration: none;
|
|
120
|
-
background:
|
|
121
|
-
color: var(--
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
141
|
+
background: none;
|
|
142
|
+
color: var(--text-color, #374151);
|
|
143
|
+
font-size: var(--font-size-sm, 0.8125rem);
|
|
144
|
+
font-weight: var(--font-weight-medium, 500);
|
|
145
|
+
line-height: 1.35;
|
|
146
|
+
border-radius: var(--radius-sm, 6px);
|
|
147
|
+
transition:
|
|
148
|
+
background-color var(--speed, 0.18s) var(--ease, ease),
|
|
149
|
+
color var(--speed, 0.18s) var(--ease, ease);
|
|
125
150
|
cursor: pointer;
|
|
126
151
|
}
|
|
127
152
|
|
|
128
153
|
.grid__subnav > ul li:hover a {
|
|
129
|
-
|
|
130
|
-
|
|
154
|
+
/* Hover is a hint, not a commitment. It used to fill solid brand blue —
|
|
155
|
+
indistinguishable from the selected tab, so passing the mouse across the
|
|
156
|
+
rail made it look as though the selection was following the cursor. */
|
|
157
|
+
background: var(--hover-color, rgba(0, 0, 0, 0.04));
|
|
158
|
+
color: var(--heading-color, inherit);
|
|
131
159
|
}
|
|
132
160
|
|
|
133
161
|
.grid__subnav > ul li .subactive {
|
|
134
|
-
font-weight:
|
|
162
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
135
163
|
background: var(--primary-color);
|
|
136
|
-
|
|
164
|
+
/* Was --background-color, the page's off-white; white is what the button
|
|
165
|
+
and nav-pill treatments elsewhere put on this blue. */
|
|
166
|
+
color: #fff;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* `outline: none` used to sit on the link with nothing put back, so the rail
|
|
170
|
+
was unusable by keyboard. The app's own ring token, same as every other
|
|
171
|
+
control. */
|
|
172
|
+
.grid__subnav > ul li a:focus-visible {
|
|
173
|
+
outline: none;
|
|
174
|
+
box-shadow: var(--focus-ring, 0 0 0 3px rgba(0, 118, 192, 0.32));
|
|
137
175
|
}
|
|
138
176
|
|
|
139
177
|
.grid__minheight {
|
|
@@ -146,9 +184,12 @@
|
|
|
146
184
|
|
|
147
185
|
.grid__subcontent {
|
|
148
186
|
flex: 0 1 80%;
|
|
149
|
-
background: var(--
|
|
187
|
+
background: var(--surface-color, #fff);
|
|
150
188
|
border-radius: var(--br);
|
|
151
|
-
|
|
189
|
+
/* Was the brand blue at 5% — a tint so faint the panel had no edge at all
|
|
190
|
+
while the rail beside it did. The shared hairline, same as every other
|
|
191
|
+
surface on the page. */
|
|
192
|
+
border: 1px solid var(--border-color, #e1e1e1);
|
|
152
193
|
box-sizing: border-box;
|
|
153
194
|
padding: 2px;
|
|
154
195
|
margin: 8px 0;
|
|
@@ -163,6 +204,24 @@
|
|
|
163
204
|
margin: 0 0 30px 0;
|
|
164
205
|
}
|
|
165
206
|
|
|
207
|
+
/* The tab panel's own inset.
|
|
208
|
+
|
|
209
|
+
`.gridtxt` had no rule at all, and its container (`.grid__subcontent`,
|
|
210
|
+
`.grid__full`) carries `padding: 2px` — so a panel's section heading and any
|
|
211
|
+
form inside it sat 2px from the panel border. Only the overview `<ul>` was
|
|
212
|
+
readable, because it padded itself. On the reports page that meant the
|
|
213
|
+
filter row's dropdowns and date pickers were flush against the panel edge.
|
|
214
|
+
|
|
215
|
+
The inset belongs to the panel, not to one child of it, so it is stated
|
|
216
|
+
here once and the `<ul>` below gives up its horizontal padding to match.
|
|
217
|
+
The `<ul>`'s vertical padding stays: with several heading/list pairs in one
|
|
218
|
+
`.gridtxt` (the overview tabs) that is what separates one section from the
|
|
219
|
+
next. */
|
|
220
|
+
.gridtxt {
|
|
221
|
+
padding: var(--spacing-md, 1rem) var(--spacing-lg, 1.5rem)
|
|
222
|
+
var(--spacing-lg, 1.5rem);
|
|
223
|
+
}
|
|
224
|
+
|
|
166
225
|
.gridtxt__header {
|
|
167
226
|
width: 100%;
|
|
168
227
|
display: block;
|
|
@@ -192,6 +251,21 @@
|
|
|
192
251
|
font-weight: 700;
|
|
193
252
|
}
|
|
194
253
|
|
|
254
|
+
/* Optional one-line subtitle under a section heading (GenericDetail renders it
|
|
255
|
+
from a tab's `description`). Sits in the heading's own space — it belongs to
|
|
256
|
+
the label, not to the content — so it takes over the heading's bottom margin
|
|
257
|
+
rather than adding a third gap. */
|
|
258
|
+
.gridtxt__description {
|
|
259
|
+
/* Pulls back half of the heading's 1rem bottom margin, so the subtitle sits
|
|
260
|
+
close under its heading and the whole block is separated from the
|
|
261
|
+
content below rather than floating between the two. */
|
|
262
|
+
margin: calc(-1 * var(--spacing-sm, 0.5rem)) 0 var(--spacing-lg, 1.5rem);
|
|
263
|
+
max-width: 70ch;
|
|
264
|
+
font-size: var(--font-size-sm, 0.8125rem);
|
|
265
|
+
line-height: 1.55;
|
|
266
|
+
color: var(--muted-color, #6e7276);
|
|
267
|
+
}
|
|
268
|
+
|
|
195
269
|
.gridtxt > ul {
|
|
196
270
|
width: 100%;
|
|
197
271
|
display: flex;
|
|
@@ -200,7 +274,9 @@
|
|
|
200
274
|
flex-wrap: wrap;
|
|
201
275
|
list-style: none;
|
|
202
276
|
box-sizing: border-box;
|
|
203
|
-
padding
|
|
277
|
+
/* Horizontal padding removed — `.gridtxt` owns the panel inset now (see
|
|
278
|
+
the note there). The vertical padding separates stacked sections. */
|
|
279
|
+
padding: 0.85rem 0;
|
|
204
280
|
margin: 0;
|
|
205
281
|
}
|
|
206
282
|
|
|
@@ -114,15 +114,19 @@
|
|
|
114
114
|
gap: 0.5rem;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/* The tab rail. Identical spec to `.grid__subnav` in GenericDetail.module.scss
|
|
118
|
+
— the two pages share one rail, so keep them in step; the reasoning for the
|
|
119
|
+
treatment is written out there. */
|
|
117
120
|
.grid__subnav {
|
|
118
121
|
flex: 1;
|
|
119
|
-
background:
|
|
122
|
+
background: var(--surface-color, #fff);
|
|
123
|
+
border: 1px solid var(--border-color, #e1e1e1);
|
|
120
124
|
border-radius: var(--br);
|
|
121
125
|
box-sizing: border-box;
|
|
122
|
-
padding:
|
|
126
|
+
padding: var(--spacing-xs, 0.25rem);
|
|
123
127
|
margin: 8px 0;
|
|
124
128
|
height: auto;
|
|
125
|
-
box-shadow:
|
|
129
|
+
box-shadow: none;
|
|
126
130
|
}
|
|
127
131
|
|
|
128
132
|
.grid__subnav > ul {
|
|
@@ -136,32 +140,45 @@
|
|
|
136
140
|
|
|
137
141
|
.grid__subnav > ul li {
|
|
138
142
|
width: 100%;
|
|
139
|
-
margin-bottom:
|
|
143
|
+
margin-bottom: var(--spacing-xs, 0.25rem);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.grid__subnav > ul li:last-child {
|
|
147
|
+
margin-bottom: 0;
|
|
140
148
|
}
|
|
141
149
|
|
|
142
150
|
.grid__subnav > ul li a {
|
|
143
151
|
width: 100%;
|
|
144
|
-
padding: 0.
|
|
152
|
+
padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
|
|
145
153
|
box-sizing: border-box;
|
|
146
154
|
display: block;
|
|
147
155
|
text-decoration: none;
|
|
148
|
-
background:
|
|
149
|
-
color: var(--
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
156
|
+
background: none;
|
|
157
|
+
color: var(--text-color, #374151);
|
|
158
|
+
font-size: var(--font-size-sm, 0.8125rem);
|
|
159
|
+
font-weight: var(--font-weight-medium, 500);
|
|
160
|
+
line-height: 1.35;
|
|
161
|
+
border-radius: var(--radius-sm, 6px);
|
|
162
|
+
transition:
|
|
163
|
+
background-color var(--speed, 0.18s) var(--ease, ease),
|
|
164
|
+
color var(--speed, 0.18s) var(--ease, ease);
|
|
153
165
|
cursor: pointer;
|
|
154
166
|
}
|
|
155
167
|
|
|
156
168
|
.grid__subnav > ul li:hover a {
|
|
157
|
-
background: var(--
|
|
158
|
-
color: var(--
|
|
169
|
+
background: var(--hover-color, rgba(0, 0, 0, 0.04));
|
|
170
|
+
color: var(--heading-color, inherit);
|
|
159
171
|
}
|
|
160
172
|
|
|
161
173
|
.grid__subnav > ul li .subactive {
|
|
162
|
-
font-weight:
|
|
174
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
163
175
|
background: var(--primary-color);
|
|
164
|
-
color:
|
|
176
|
+
color: #fff;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.grid__subnav > ul li a:focus-visible {
|
|
180
|
+
outline: none;
|
|
181
|
+
box-shadow: var(--focus-ring, 0 0 0 3px rgba(0, 118, 192, 0.32));
|
|
165
182
|
}
|
|
166
183
|
|
|
167
184
|
.grid__subnav__alternate {
|
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The tab rail's own stylesheet, which predates the 6.3 token language: it was
|
|
3
|
+
* still drawing its own card (a hardcoded rgba drop shadow and radius) and its
|
|
4
|
+
* own type scale (0.85/0.9rem literals) around items that the host surface —
|
|
5
|
+
* `.grid__subnav` in GenericIndex/GenericDetail — already frames. Two nested
|
|
6
|
+
* cards, two shadows, and a rail whose type did not match anything else on the
|
|
7
|
+
* page. Everything here now reads the same tokens as the rest of the library,
|
|
8
|
+
* and the framing is left to the surface it sits on.
|
|
9
|
+
*
|
|
10
|
+
* Literal fallbacks throughout: a bare `var()` for a token a consuming app has
|
|
11
|
+
* not declared is invalid at computed-value time and the whole declaration is
|
|
12
|
+
* dropped, which is exactly how a rail like this loses its padding.
|
|
13
|
+
*/
|
|
14
|
+
|
|
1
15
|
.tableFilterContainer {
|
|
2
16
|
width: 100%;
|
|
3
17
|
position: relative;
|
|
@@ -9,11 +23,11 @@
|
|
|
9
23
|
list-style: none;
|
|
10
24
|
padding: 0;
|
|
11
25
|
margin: 0;
|
|
12
|
-
font-size: 0.
|
|
26
|
+
font-size: var(--font-size-sm, 0.8125rem);
|
|
13
27
|
width: 100%;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
28
|
+
/* No radius, no shadow, no `overflow: hidden`. The panel around this list
|
|
29
|
+
supplies the surface; the clipping additionally cut the focus ring off
|
|
30
|
+
the first and last items. */
|
|
17
31
|
}
|
|
18
32
|
|
|
19
33
|
/* Parent category styling */
|
|
@@ -29,19 +43,23 @@
|
|
|
29
43
|
display: flex;
|
|
30
44
|
justify-content: space-between;
|
|
31
45
|
align-items: center;
|
|
32
|
-
padding:
|
|
33
|
-
|
|
34
|
-
font-
|
|
46
|
+
padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
|
|
47
|
+
border-radius: var(--radius-sm, 6px);
|
|
48
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
49
|
+
font-size: var(--font-size-sm, 0.8125rem);
|
|
35
50
|
background-color: var(--primary-color, #000);
|
|
36
|
-
color:
|
|
37
|
-
|
|
51
|
+
color: #fff;
|
|
52
|
+
/* Was `all 0.2s`, which animates layout properties too. */
|
|
53
|
+
transition:
|
|
54
|
+
background-color var(--speed, 0.18s) var(--ease, ease),
|
|
55
|
+
color var(--speed, 0.18s) var(--ease, ease);
|
|
38
56
|
position: relative;
|
|
39
57
|
text-decoration: none;
|
|
40
58
|
}
|
|
41
59
|
|
|
42
60
|
.parentLink:hover {
|
|
43
|
-
background-color: var(--
|
|
44
|
-
color:
|
|
61
|
+
background-color: var(--primary-color-darker, #222) !important;
|
|
62
|
+
color: #fff !important;
|
|
45
63
|
text-decoration: none;
|
|
46
64
|
}
|
|
47
65
|
|
|
@@ -63,7 +81,9 @@
|
|
|
63
81
|
overflow: hidden;
|
|
64
82
|
transition: max-height 0.3s ease-out, opacity 0.3s ease-out;
|
|
65
83
|
opacity: 0;
|
|
66
|
-
|
|
84
|
+
/* Flush with the rail. The child links carry their own hover surface now,
|
|
85
|
+
so a tint behind the whole submenu only made a second panel edge. */
|
|
86
|
+
background: none;
|
|
67
87
|
}
|
|
68
88
|
|
|
69
89
|
.visibleMenu {
|
|
@@ -74,7 +94,7 @@
|
|
|
74
94
|
.childItem {
|
|
75
95
|
margin: 0;
|
|
76
96
|
position: relative;
|
|
77
|
-
border-bottom: 1px solid
|
|
97
|
+
border-bottom: 1px solid var(--border-color, #e1e1e1);
|
|
78
98
|
}
|
|
79
99
|
|
|
80
100
|
.childItem:last-child {
|
|
@@ -82,18 +102,20 @@
|
|
|
82
102
|
}
|
|
83
103
|
|
|
84
104
|
.childLink {
|
|
85
|
-
padding:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
font-
|
|
105
|
+
padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem) var(--spacing-sm, 0.5rem)
|
|
106
|
+
var(--spacing-xl, 1.75rem);
|
|
107
|
+
background: none;
|
|
108
|
+
color: var(--text-color, #333);
|
|
109
|
+
font-weight: var(--font-weight-regular, 400);
|
|
110
|
+
font-size: var(--font-size-sm, 0.8125rem);
|
|
111
|
+
border-radius: var(--radius-sm, 6px);
|
|
90
112
|
display: block;
|
|
91
113
|
text-decoration: none;
|
|
92
114
|
}
|
|
93
115
|
|
|
94
116
|
.childLink:hover {
|
|
95
117
|
background-color: var(--hover-color, #e8e8e8) !important;
|
|
96
|
-
color: var(--
|
|
118
|
+
color: var(--heading-color, #000) !important;
|
|
97
119
|
text-decoration: none;
|
|
98
120
|
}
|
|
99
121
|
|
|
@@ -104,37 +126,55 @@
|
|
|
104
126
|
color: rgba(var(--paragraph-color-rgb, 153, 153, 153), 0.6);
|
|
105
127
|
}
|
|
106
128
|
|
|
107
|
-
/* Active states
|
|
129
|
+
/* Active states. The brand fill is the one loud thing in the rail, and it
|
|
130
|
+
marks exactly one item — which is what makes it readable as a selection
|
|
131
|
+
rather than as decoration. */
|
|
108
132
|
.subactive {
|
|
109
|
-
font-weight: 600;
|
|
133
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
110
134
|
background: var(--primary-color, #000) !important;
|
|
111
|
-
|
|
135
|
+
/* Was --tertiary-color, which several projects set to their page
|
|
136
|
+
off-white; white is what sits on this blue elsewhere. */
|
|
137
|
+
color: #fff !important;
|
|
112
138
|
}
|
|
113
139
|
|
|
114
140
|
.subactivechildren {
|
|
115
|
-
font-weight: 500;
|
|
116
|
-
background-color: var(--
|
|
141
|
+
font-weight: var(--font-weight-medium, 500);
|
|
142
|
+
background-color: var(--primary-color-light, #f0f0f0) !important;
|
|
117
143
|
color: var(--primary-color, #000) !important;
|
|
118
144
|
}
|
|
119
145
|
|
|
120
|
-
/* Base link styling
|
|
146
|
+
/* Base link styling. Unselected tabs are text on the panel, not tinted
|
|
147
|
+
blocks — see the note on `.grid__subnav` in GenericDetail.module.scss. */
|
|
121
148
|
.link {
|
|
122
149
|
display: block;
|
|
123
|
-
padding:
|
|
150
|
+
padding: var(--spacing-sm, 0.5rem) var(--spacing-md, 1rem);
|
|
124
151
|
text-decoration: none;
|
|
125
|
-
background:
|
|
126
|
-
color: var(--
|
|
127
|
-
font-size: 0.
|
|
128
|
-
|
|
152
|
+
background: none;
|
|
153
|
+
color: var(--text-color, #333);
|
|
154
|
+
font-size: var(--font-size-sm, 0.8125rem);
|
|
155
|
+
font-weight: var(--font-weight-medium, 500);
|
|
156
|
+
border-radius: var(--radius-sm, 6px);
|
|
157
|
+
transition:
|
|
158
|
+
background-color var(--speed, 0.18s) var(--ease, ease),
|
|
159
|
+
color var(--speed, 0.18s) var(--ease, ease);
|
|
129
160
|
cursor: pointer;
|
|
130
161
|
}
|
|
131
162
|
|
|
132
163
|
.link:hover {
|
|
133
164
|
background: var(--hover-color, #e8e8e8) !important;
|
|
134
|
-
|
|
165
|
+
/* Was the brand blue, which on hover made an unselected tab look like the
|
|
166
|
+
selected one. */
|
|
167
|
+
color: var(--heading-color, #000) !important;
|
|
135
168
|
text-decoration: none;
|
|
136
169
|
}
|
|
137
170
|
|
|
171
|
+
.link:focus-visible,
|
|
172
|
+
.childLink:focus-visible,
|
|
173
|
+
.parentLink:focus-visible {
|
|
174
|
+
outline: none;
|
|
175
|
+
box-shadow: var(--focus-ring, 0 0 0 3px rgba(0, 118, 192, 0.32));
|
|
176
|
+
}
|
|
177
|
+
|
|
138
178
|
.activetab button {
|
|
139
179
|
font-weight: bold;
|
|
140
180
|
background: var(--hover-color, #e0e0e0) !important;
|
|
@@ -146,19 +186,20 @@
|
|
|
146
186
|
display: none;
|
|
147
187
|
align-items: center;
|
|
148
188
|
justify-content: space-between;
|
|
149
|
-
padding:
|
|
189
|
+
padding: var(--spacing-sm, 0.75rem) var(--spacing-md, 1rem);
|
|
150
190
|
background: var(--primary-color, #000);
|
|
151
|
-
color:
|
|
152
|
-
border-radius:
|
|
191
|
+
color: #fff;
|
|
192
|
+
border-radius: var(--radius-sm, 6px);
|
|
153
193
|
cursor: pointer;
|
|
154
|
-
margin-bottom:
|
|
194
|
+
margin-bottom: var(--spacing-sm, 0.5rem);
|
|
155
195
|
border: none;
|
|
156
|
-
box-shadow: 0 1px 3px rgba(
|
|
196
|
+
box-shadow: var(--shadow-sm, 0 1px 3px rgba(0, 0, 0, 0.2));
|
|
157
197
|
}
|
|
158
198
|
|
|
159
199
|
.activeFilterLabel {
|
|
160
|
-
font-weight: 600;
|
|
161
|
-
|
|
200
|
+
font-weight: var(--font-weight-semibold, 600);
|
|
201
|
+
font-size: var(--font-size-sm, 0.8125rem);
|
|
202
|
+
color: #fff;
|
|
162
203
|
}
|
|
163
204
|
|
|
164
205
|
.mobileMenuIcon {
|
|
@@ -175,7 +216,7 @@
|
|
|
175
216
|
display: block;
|
|
176
217
|
height: 2px;
|
|
177
218
|
width: 100%;
|
|
178
|
-
background-color:
|
|
219
|
+
background-color: #fff;
|
|
179
220
|
border-radius: 2px;
|
|
180
221
|
transition: all 0.3s ease;
|
|
181
222
|
}
|
|
@@ -206,20 +247,17 @@
|
|
|
206
247
|
display: none; /* Hide hamburger menu on tablets */
|
|
207
248
|
}
|
|
208
249
|
|
|
209
|
-
/*
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
250
|
+
/* Roomier tap targets. Only the padding moves: the literal 0.85/0.9rem
|
|
251
|
+
sizes that used to sit here re-declared the rail's type at a scale
|
|
252
|
+
nothing else on the page uses, so the labels changed size at 1024px for
|
|
253
|
+
no reason. Density is a padding question. */
|
|
254
|
+
.parentLink,
|
|
215
255
|
.link {
|
|
216
|
-
padding:
|
|
217
|
-
font-size: 0.85rem;
|
|
256
|
+
padding: 0.65rem var(--spacing-md, 1rem);
|
|
218
257
|
}
|
|
219
258
|
|
|
220
259
|
.childLink {
|
|
221
|
-
padding:
|
|
222
|
-
font-size: 0.85rem;
|
|
260
|
+
padding: 0.65rem var(--spacing-md, 1rem) 0.65rem var(--spacing-xl, 1.75rem);
|
|
223
261
|
}
|
|
224
262
|
}
|
|
225
263
|
|
|
@@ -240,25 +278,21 @@
|
|
|
240
278
|
display: block;
|
|
241
279
|
max-height: 1000px;
|
|
242
280
|
overflow-y: auto;
|
|
243
|
-
|
|
244
|
-
border
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
.parentLink {
|
|
250
|
-
padding: 10px 16px; /* Slightly larger touch targets for mobile */
|
|
251
|
-
font-size: 0.9rem;
|
|
281
|
+
padding: var(--spacing-xs, 0.25rem);
|
|
282
|
+
border: 1px solid var(--border-color, #e1e1e1);
|
|
283
|
+
border-radius: var(--br, 8px);
|
|
284
|
+
margin-bottom: var(--spacing-sm, 0.5rem);
|
|
285
|
+
box-shadow: var(--shadow-sm, 0 2px 5px rgba(0, 0, 0, 0.1));
|
|
252
286
|
}
|
|
253
287
|
|
|
288
|
+
/* Larger tap targets on a phone; the type scale is left alone, as above. */
|
|
289
|
+
.parentLink,
|
|
254
290
|
.link {
|
|
255
|
-
padding:
|
|
256
|
-
font-size: 0.85rem;
|
|
291
|
+
padding: 0.75rem var(--spacing-md, 1rem);
|
|
257
292
|
}
|
|
258
293
|
|
|
259
294
|
.childLink {
|
|
260
|
-
padding:
|
|
261
|
-
font-size: 0.85rem;
|
|
295
|
+
padding: 0.75rem var(--spacing-md, 1rem) 0.75rem var(--spacing-xl, 1.75rem);
|
|
262
296
|
}
|
|
263
297
|
|
|
264
298
|
.collapsibleMenu {
|