@visns-studio/visns-components 6.3.17 → 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.17",
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": [
@@ -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,69 +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
- {/* Only once there is something to
484
- measure. Rendered unconditionally it
485
- sat under an empty password box
486
- reporting "too short" before the user
487
- had typed a character. */}
488
- {profile.password ? (
489
- <PasswordStrengthBar
490
- password={profile.password}
491
- className={styles.strengthMeter}
492
- scoreWordClassName={
493
- styles.strengthWord
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}
512
+ >
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
494
542
  }
495
543
  />
496
- ) : null}
497
- </FormField>
498
-
499
- <FormField
500
- type="password"
501
- name="password_confirmation"
502
- value={profile.password_confirmation}
503
- onChange={handleChangeProfile}
504
- label="Password Confirmation"
505
- error={inputClasses.password_confirmation}
506
- />
507
-
508
- <div
509
- className={`${styles.formItem} ${styles.formItemFull}`}
510
- >
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}>
511
561
  <div className={styles.signatureField}>
512
- <span
513
- className={styles.signatureLabel}
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}
514
569
  >
515
- Signature
516
- </span>
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>
517
586
  <div
518
587
  className={
519
588
  styles.signaturePadWrapper
@@ -521,6 +590,27 @@ const Profile = ({ userProfile, setUserProfile }) => {
521
590
  >
522
591
  <SignaturePad
523
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. */
524
614
  height={200}
525
615
  options={{
526
616
  penColor: 'black',
@@ -529,11 +619,16 @@ const Profile = ({ userProfile, setUserProfile }) => {
529
619
  }}
530
620
  redrawOnResize
531
621
  canvasProps={{
532
- /* The frame lives on
533
- `.sigcanvas` now. Its
534
- geometry is unchanged —
535
- border and padding move
536
- where the ink lands. */
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. */
537
632
  style: {
538
633
  width: '100%',
539
634
  },
@@ -541,26 +636,17 @@ const Profile = ({ userProfile, setUserProfile }) => {
541
636
  }}
542
637
  />
543
638
  </div>
544
- <small
545
- className={styles.signatureHint}
546
- >
547
- Draw your signature above
548
- </small>
549
639
  </div>
550
640
  </div>
551
- </form>
552
- </div>
553
-
554
- {/* The form card's footer. Secondary before primary. */}
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. */}
555
649
  <div className={styles.polActions}>
556
- <button
557
- className={`${styles.btn} ${styles.btnSecondary}`}
558
- onClick={handleClearSignature}
559
- disabled={isLoading}
560
- type="button"
561
- >
562
- Clear Signature
563
- </button>
564
650
  <button
565
651
  className={`${styles.btn} ${
566
652
  isLoading ? styles.loading : ''
@@ -580,66 +666,97 @@ const Profile = ({ userProfile, setUserProfile }) => {
580
666
  <div className={styles.gridtxt__header}>
581
667
  <span>Two-Factor Authentication</span>
582
668
  </div>
583
- <div
584
- className={styles.formcontainer}
585
- style={{ maxWidth: '100%' }}
586
- >
587
- <div className={styles.twoFactorSection}>
588
- <div className={styles.twoFactorStatus}>
589
- <h3>
590
- Status:{' '}
591
- {twoFactorEnabled ? (
592
- <span
593
- className={styles.statusEnabled}
594
- >
595
- Enabled
596
- </span>
597
- ) : (
598
- <span
599
- className={
600
- styles.statusDisabled
601
- }
602
- >
603
- Disabled
604
- </span>
605
- )}
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
606
678
  </h3>
607
- <p>
679
+ <p className={styles.sectionDescription}>
608
680
  Two-factor authentication adds an extra
609
681
  layer of security to your account. When
610
682
  enabled, you'll need to provide both
611
683
  your password and a verification code
612
- from your authenticator app when signing
613
- in.
684
+ from your authenticator app when
685
+ signing in.
614
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>
615
710
 
616
- {!twoFactorEnabled &&
617
- !showTwoFactorSetup && (
618
- <button
619
- className={`${styles.btn} ${styles.btnPrimary}`}
620
- onClick={
621
- fetchTwoFactorSetupData
622
- }
623
- disabled={isLoading}
624
- >
625
- {isLoading
626
- ? 'Loading...'
627
- : 'Set up two-factor authentication'}
628
- </button>
629
- )}
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
+ )}
630
725
 
631
- {twoFactorEnabled &&
632
- userProfile.two_factor_confirmed_at && (
633
- <div>
634
- <p
726
+ {twoFactorEnabled &&
727
+ userProfile.two_factor_confirmed_at && (
728
+ <div
635
729
  className={
636
- styles.confirmedMessage
730
+ styles.statusActions
637
731
  }
638
732
  >
639
- Two-factor authentication is
640
- set up and confirmed. Your
641
- account is now more secure.
642
- </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 && (
643
760
  <button
644
761
  className={`${styles.btn} ${styles.btnDanger}`}
645
762
  onClick={disableTwoFactor}
@@ -649,29 +766,30 @@ const Profile = ({ userProfile, setUserProfile }) => {
649
766
  ? 'Disabling...'
650
767
  : 'Disable two-factor authentication'}
651
768
  </button>
652
- </div>
653
- )}
654
-
655
- {twoFactorEnabled &&
656
- !userProfile.two_factor_confirmed_at && (
657
- <button
658
- className={`${styles.btn} ${styles.btnDanger}`}
659
- onClick={disableTwoFactor}
660
- disabled={isLoading}
661
- >
662
- {isLoading
663
- ? 'Disabling...'
664
- : 'Disable two-factor authentication'}
665
- </button>
666
- )}
769
+ )}
770
+ </div>
667
771
  </div>
772
+ </section>
668
773
 
669
- {showTwoFactorSetup && (
670
- <div className={styles.twoFactorSetup}>
671
- <h3>
774
+ {showTwoFactorSetup && (
775
+ <section className={styles.section}>
776
+ <div className={styles.sectionAside}>
777
+ <h3 className={styles.sectionTitle}>
672
778
  Set up two-factor authentication
673
779
  </h3>
674
- <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}>
675
793
  <li>
676
794
  <p>
677
795
  Install an authenticator app
@@ -904,22 +1022,27 @@ const Profile = ({ userProfile, setUserProfile }) => {
904
1022
  device.
905
1023
  </p>
906
1024
  </li>
907
- </ol>
908
- <div className={styles.setupActions}>
909
- <button
910
- className={`${styles.btn} ${styles.btnSecondary}`}
911
- onClick={() =>
912
- setShowTwoFactorSetup(false)
913
- }
914
- type="button"
915
- disabled={isLoading}
1025
+ </ol>
1026
+ <div
1027
+ className={styles.setupActions}
916
1028
  >
917
- Cancel
918
- </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>
919
1042
  </div>
920
1043
  </div>
921
- )}
922
- </div>
1044
+ </section>
1045
+ )}
923
1046
  </div>
924
1047
  </div>
925
1048
  );