@visns-studio/visns-components 6.3.16 → 6.3.18

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.16",
94
+ "version": "6.3.18",
95
95
  "description": "Various packages to assist in the development of our Custom Applications.",
96
96
  "main": "src/index.js",
97
97
  "files": [
@@ -239,7 +239,14 @@ function Notification(props) {
239
239
  </button>
240
240
 
241
241
  {show && (
242
- <div className={styles.dropdownWrapper} ref={wrapperRef}>
242
+ <div
243
+ className={styles.dropdownWrapper}
244
+ ref={wrapperRef}
245
+ /* Tells a scrollable host — the navigation's action pill —
246
+ to stop clipping while this is open. See
247
+ Navigation.module.scss `.hactions > ul`. */
248
+ data-nav-overlay="open"
249
+ >
243
250
  <div className={styles.notibox}>
244
251
  <div className={styles.notiboxHeader}>
245
252
  <h3>Notifications</h3>
@@ -87,7 +87,9 @@ function SwitchAccount({ setSystemAuth, setting, userProfile }) {
87
87
  </a>
88
88
 
89
89
  {show && (
90
- <div ref={wrapperRef}>
90
+ /* `data-nav-overlay` stops the navigation's scrollable action
91
+ pill from clipping this list. See Navigation.module.scss. */
92
+ <div ref={wrapperRef} data-nav-overlay="open">
91
93
  {data.length > 0 ? (
92
94
  <div className={styles.notibox}>
93
95
  {data.map((item) => (
@@ -11,7 +11,18 @@ import CustomFetch from '../Fetch';
11
11
  import TableFilter from '../TableFilter';
12
12
  import styles from '../styles/Profile.module.scss';
13
13
 
14
- // Form field component to reduce repetition
14
+ /**
15
+ * One labelled control.
16
+ *
17
+ * The label used to be a `<span>` absolutely positioned inside the box, which
18
+ * is why this component carried two copies of the same `<input>`: the floating
19
+ * treatment needed the span after the input for text fields and before it for
20
+ * passwords, which start in the raised state. A label above the box needs
21
+ * neither trick, so there is one input, in one order, and it is addressable by
22
+ * a real `htmlFor`.
23
+ *
24
+ * Every prop, and the value each of them carries, is unchanged.
25
+ */
15
26
  const FormField = memo(
16
27
  ({
17
28
  type = 'text',
@@ -23,39 +34,23 @@ const FormField = memo(
23
34
  error = '',
24
35
  children,
25
36
  }) => {
26
- const isPrefocused = type === 'password';
37
+ const id = `profile-${name}`;
27
38
 
28
39
  return (
29
40
  <div className={styles.formItem}>
30
- <label className={styles.fi__label}>
31
- {!isPrefocused && (
32
- <input
33
- type={type}
34
- name={name}
35
- onChange={onChange}
36
- className={`${styles.input} ${error}`}
37
- value={value}
38
- readOnly={readOnly}
39
- />
40
- )}
41
- <span
42
- className={`${styles.fi__span} ${
43
- isPrefocused ? styles.prefocus : ''
44
- }`}
45
- >
46
- {label}
47
- </span>
48
- {isPrefocused && (
49
- <input
50
- type={type}
51
- name={name}
52
- onChange={onChange}
53
- value={value}
54
- className={`${styles.input} ${error}`}
55
- />
56
- )}
57
- {children}
41
+ <label className={styles.fieldLabel} htmlFor={id}>
42
+ {label}
58
43
  </label>
44
+ <input
45
+ id={id}
46
+ type={type}
47
+ name={name}
48
+ onChange={onChange}
49
+ className={`${styles.input} ${error}`}
50
+ value={value}
51
+ readOnly={readOnly}
52
+ />
53
+ {children}
59
54
  </div>
60
55
  );
61
56
  }
@@ -451,56 +446,143 @@ const Profile = ({ userProfile, setUserProfile }) => {
451
446
  <div className={styles.gridtxt__header}>
452
447
  <span>Overview</span>
453
448
  </div>
454
- <div className={styles.formcontainer}>
455
- <form
456
- className={styles.modalForm}
457
- onSubmit={handleSubmitProfile}
458
- >
459
- <FormField
460
- name="name"
461
- value={profile.name}
462
- onChange={handleChangeProfile}
463
- label="Name *"
464
- error={inputClasses.name}
465
- />
466
-
467
- <FormField
468
- name="email"
469
- value={profile.email}
470
- readOnly={true}
471
- label="Email *"
472
- error={inputClasses.email}
473
- />
474
-
475
- <FormField
476
- type="password"
477
- name="password"
478
- value={profile.password}
479
- onChange={handleChangeProfile}
480
- label="Password"
481
- error={inputClasses.password}
482
- >
483
- <PasswordStrengthBar
484
- password={profile.password}
485
- />
486
- </FormField>
487
-
488
- <FormField
489
- type="password"
490
- name="password_confirmation"
491
- value={profile.password_confirmation}
492
- onChange={handleChangeProfile}
493
- label="Password Confirmation"
494
- error={inputClasses.password_confirmation}
495
- />
496
-
497
- <div className={styles.formItem}>
498
- <label className={styles.fi__label}>
499
- <span
500
- className={`${styles.fi__span} ${styles.prefocus}`}
449
+ {/* Three decisions, three sections, rather than one
450
+ flat run of fields: what the account is called,
451
+ whether to change the password, and what the
452
+ signature looks like. Each says what it is on the
453
+ left and carries only its own controls on the
454
+ right. */}
455
+ <form
456
+ className={styles.form}
457
+ onSubmit={handleSubmitProfile}
458
+ >
459
+ <section className={styles.section}>
460
+ <div className={styles.sectionAside}>
461
+ <h3 className={styles.sectionTitle}>
462
+ Account
463
+ </h3>
464
+ <p className={styles.sectionDescription}>
465
+ The name shown beside your work. Your
466
+ sign-in address is set by an
467
+ administrator and cannot be edited
468
+ here.
469
+ </p>
470
+ </div>
471
+ <div className={styles.sectionFields}>
472
+ <div className={styles.fieldPair}>
473
+ <FormField
474
+ name="name"
475
+ value={profile.name}
476
+ onChange={handleChangeProfile}
477
+ label="Name *"
478
+ error={inputClasses.name}
479
+ />
480
+
481
+ <FormField
482
+ name="email"
483
+ value={profile.email}
484
+ readOnly={true}
485
+ label="Email *"
486
+ error={inputClasses.email}
487
+ />
488
+ </div>
489
+ </div>
490
+ </section>
491
+
492
+ <section className={styles.section}>
493
+ <div className={styles.sectionAside}>
494
+ <h3 className={styles.sectionTitle}>
495
+ Change password
496
+ </h3>
497
+ <p className={styles.sectionDescription}>
498
+ Leave both fields blank to keep your
499
+ current password — nothing is sent
500
+ unless you type a new one.
501
+ </p>
502
+ </div>
503
+ <div className={styles.sectionFields}>
504
+ <div className={styles.fieldPair}>
505
+ <FormField
506
+ type="password"
507
+ name="password"
508
+ value={profile.password}
509
+ onChange={handleChangeProfile}
510
+ label="Password"
511
+ error={inputClasses.password}
501
512
  >
502
- Signature
503
- </span>
513
+ {/* Only once there is something to
514
+ measure. Rendered
515
+ unconditionally it sat under an
516
+ empty password box reporting
517
+ "too short" before the user had
518
+ typed a character. */}
519
+ {profile.password ? (
520
+ <PasswordStrengthBar
521
+ password={profile.password}
522
+ className={
523
+ styles.strengthMeter
524
+ }
525
+ scoreWordClassName={
526
+ styles.strengthWord
527
+ }
528
+ />
529
+ ) : null}
530
+ </FormField>
531
+
532
+ <FormField
533
+ type="password"
534
+ name="password_confirmation"
535
+ value={
536
+ profile.password_confirmation
537
+ }
538
+ onChange={handleChangeProfile}
539
+ label="Password Confirmation"
540
+ error={
541
+ inputClasses.password_confirmation
542
+ }
543
+ />
544
+ </div>
545
+ </div>
546
+ </section>
547
+
548
+ <section className={styles.section}>
549
+ <div className={styles.sectionAside}>
550
+ <h3 className={styles.sectionTitle}>
551
+ Signature
552
+ </h3>
553
+ <p className={styles.sectionDescription}>
554
+ Applied to the documents generated for
555
+ you. Drawing a new one replaces the
556
+ signature held on your account when you
557
+ save.
558
+ </p>
559
+ </div>
560
+ <div className={styles.sectionFields}>
561
+ <div className={styles.signatureField}>
562
+ {/* Clear belongs to the pad, not to
563
+ the page: it sits on the caption's
564
+ line, quiet and small, instead of
565
+ standing beside Save at the bottom
566
+ of the screen. */}
567
+ <div
568
+ className={styles.signatureHeader}
569
+ >
570
+ <span
571
+ className={
572
+ styles.signatureCaption
573
+ }
574
+ >
575
+ Draw your signature
576
+ </span>
577
+ <button
578
+ className={styles.btnQuiet}
579
+ onClick={handleClearSignature}
580
+ disabled={isLoading}
581
+ type="button"
582
+ >
583
+ Clear
584
+ </button>
585
+ </div>
504
586
  <div
505
587
  className={
506
588
  styles.signaturePadWrapper
@@ -508,6 +590,27 @@ const Profile = ({ userProfile, setUserProfile }) => {
508
590
  >
509
591
  <SignaturePad
510
592
  ref={signatureRef}
593
+ /* Do not lower this, and do
594
+ not narrow
595
+ `--profile-signature-width`
596
+ either. The effect below
597
+ loads the stored signature
598
+ into the pad on mount, so
599
+ the pad is non-empty on an
600
+ ordinary save that never
601
+ touched it, and
602
+ `handleSubmitProfile` then
603
+ re-exports the canvas with
604
+ `toDataURL()` at whatever
605
+ size the canvas currently
606
+ is. A smaller pad therefore
607
+ writes a signature cropped
608
+ to the new geometry back to
609
+ the server — silently, on a
610
+ save the user made for some
611
+ other reason. Growing it is
612
+ safe; shrinking it destroys
613
+ ink. */
511
614
  height={200}
512
615
  options={{
513
616
  penColor: 'black',
@@ -516,30 +619,33 @@ const Profile = ({ userProfile, setUserProfile }) => {
516
619
  }}
517
620
  redrawOnResize
518
621
  canvasProps={{
622
+ /* The frame is on the
623
+ wrapper. A border or
624
+ padding on the canvas
625
+ itself sits inside the
626
+ bounding rect
627
+ signature_pad measures
628
+ against and outside the
629
+ drawing surface, which
630
+ offsets every stroke by
631
+ exactly that much. */
519
632
  style: {
520
- border: '2px solid #ccc',
521
- borderRadius: '10px',
522
- padding: '10px',
523
633
  width: '100%',
524
634
  },
525
635
  className: styles.sigcanvas,
526
636
  }}
527
637
  />
528
- <small
529
- style={{
530
- display: 'block',
531
- marginTop: '5px',
532
- color: '#666',
533
- }}
534
- >
535
- Draw your signature above
536
- </small>
537
638
  </div>
538
- </label>
639
+ </div>
539
640
  </div>
540
- </form>
541
- </div>
542
-
641
+ </section>
642
+ </form>
643
+
644
+ {/* The panel's footer. Save stands alone now that
645
+ Clear lives with the pad it clears. It stays
646
+ outside the `<form>`, where it has always been — a
647
+ submit button inside it would fire
648
+ `handleSubmitProfile` twice. */}
543
649
  <div className={styles.polActions}>
544
650
  <button
545
651
  className={`${styles.btn} ${
@@ -551,14 +657,6 @@ const Profile = ({ userProfile, setUserProfile }) => {
551
657
  >
552
658
  {isLoading ? 'Saving...' : 'Save'}
553
659
  </button>
554
- <button
555
- className={styles.btn}
556
- onClick={handleClearSignature}
557
- disabled={isLoading}
558
- type="button"
559
- >
560
- Clear Signature
561
- </button>
562
660
  </div>
563
661
  </div>
564
662
  );
@@ -568,66 +666,97 @@ const Profile = ({ userProfile, setUserProfile }) => {
568
666
  <div className={styles.gridtxt__header}>
569
667
  <span>Two-Factor Authentication</span>
570
668
  </div>
571
- <div
572
- className={styles.formcontainer}
573
- style={{ maxWidth: '100%' }}
574
- >
575
- <div className={styles.twoFactorSection}>
576
- <div className={styles.twoFactorStatus}>
577
- <h3>
578
- Status:{' '}
579
- {twoFactorEnabled ? (
580
- <span
581
- className={styles.statusEnabled}
582
- >
583
- Enabled
584
- </span>
585
- ) : (
586
- <span
587
- className={
588
- styles.statusDisabled
589
- }
590
- >
591
- Disabled
592
- </span>
593
- )}
669
+ {/* Same sectioning as Overview, so the two tabs read
670
+ as one screen: what 2FA is on the left, the state
671
+ of it and the control that changes that state on
672
+ the right. */}
673
+ <div className={styles.form}>
674
+ <section className={styles.section}>
675
+ <div className={styles.sectionAside}>
676
+ <h3 className={styles.sectionTitle}>
677
+ Authenticator app
594
678
  </h3>
595
- <p>
679
+ <p className={styles.sectionDescription}>
596
680
  Two-factor authentication adds an extra
597
681
  layer of security to your account. When
598
682
  enabled, you'll need to provide both
599
683
  your password and a verification code
600
- from your authenticator app when signing
601
- in.
684
+ from your authenticator app when
685
+ signing in.
602
686
  </p>
687
+ </div>
688
+ <div className={styles.sectionFields}>
689
+ <div className={styles.twoFactorStatus}>
690
+ <h4>
691
+ Status:{' '}
692
+ {twoFactorEnabled ? (
693
+ <span
694
+ className={
695
+ styles.statusEnabled
696
+ }
697
+ >
698
+ Enabled
699
+ </span>
700
+ ) : (
701
+ <span
702
+ className={
703
+ styles.statusDisabled
704
+ }
705
+ >
706
+ Disabled
707
+ </span>
708
+ )}
709
+ </h4>
603
710
 
604
- {!twoFactorEnabled &&
605
- !showTwoFactorSetup && (
606
- <button
607
- className={`${styles.btn} ${styles.btnPrimary}`}
608
- onClick={
609
- fetchTwoFactorSetupData
610
- }
611
- disabled={isLoading}
612
- >
613
- {isLoading
614
- ? 'Loading...'
615
- : 'Set up two-factor authentication'}
616
- </button>
617
- )}
711
+ {!twoFactorEnabled &&
712
+ !showTwoFactorSetup && (
713
+ <button
714
+ className={`${styles.btn} ${styles.btnPrimary}`}
715
+ onClick={
716
+ fetchTwoFactorSetupData
717
+ }
718
+ disabled={isLoading}
719
+ >
720
+ {isLoading
721
+ ? 'Loading...'
722
+ : 'Set up two-factor authentication'}
723
+ </button>
724
+ )}
618
725
 
619
- {twoFactorEnabled &&
620
- userProfile.two_factor_confirmed_at && (
621
- <div>
622
- <p
726
+ {twoFactorEnabled &&
727
+ userProfile.two_factor_confirmed_at && (
728
+ <div
623
729
  className={
624
- styles.confirmedMessage
730
+ styles.statusActions
625
731
  }
626
732
  >
627
- Two-factor authentication is
628
- set up and confirmed. Your
629
- account is now more secure.
630
- </p>
733
+ <p
734
+ className={
735
+ styles.confirmedMessage
736
+ }
737
+ >
738
+ Two-factor
739
+ authentication is set up
740
+ and confirmed. Your
741
+ account is now more
742
+ secure.
743
+ </p>
744
+ <button
745
+ className={`${styles.btn} ${styles.btnDanger}`}
746
+ onClick={
747
+ disableTwoFactor
748
+ }
749
+ disabled={isLoading}
750
+ >
751
+ {isLoading
752
+ ? 'Disabling...'
753
+ : 'Disable two-factor authentication'}
754
+ </button>
755
+ </div>
756
+ )}
757
+
758
+ {twoFactorEnabled &&
759
+ !userProfile.two_factor_confirmed_at && (
631
760
  <button
632
761
  className={`${styles.btn} ${styles.btnDanger}`}
633
762
  onClick={disableTwoFactor}
@@ -637,29 +766,30 @@ const Profile = ({ userProfile, setUserProfile }) => {
637
766
  ? 'Disabling...'
638
767
  : 'Disable two-factor authentication'}
639
768
  </button>
640
- </div>
641
- )}
642
-
643
- {twoFactorEnabled &&
644
- !userProfile.two_factor_confirmed_at && (
645
- <button
646
- className={`${styles.btn} ${styles.btnDanger}`}
647
- onClick={disableTwoFactor}
648
- disabled={isLoading}
649
- >
650
- {isLoading
651
- ? 'Disabling...'
652
- : 'Disable two-factor authentication'}
653
- </button>
654
- )}
769
+ )}
770
+ </div>
655
771
  </div>
772
+ </section>
656
773
 
657
- {showTwoFactorSetup && (
658
- <div className={styles.twoFactorSetup}>
659
- <h3>
774
+ {showTwoFactorSetup && (
775
+ <section className={styles.section}>
776
+ <div className={styles.sectionAside}>
777
+ <h3 className={styles.sectionTitle}>
660
778
  Set up two-factor authentication
661
779
  </h3>
662
- <ol className={styles.setupSteps}>
780
+ <p
781
+ className={
782
+ styles.sectionDescription
783
+ }
784
+ >
785
+ Pair an app with your account, then
786
+ confirm the pairing with a code from
787
+ it. Keep the recovery codes.
788
+ </p>
789
+ </div>
790
+ <div className={styles.sectionFields}>
791
+ <div className={styles.twoFactorSetup}>
792
+ <ol className={styles.setupSteps}>
663
793
  <li>
664
794
  <p>
665
795
  Install an authenticator app
@@ -892,22 +1022,27 @@ const Profile = ({ userProfile, setUserProfile }) => {
892
1022
  device.
893
1023
  </p>
894
1024
  </li>
895
- </ol>
896
- <div className={styles.setupActions}>
897
- <button
898
- className={`${styles.btn} ${styles.btnSecondary}`}
899
- onClick={() =>
900
- setShowTwoFactorSetup(false)
901
- }
902
- type="button"
903
- disabled={isLoading}
1025
+ </ol>
1026
+ <div
1027
+ className={styles.setupActions}
904
1028
  >
905
- Cancel
906
- </button>
1029
+ <button
1030
+ className={`${styles.btn} ${styles.btnSecondary}`}
1031
+ onClick={() =>
1032
+ setShowTwoFactorSetup(
1033
+ false
1034
+ )
1035
+ }
1036
+ type="button"
1037
+ disabled={isLoading}
1038
+ >
1039
+ Cancel
1040
+ </button>
1041
+ </div>
907
1042
  </div>
908
1043
  </div>
909
- )}
910
- </div>
1044
+ </section>
1045
+ )}
911
1046
  </div>
912
1047
  </div>
913
1048
  );