@visns-studio/visns-components 6.31.2 → 6.32.1

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/README.md CHANGED
@@ -9,6 +9,53 @@ A comprehensive React component library used by the VISNS Studio team for CRM an
9
9
 
10
10
  VISNS Components is a React-based UI component library that provides a set of reusable, consistent, and customizable components for building web applications. It includes components for authentication, data grids, forms, navigation, and more, designed to work seamlessly together.
11
11
 
12
+ ## Recent Updates (v6.32.1)
13
+
14
+ ### Boxed overview: one card per section, no note accent
15
+
16
+ Follow-up to 6.32.0's opt-in boxed overview. The tinted heading bar alone did
17
+ not separate sections once every field was a box, so in boxed mode each
18
+ section is now its own bordered card with its heading as the card's header
19
+ strip — Transport and PPE Requirement read as two things rather than one long
20
+ run of boxes. Note-shaped fields (Description, Comments) are ordinary fields
21
+ here too: the same neutral border as every other box, with no accent colour.
22
+ Plain mode renders byte-identical markup; nothing changes for apps that did
23
+ not opt in.
24
+
25
+ ## Recent Updates (v6.32.0)
26
+
27
+ ### The pre-6.15 boxed overview is back, as an opt-in
28
+
29
+ 6.15.0 rebuilt the `GenericDetail` overview tab: one card per section, fields
30
+ as stacked label-above-value pairs on a two-column grid, the heading
31
+ left-aligned on a hairline. That stays the default. But a project whose users
32
+ read their detail pages as a grid of discrete facts — every field in its own
33
+ tinted, bordered box with a run-in `Label: value`, under a centred tinted
34
+ heading bar — can now ask for the old treatment back across the whole app,
35
+ the same way `setDefaultFormLayout` works:
36
+
37
+ ```jsx
38
+ import { setDefaultOverviewStyle } from '@visns-studio/visns-components';
39
+
40
+ setDefaultOverviewStyle('boxed'); // once, at bootstrap, before the first render
41
+ ```
42
+
43
+ A single page can go the other way, or opt in on its own, from its view
44
+ config: `"page": { "overviewStyle": "boxed" }` — or `"plain"` — wins over the
45
+ project default. `'plain'` is the shipped default, so an app that calls
46
+ nothing is completely unchanged. `getDefaultOverviewStyle()` is exported too.
47
+
48
+ ### The title bar's counts no longer run together
49
+
50
+ `GenericIndex` prints each `page.tableInfo.heading` and the "N total" count as
51
+ its own span in the title bar. Until 6.15.0 those spans carried their own
52
+ spacing — `[0 Enquiry Generated Today] ` — and when the brackets were retired
53
+ the only space between them went with them, so a leads page read
54
+ "0 Enquiry Generated Today0 Enquiry with Follow Ups79 total". The tab-strip and
55
+ embedded rows already laid the same node out as a flex row with a 0.75rem gap;
56
+ the `.crmtitle` / `.crmtitleSimple` bar now does the same (wrapping when the
57
+ bar is narrow). CSS only; no markup or config change.
58
+
12
59
  ## Recent Updates (v6.31.2)
13
60
 
14
61
  ### `MergeEntity` compares the two records it is actually merging
package/package.json CHANGED
@@ -93,7 +93,7 @@
93
93
  "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
94
94
  },
95
95
  "name": "@visns-studio/visns-components",
96
- "version": "6.31.2",
96
+ "version": "6.32.1",
97
97
  "description": "Various packages to assist in the development of our Custom Applications.",
98
98
  "main": "src/index.js",
99
99
  "files": [
@@ -48,6 +48,7 @@ import { toast } from 'react-toastify';
48
48
  import imageCompression from 'browser-image-compression';
49
49
  import fetchUtil from '../../utils/fetchUtil';
50
50
  import { resolveFormVariant } from '../../utils/formLayout';
51
+ import { resolveOverviewStyle } from '../../utils/overviewStyle';
51
52
  import {
52
53
  CheckCircle as CircleCheck,
53
54
  Copy,
@@ -290,6 +291,31 @@ function GenericDetail({
290
291
  const actionPlacement = resolveEditPlacement(setting, editPlacement);
291
292
  const drawActionsInHeader = isHeaderPlacement(actionPlacement);
292
293
 
294
+ /**
295
+ * `'plain'` (the 6.15 card) unless this page's config asks for the older
296
+ * boxed fields, or the app set `setDefaultOverviewStyle('boxed')` at
297
+ * bootstrap. Resolved once per render so every section of the overview
298
+ * draws the same way.
299
+ */
300
+ const overviewStyle = resolveOverviewStyle(page?.overviewStyle);
301
+ const overviewIsBoxed = overviewStyle === 'boxed';
302
+
303
+ /**
304
+ * How one overview section is wrapped.
305
+ *
306
+ * Boxed mode draws every section as its own card, so it needs a real
307
+ * element around the heading and the field list. Plain mode must NOT gain
308
+ * one: its rules are written as `.gridtxt > ul.customer__overview`, and a
309
+ * wrapper would stop the list being a direct child — so it keeps the
310
+ * `React.Fragment` it has always used and its markup is unchanged. A
311
+ * Fragment accepts `key` but nothing else, hence the separate props
312
+ * object rather than a className passed unconditionally.
313
+ */
314
+ const SectionWrap = overviewIsBoxed ? 'section' : React.Fragment;
315
+ const sectionWrapProps = overviewIsBoxed
316
+ ? { className: styles.gridtxt__section }
317
+ : {};
318
+
293
319
  /** Fileupload states */
294
320
  const [files, setFiles] = useState([]);
295
321
 
@@ -2921,11 +2947,18 @@ function GenericDetail({
2921
2947
  </div>
2922
2948
  ) : (
2923
2949
  // Show the regular overview when not editing
2924
- <div className={styles.gridtxt}>
2950
+ <div
2951
+ className={
2952
+ overviewIsBoxed
2953
+ ? `${styles.gridtxt} ${styles['gridtxt--boxed']}`
2954
+ : styles.gridtxt
2955
+ }
2956
+ >
2925
2957
  {activeTabConfig.sections.map(
2926
2958
  (section, sectionIndex) => (
2927
- <React.Fragment
2959
+ <SectionWrap
2928
2960
  key={`section-${activeTabConfig.id}-${sectionIndex}`}
2961
+ {...sectionWrapProps}
2929
2962
  >
2930
2963
  <div
2931
2964
  className={
@@ -3095,16 +3128,33 @@ function GenericDetail({
3095
3128
  liClass
3096
3129
  }
3097
3130
  >
3131
+ {/* `overviewFieldLabel` strips
3132
+ the trailing colon a lot of
3133
+ configs still carry, because
3134
+ stacked above the value it is
3135
+ noise. Boxed fields read as a
3136
+ run-in "Label: value", so the
3137
+ colon is put back here — from
3138
+ one place, whatever the config
3139
+ wrote. */}
3098
3140
  {item.label && (
3099
- <strong
3100
- className={
3101
- styles.overviewLabel
3102
- }
3103
- >
3104
- {overviewFieldLabel(
3105
- item.label
3106
- )}
3107
- </strong>
3141
+ <>
3142
+ <strong
3143
+ className={
3144
+ styles.overviewLabel
3145
+ }
3146
+ >
3147
+ {overviewFieldLabel(
3148
+ item.label
3149
+ )}
3150
+ {overviewIsBoxed
3151
+ ? ':'
3152
+ : ''}
3153
+ </strong>
3154
+ {overviewIsBoxed
3155
+ ? ' '
3156
+ : null}
3157
+ </>
3108
3158
  )}
3109
3159
  <span
3110
3160
  className={
@@ -3121,7 +3171,7 @@ function GenericDetail({
3121
3171
  )}
3122
3172
  </ul>
3123
3173
  )}
3124
- </React.Fragment>
3174
+ </SectionWrap>
3125
3175
  )
3126
3176
  )}
3127
3177
  </div>
@@ -473,6 +473,172 @@
473
473
  color: var(--muted-color, rgba(var(--paragraph-color-rgb, 34, 30, 51), 0.45));
474
474
  }
475
475
 
476
+ /* ==========================================================================
477
+ The boxed overview — opt-in, and only opt-in.
478
+ --------------------------------------------------------------------------
479
+ The 6.15 rework above is the default and stays the default. But one team
480
+ reads their detail pages as a grid of discrete facts, not as a card of
481
+ pairs, and asked for the pre-6.15 treatment back: every field in its own
482
+ tinted, bordered box with a run-in "Label: value", under a centred tinted
483
+ heading bar. Rather than fork the component, an app calls
484
+ `setDefaultOverviewStyle('boxed')` at bootstrap (or a single page sets
485
+ `page.overviewStyle`), GenericDetail adds `.gridtxt--boxed`, and these
486
+ rules put the 5.26 geometry back.
487
+
488
+ Each section is also its own card here, wrapped in `.gridtxt__section`.
489
+ The tinted heading bar on its own was enough to separate sections while the
490
+ fields below it were plain stacked pairs, but once every field is a tinted,
491
+ bordered box the bar reads as one more box in the run: bar, boxes, bar,
492
+ boxes, all inside a single panel, with nothing saying where Transport ends
493
+ and PPE Requirement begins. So the section gets a bordered card and the
494
+ heading becomes that card's header strip — square, full-bleed, hairline
495
+ underneath. Plain mode keeps emitting the header and the `<ul>` as direct
496
+ children of `.gridtxt` (the rules above depend on it), so every selector
497
+ below reaches through `> .gridtxt__section >` and only boxed markup has
498
+ one.
499
+
500
+ One thing from 5.26 is deliberately NOT restored: the red accent on a
501
+ `.notecolor` field. Description and Comments are ordinary fields here too,
502
+ and the brand red read as an error on a page with nothing wrong — the same
503
+ reasoning that removed it from the plain overview. There is no boxed
504
+ `.notecolor` rule below because there does not need to be: the generic
505
+ boxed `li` rule is `.gridtxt.gridtxt--boxed > .gridtxt__section >
506
+ ul.customer__overview li` (0,4,2) and the plain accent it has to beat is
507
+ `.gridtxt > ul .notecolor` (0,2,1), so a note field simply takes the same
508
+ neutral border and tint as every other box.
509
+
510
+ Specificity is doing real work here. The rules being undone are
511
+ `.gridtxt > ul.customer__overview li` above AND the bare `.overviewLabel` /
512
+ `.gridtxt__header` in the 2026 pass at the bottom of the file, which win on
513
+ source order — so every selector below doubles up the class
514
+ (`.gridtxt.gridtxt--boxed …`) or qualifies the target with the modifier,
515
+ and nothing here relies on being later in the file.
516
+
517
+ Token fallbacks are literal for the same reason they are everywhere else in
518
+ this file: a bare var() for a token the host has not declared is invalid at
519
+ computed-value time and the declaration is simply dropped. */
520
+
521
+ /* The panel's own inset is dialled back once the sections are cards: the tab
522
+ card already pads, and `.gridtxt`'s 1rem/1.5rem on top of a card border on
523
+ top of the `<ul>`'s 0.85rem stacked three insets between the panel edge and
524
+ the first field. Small but not zero — the cards still need to sit off the
525
+ panel border. */
526
+ .gridtxt.gridtxt--boxed {
527
+ padding: var(--spacing-sm, 0.5rem);
528
+ }
529
+
530
+ /* One section, one card. `overflow: hidden` is what lets the square heading
531
+ strip below sit flush inside the rounded corners. */
532
+ .gridtxt.gridtxt--boxed > .gridtxt__section {
533
+ display: block;
534
+ box-sizing: border-box;
535
+ background: var(--surface-color, #fff);
536
+ border: 1px solid rgba(var(--primary-rgb, 27, 57, 51), 0.12);
537
+ border-radius: var(--br, 5px);
538
+ overflow: hidden;
539
+ margin: 0 0 var(--spacing-md, 1rem);
540
+ }
541
+
542
+ .gridtxt.gridtxt--boxed > .gridtxt__section:last-child {
543
+ margin-bottom: 0;
544
+ }
545
+
546
+ /* The heading, back to a centred tinted bar — and now the card's header
547
+ strip, so it runs the full width with square corners and a hairline under
548
+ it. `display: flex` is kept so a section's optional `.gridtxt__meta` still
549
+ renders; it is centred with the title rather than pushed to the far edge,
550
+ since there is no edge to push to once the bar is tinted. */
551
+ .gridtxt.gridtxt--boxed > .gridtxt__section > .gridtxt__header {
552
+ width: 100%;
553
+ display: flex;
554
+ align-items: baseline;
555
+ justify-content: center;
556
+ gap: 0;
557
+ flex-wrap: wrap;
558
+ box-sizing: border-box;
559
+ padding: 10px 20px;
560
+ border-bottom: 1px solid rgba(var(--primary-rgb, 27, 57, 51), 0.08);
561
+ border-radius: 0;
562
+ margin-bottom: 0;
563
+ background: rgba(var(--primary-rgb, 27, 57, 51), 0.05);
564
+ font-size: var(--font-size-md, 0.9rem);
565
+ font-weight: 700;
566
+ letter-spacing: 0;
567
+ text-transform: none;
568
+ text-align: center;
569
+ color: var(--paragraph-color, #221e33);
570
+ }
571
+
572
+ /* The meta sits beside the centred title, not opposite it, so it needs its own
573
+ small gap where the header's `space-between` used to provide one. */
574
+ .gridtxt.gridtxt--boxed > .gridtxt__section > .gridtxt__header > .gridtxt__meta {
575
+ margin-left: var(--spacing-sm, 0.5rem);
576
+ }
577
+
578
+ /* A wrapping row of boxes, not a two-column grid: a full-width field can then
579
+ sit between two half-width ones without leaving a hole in a grid track. */
580
+ .gridtxt.gridtxt--boxed > .gridtxt__section > ul.customer__overview {
581
+ width: 100%;
582
+ display: flex;
583
+ justify-content: flex-start;
584
+ flex-direction: row;
585
+ flex-wrap: wrap;
586
+ gap: 0;
587
+ list-style: none;
588
+ box-sizing: border-box;
589
+ padding: 0.85rem;
590
+ margin: 0;
591
+ }
592
+
593
+ /* The box itself. `display: block` is the whole point — the label and the
594
+ value are inline text in one paragraph, not two stacked rows. */
595
+ .gridtxt.gridtxt--boxed > .gridtxt__section > ul.customer__overview li {
596
+ width: auto;
597
+ flex: 0 0 calc(50% - 10px);
598
+ min-width: 0;
599
+ display: block;
600
+ box-sizing: border-box;
601
+ padding: 0.65rem;
602
+ margin: 3px;
603
+ border: 1px solid rgba(var(--primary-rgb, 27, 57, 51), 0.095);
604
+ border-radius: var(--br, 5px);
605
+ background: rgba(var(--primary-rgb, 27, 57, 51), 0.015);
606
+ color: var(--paragraph-color, #221e33);
607
+ font-size: 0.85rem;
608
+ line-height: 1.45;
609
+ }
610
+
611
+ .gridtxt.gridtxt--boxed > .gridtxt__section > ul.customer__overview .fw-grid-item {
612
+ flex: 0 0 calc(100% - 10px);
613
+ grid-column: auto;
614
+ }
615
+
616
+ /* Run-in label: ordinary bold text with a colon after it, not the eyebrow the
617
+ stacked layout wears. */
618
+ .gridtxt--boxed .overviewLabel {
619
+ display: inline;
620
+ font-size: 0.85rem;
621
+ font-weight: 700;
622
+ letter-spacing: 0;
623
+ text-transform: none;
624
+ line-height: inherit;
625
+ color: var(--paragraph-color, #221e33);
626
+ }
627
+
628
+ .gridtxt--boxed .overviewValue {
629
+ display: inline;
630
+ font-size: 0.85rem;
631
+ line-height: inherit;
632
+ }
633
+
634
+ /* One column on a phone — half of a narrow screen is not a readable box. */
635
+ @media (max-width: 759px) {
636
+ .gridtxt.gridtxt--boxed > .gridtxt__section > ul.customer__overview li,
637
+ .gridtxt.gridtxt--boxed > .gridtxt__section > ul.customer__overview .fw-grid-item {
638
+ flex: 0 0 calc(100% - 10px);
639
+ }
640
+ }
641
+
476
642
  .progress {
477
643
  width: 100%;
478
644
  display: block;
@@ -881,6 +881,16 @@
881
881
  position: static;
882
882
  width: max-content;
883
883
  flex: 0 0 auto;
884
+ /* Each `tableInfo` heading and the "N total" count is its own span.
885
+ Until 6.15.0 they were spaced by their own text — "[0 Today] " —
886
+ and when the brackets went, so did the only space between them,
887
+ so "0 Today0 Follow Ups79 total" ran together. The tab-strip and
888
+ embedded rows below already lay the same node out as a flex row;
889
+ the title bar now does too. */
890
+ display: flex;
891
+ align-items: baseline;
892
+ flex-wrap: wrap;
893
+ gap: 0.75rem;
884
894
  }
885
895
 
886
896
  .titleInfo span {
package/src/index.js CHANGED
@@ -22,6 +22,14 @@ import {
22
22
  getDefaultFormLayout,
23
23
  resolveFormVariant,
24
24
  } from './utils/formLayout';
25
+ // Project-wide default for how a detail page's overview fields are drawn — the
26
+ // 6.15 card unless an app calls setDefaultOverviewStyle('boxed') at bootstrap.
27
+ // A page config's own `page.overviewStyle` still wins.
28
+ import {
29
+ setDefaultOverviewStyle,
30
+ getDefaultOverviewStyle,
31
+ resolveOverviewStyle,
32
+ } from './utils/overviewStyle';
25
33
 
26
34
  /** CRM Components */
27
35
  import AsyncSelect from './components/AsyncSelect';
@@ -550,6 +558,7 @@ export {
550
558
  GenericReport,
551
559
  GenericSort,
552
560
  getDefaultFormLayout,
561
+ getDefaultOverviewStyle,
553
562
  GroupedReportRenderer,
554
563
  hasChallengeState,
555
564
  ImpersonateGate,
@@ -598,6 +607,7 @@ export {
598
607
  resolveEditPlacement,
599
608
  isHeaderPlacement,
600
609
  resolveFormVariant,
610
+ resolveOverviewStyle,
601
611
  // Passkeys (WebAuthn). Exported so a consuming app can build its own
602
612
  // enrolment screen against the same marshalling the login screen uses.
603
613
  base64UrlToBytes,
@@ -612,6 +622,7 @@ export {
612
622
  SelectList,
613
623
  Select,
614
624
  setDefaultFormLayout,
625
+ setDefaultOverviewStyle,
615
626
  showConfirmDialog,
616
627
  SortableList,
617
628
  StagePopupModal,
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Project-wide default for how a detail page's overview fields are drawn.
3
+ *
4
+ * GenericDetail's overview tab can draw a section either as the 6.15 card
5
+ * (`'plain'` — one panel, fields as stacked label-above-value pairs on a
6
+ * two-column grid, a left-aligned heading on a hairline) or as the pre-6.15
7
+ * boxes (`'boxed'` — every field in its own tinted, bordered box with the
8
+ * label inline as "Label: value", under a centred tinted heading bar). The
9
+ * library ships with `'plain'` as its default so existing projects keep the
10
+ * presentation they already have. An app that wants the older, boxier reading
11
+ * calls `setDefaultOverviewStyle('boxed')` once at bootstrap (before the first
12
+ * render) and every GenericDetail overview follows.
13
+ *
14
+ * An individual page config still wins in both directions: set
15
+ * `"page": { "overviewStyle": "boxed" }` — or `'plain'` — on the view and that
16
+ * page ignores the project default.
17
+ */
18
+
19
+ /** The two presentations the overview understands. */
20
+ const VALID_STYLES = ['plain', 'boxed'];
21
+
22
+ /** Library default — the 6.15 card, unless a project opts out. */
23
+ let defaultOverviewStyle = 'plain';
24
+
25
+ /** Warn at most once so a bad bootstrap call is not a console flood. */
26
+ let warnedInvalidStyle = false;
27
+
28
+ /**
29
+ * Set the project-wide default presentation for overview fields.
30
+ * @param {'plain'|'boxed'} style - Presentation to use when a page config does
31
+ * not specify its own `page.overviewStyle`.
32
+ * @returns {void} Anything other than 'plain' or 'boxed' is ignored.
33
+ */
34
+ export const setDefaultOverviewStyle = (style) => {
35
+ if (!VALID_STYLES.includes(style)) {
36
+ if (!warnedInvalidStyle) {
37
+ warnedInvalidStyle = true;
38
+ // eslint-disable-next-line no-console
39
+ console.warn(
40
+ `setDefaultOverviewStyle: expected 'plain' or 'boxed', received ${JSON.stringify(style)}. Keeping '${defaultOverviewStyle}'.`
41
+ );
42
+ }
43
+ return;
44
+ }
45
+
46
+ defaultOverviewStyle = style;
47
+ };
48
+
49
+ /**
50
+ * Read the current project-wide default.
51
+ * @returns {'plain'|'boxed'} The default presentation for overview fields.
52
+ */
53
+ export const getDefaultOverviewStyle = () => defaultOverviewStyle;
54
+
55
+ /**
56
+ * Resolve the overview presentation for one detail page.
57
+ * @param {string} [style] - The page config's own `page.overviewStyle`, if it
58
+ * has one.
59
+ * @returns {'plain'|'boxed'} The page's own style when it is valid, otherwise
60
+ * the project-wide default.
61
+ */
62
+ export const resolveOverviewStyle = (style) =>
63
+ (VALID_STYLES.includes(style) ? style : defaultOverviewStyle);