@visns-studio/visns-components 6.32.3 → 6.34.3

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.
Files changed (53) hide show
  1. package/README.md +483 -0
  2. package/package.json +1 -1
  3. package/src/components/DataGrid.jsx +44 -2
  4. package/src/components/Field.jsx +32 -0
  5. package/src/components/Form.jsx +39 -2
  6. package/src/components/Navigation.jsx +274 -15
  7. package/src/components/Notification.jsx +13 -2
  8. package/src/components/TableFilter.jsx +14 -2
  9. package/src/components/auth/Login.jsx +11 -4
  10. package/src/components/auth/PasskeyEnrolPrompt.jsx +269 -0
  11. package/src/components/auth/Profile.jsx +422 -26
  12. package/src/components/auth/Reset.jsx +1 -1
  13. package/src/components/auth/TwoFactorAuth.jsx +1 -1
  14. package/src/components/auth/Verify.jsx +2 -2
  15. package/src/components/auth/authEndpoints.js +43 -0
  16. package/src/components/auth/passkeyClient.js +102 -0
  17. package/src/components/auth/passkeyPrompt.js +408 -0
  18. package/src/components/auth/profileLayout.js +45 -0
  19. package/src/components/controls/DataGridSearch.jsx +16 -2
  20. package/src/components/controls/DataGridSortSheet.jsx +2 -0
  21. package/src/components/emailCampaigns/BlockEditor.jsx +486 -0
  22. package/src/components/emailCampaigns/CampaignEditor.jsx +614 -0
  23. package/src/components/emailCampaigns/CampaignReport.jsx +98 -0
  24. package/src/components/emailCampaigns/EmailCampaigns.jsx +422 -0
  25. package/src/components/emailCampaigns/EmailLists.jsx +505 -0
  26. package/src/components/emailCampaigns/emailCampaignApi.js +135 -0
  27. package/src/components/generic/ActionButtons.jsx +1 -1
  28. package/src/components/generic/GenericAuth.jsx +197 -21
  29. package/src/components/generic/GenericDetail.jsx +95 -3
  30. package/src/components/generic/GenericMain.jsx +7 -0
  31. package/src/components/generic/StandardModal.jsx +71 -158
  32. package/src/components/sms/SmsCampaigns.jsx +2517 -0
  33. package/src/components/sms/SmsInbox.jsx +20 -5
  34. package/src/components/sms/SmsLineSettings.jsx +2 -5
  35. package/src/components/sms/SmsThreadPanel.jsx +20 -0
  36. package/src/components/sms/smsEndpoints.js +23 -0
  37. package/src/components/styles/DataGrid.module.scss +22 -0
  38. package/src/components/styles/EmailCampaigns.module.scss +922 -0
  39. package/src/components/styles/Form.module.scss +100 -2
  40. package/src/components/styles/Navigation.module.scss +322 -5
  41. package/src/components/styles/Notification.module.scss +140 -8
  42. package/src/components/styles/PasskeyEnrolPrompt.module.scss +105 -0
  43. package/src/components/styles/Profile.module.scss +157 -0
  44. package/src/components/styles/Sms.module.scss +484 -4
  45. package/src/components/styles/StandardModal.module.scss +273 -0
  46. package/src/components/styles/Vault.module.scss +5 -3
  47. package/src/components/styles/global.css +38 -7
  48. package/src/components/utils/navCollapsed.js +201 -0
  49. package/src/components/utils/rowActionSettings.js +76 -0
  50. package/src/components/utils/usePasskeysEnabled.js +84 -0
  51. package/src/components/utils/useShellLayout.js +131 -0
  52. package/src/index.js +107 -1
  53. package/src/utils/rememberTab.js +70 -0
@@ -1068,19 +1068,48 @@ $sms-mono: 'SFMono-Regular', 'Menlo', 'Consolas', monospace;
1068
1068
 
1069
1069
  .pageTitle {
1070
1070
  display: flex;
1071
- gap: 0.5rem;
1071
+ flex-wrap: wrap;
1072
+ column-gap: 0.3em;
1072
1073
  align-items: center;
1073
1074
  margin: 0;
1074
- color: var(--s-paragraph);
1075
- font-size: 1.5rem;
1075
+ /* The same title every other screen of a host CRM draws: `.crmtitle h1` in
1076
+ GenericDetail/GenericIndex and PageHeader's `.title` all read
1077
+ --page-header-title-size and the heading colour. This was 1.5rem in the
1078
+ paragraph colour, a size larger than every page around it. */
1079
+ color: var(--heading-color, var(--s-paragraph));
1080
+ font-size: var(--page-header-title-size, 1.25rem);
1076
1081
  font-weight: 700;
1077
- line-height: 1.2;
1082
+ line-height: 1.25;
1083
+ letter-spacing: -0.02em;
1078
1084
 
1079
1085
  svg {
1080
1086
  color: var(--s-primary);
1081
1087
  }
1082
1088
  }
1083
1089
 
1090
+ /* A breadcrumb inside the title — `Messages › SMS campaigns`. The library's
1091
+ trail spec (`.crmtitle h1 a` / `h1 svg` in GenericDetail.module.scss):
1092
+ the parent muted at 500, a 16px chevron, the current page at 700. */
1093
+ .pageTitleParent {
1094
+ color: var(--muted-color, var(--s-muted));
1095
+ font-weight: 500;
1096
+ text-decoration: none;
1097
+ transition: color 0.15s ease;
1098
+
1099
+ &:hover,
1100
+ &:focus-visible {
1101
+ color: var(--paragraph-color, var(--s-paragraph));
1102
+ text-decoration: underline;
1103
+ }
1104
+ }
1105
+
1106
+ .pageTitle .pageTitleChevron {
1107
+ flex: 0 0 auto;
1108
+ max-width: 15px;
1109
+ margin: 0 0.4rem;
1110
+ color: var(--muted-color, var(--s-muted));
1111
+ }
1112
+
1084
1113
  .headActions {
1085
1114
  display: flex;
1086
1115
  flex-wrap: wrap;
@@ -1825,6 +1854,26 @@ $sms-mono: 'SFMono-Regular', 'Menlo', 'Consolas', monospace;
1825
1854
  // One card: the text on top, every control in a bar along its bottom edge.
1826
1855
  // Buttons beside a textarea line up with nothing once the box grows; a bar
1827
1856
  // inside the same border always reads as part of the message being written.
1857
+ // The opted-out line, above the composer and nothing else.
1858
+ //
1859
+ // Muted on purpose. It is a fact worth knowing before typing, not a warning
1860
+ // about what is being typed — and everything below it stays live, because an
1861
+ // opt-out is about bulk sends and this is a person answering a person.
1862
+ .optedOutNote {
1863
+ display: flex;
1864
+ gap: 0.4rem;
1865
+ align-items: center;
1866
+ margin: 0 0 0.45rem;
1867
+ color: var(--s-muted);
1868
+ font-size: 0.76rem;
1869
+ line-height: 1.4;
1870
+
1871
+ svg {
1872
+ flex-shrink: 0;
1873
+ color: var(--s-muted);
1874
+ }
1875
+ }
1876
+
1828
1877
  .composerBox {
1829
1878
  display: flex;
1830
1879
  flex-direction: column;
@@ -2776,3 +2825,434 @@ $sms-mono: 'SFMono-Regular', 'Menlo', 'Consolas', monospace;
2776
2825
  border-radius: var(--s-br);
2777
2826
  }
2778
2827
  }
2828
+
2829
+ // ---------------------------------------------------------------------------
2830
+ // SmsCampaigns — the list, the three-step flow, one campaign, the opt-outs
2831
+ // ---------------------------------------------------------------------------
2832
+ //
2833
+ // The page mounts inside `.settings`, so it inherits the tokens and the
2834
+ // padding the messaging settings screen already has: a campaign list is a
2835
+ // table of records with an action bar over it, which is exactly what that
2836
+ // screen is. Everything below is the parts a settings screen has no vocabulary
2837
+ // for — a progress bar, a step rail, a drop target and a preview column.
2838
+
2839
+ // "Sending" is not a warning.
2840
+ //
2841
+ // A campaign doing precisely what it was told to do drawn in amber teaches
2842
+ // people to stop reading the chips, and then the paused one — the state where
2843
+ // nothing is going out and somebody has to act — reads the same as the healthy
2844
+ // one. So it takes the brand tint: visibly live, and visibly not a problem.
2845
+ .campaignPillLive {
2846
+ background: var(--s-tint);
2847
+ color: var(--s-primary);
2848
+ }
2849
+
2850
+ // The flag on a number the browser cannot read as one. A pill inside a cell,
2851
+ // so it needs the gap the cell does not give it.
2852
+ .campaignFlag {
2853
+ margin-left: 0.4rem;
2854
+ }
2855
+
2856
+ /* ------------------------------------------------------------- progress */
2857
+
2858
+ .campaignProgress {
2859
+ display: flex;
2860
+ gap: 0.5rem;
2861
+ align-items: center;
2862
+ min-width: 0;
2863
+ }
2864
+
2865
+ // The track is fixed and the fill is a percentage of it. Both are needed: a
2866
+ // bar that shrinks to its own fill has no "how much is left" in it, which is
2867
+ // the only question a progress bar answers.
2868
+ .campaignBar {
2869
+ flex: 1 1 auto;
2870
+ box-sizing: border-box;
2871
+ min-width: 90px;
2872
+ height: 6px;
2873
+ border-radius: 999px;
2874
+ background: color-mix(in srgb, var(--s-paragraph) 12%, var(--s-background));
2875
+ overflow: hidden;
2876
+ }
2877
+
2878
+ .campaignBarFill {
2879
+ display: block;
2880
+ height: 100%;
2881
+ border-radius: 999px;
2882
+ background: var(--s-primary);
2883
+ // Short, and only ever forward: the number behind it moves a few percent
2884
+ // every poll, and an unanimated jump reads as the page reloading.
2885
+ transition: width 0.3s ease;
2886
+ }
2887
+
2888
+ .campaignProgressCount {
2889
+ flex: 0 0 auto;
2890
+ color: var(--s-muted);
2891
+ font-size: 0.78rem;
2892
+ font-variant-numeric: tabular-nums;
2893
+ white-space: nowrap;
2894
+ }
2895
+
2896
+ // The bar needs room in a table cell that would otherwise size to its text.
2897
+ .campaignProgressCell {
2898
+ min-width: 170px;
2899
+ }
2900
+
2901
+ /* --------------------------------------------------------------- wizard */
2902
+
2903
+ .campaignWizard,
2904
+ .campaignDetail {
2905
+ display: flex;
2906
+ flex-direction: column;
2907
+ gap: 1rem;
2908
+ }
2909
+
2910
+ // The step rail. Numbered, because "Message" on its own does not say whether
2911
+ // it comes before or after the list — and the numbers are what the Back button
2912
+ // is moving between.
2913
+ .campaignSteps {
2914
+ display: flex;
2915
+ flex-wrap: wrap;
2916
+ gap: 0.4rem 1.25rem;
2917
+ margin: 0;
2918
+ padding: 0;
2919
+ list-style: none;
2920
+ }
2921
+
2922
+ .campaignStep {
2923
+ display: inline-flex;
2924
+ gap: 0.45rem;
2925
+ align-items: center;
2926
+ color: var(--s-muted);
2927
+ font-size: 0.82rem;
2928
+ font-weight: 600;
2929
+ }
2930
+
2931
+ .campaignStepOn {
2932
+ color: var(--s-paragraph);
2933
+
2934
+ .campaignStepNumber {
2935
+ border-color: var(--s-primary);
2936
+ background: var(--s-primary);
2937
+ color: #fff;
2938
+ }
2939
+ }
2940
+
2941
+ .campaignStepNumber {
2942
+ display: inline-flex;
2943
+ flex: 0 0 auto;
2944
+ align-items: center;
2945
+ justify-content: center;
2946
+ box-sizing: border-box;
2947
+ width: 22px;
2948
+ height: 22px;
2949
+ border: 1px solid var(--s-line);
2950
+ border-radius: 50%;
2951
+ background: var(--s-background);
2952
+ color: var(--s-muted);
2953
+ font-size: 0.74rem;
2954
+ font-weight: 700;
2955
+ font-variant-numeric: tabular-nums;
2956
+ }
2957
+
2958
+ // Two ways in, side by side and of equal weight — neither is the "proper" one.
2959
+ // They stack below the tablet breakpoint, where two columns of this leave the
2960
+ // paste box too narrow to see a line of `Name, number` in.
2961
+ .campaignSources {
2962
+ display: grid;
2963
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
2964
+ gap: 1rem;
2965
+ }
2966
+
2967
+ .campaignSource {
2968
+ display: flex;
2969
+ flex-direction: column;
2970
+ gap: 0.6rem;
2971
+ box-sizing: border-box;
2972
+ padding: 0.85rem 1rem;
2973
+ border: 1px solid var(--s-line);
2974
+ border-radius: var(--s-br);
2975
+ background: var(--s-background);
2976
+ }
2977
+
2978
+ .campaignSourceTitle {
2979
+ display: flex;
2980
+ gap: 0.4rem;
2981
+ align-items: center;
2982
+ margin: 0;
2983
+ color: var(--s-paragraph);
2984
+ font-size: 0.92rem;
2985
+ font-weight: 700;
2986
+
2987
+ svg {
2988
+ color: var(--s-primary);
2989
+ }
2990
+ }
2991
+
2992
+ .campaignDrop {
2993
+ @include sms-focus;
2994
+
2995
+ display: flex;
2996
+ flex-direction: column;
2997
+ gap: 0.5rem;
2998
+ align-items: center;
2999
+ justify-content: center;
3000
+ box-sizing: border-box;
3001
+ min-height: 120px;
3002
+ padding: 1rem;
3003
+ border: 1px dashed var(--s-line);
3004
+ border-radius: var(--s-br);
3005
+ background: var(--s-surface);
3006
+ color: var(--s-muted);
3007
+ text-align: center;
3008
+ cursor: pointer;
3009
+
3010
+ p {
3011
+ max-width: 34ch;
3012
+ text-align: center;
3013
+ }
3014
+
3015
+ &:hover {
3016
+ border-color: var(--s-primary);
3017
+ }
3018
+ }
3019
+
3020
+ .campaignDropOn {
3021
+ border-color: var(--s-primary);
3022
+ background: var(--s-tint);
3023
+ color: var(--s-primary);
3024
+ }
3025
+
3026
+ // Two selects and the button that takes the rows, on one line where there is
3027
+ // room. The button's own width is its label, which grows with the count.
3028
+ .campaignColumns {
3029
+ display: flex;
3030
+ flex-wrap: wrap;
3031
+ gap: 0.6rem;
3032
+ align-items: flex-end;
3033
+
3034
+ > .field {
3035
+ flex: 1 1 140px;
3036
+ min-width: 0;
3037
+ }
3038
+
3039
+ > .help {
3040
+ flex: 1 1 100%;
3041
+ }
3042
+ }
3043
+
3044
+ .campaignCount {
3045
+ color: var(--s-paragraph);
3046
+ font-size: 0.88rem;
3047
+
3048
+ strong {
3049
+ font-weight: 700;
3050
+ }
3051
+ }
3052
+
3053
+ /* -------------------------------------------------------------- compose */
3054
+
3055
+ // The message on the left, what it becomes on the right. Side by side rather
3056
+ // than stacked because the point of the preview is watching it change as the
3057
+ // message is typed, and a panel below the fold is a panel nobody sees move.
3058
+ .campaignCompose {
3059
+ display: grid;
3060
+ grid-template-columns: minmax(0, 1fr) minmax(0, 22rem);
3061
+ gap: 1rem;
3062
+ align-items: start;
3063
+ }
3064
+
3065
+ .campaignComposeMain {
3066
+ display: flex;
3067
+ flex-direction: column;
3068
+ gap: 0.85rem;
3069
+ min-width: 0;
3070
+ }
3071
+
3072
+ .campaignPreview {
3073
+ display: flex;
3074
+ flex-direction: column;
3075
+ gap: 0.6rem;
3076
+ box-sizing: border-box;
3077
+ padding: 0.85rem 1rem;
3078
+ border: 1px solid var(--s-line);
3079
+ border-radius: var(--s-br);
3080
+ background: var(--s-surface);
3081
+ }
3082
+
3083
+ .campaignPreviewList {
3084
+ display: flex;
3085
+ flex-direction: column;
3086
+ gap: 0.6rem;
3087
+ margin: 0;
3088
+ padding: 0;
3089
+ list-style: none;
3090
+ }
3091
+
3092
+ .campaignPreviewItem {
3093
+ display: flex;
3094
+ flex-direction: column;
3095
+ gap: 0.35rem;
3096
+ box-sizing: border-box;
3097
+ padding: 0.6rem 0.7rem;
3098
+ border: 1px solid var(--s-line);
3099
+ border-radius: var(--s-br);
3100
+ background: var(--s-background);
3101
+ }
3102
+
3103
+ .campaignPreviewWho {
3104
+ display: flex;
3105
+ gap: 0.5rem;
3106
+ align-items: center;
3107
+ min-width: 0;
3108
+
3109
+ strong {
3110
+ display: block;
3111
+ overflow: hidden;
3112
+ color: var(--s-paragraph);
3113
+ font-size: 0.84rem;
3114
+ font-weight: 700;
3115
+ text-overflow: ellipsis;
3116
+ white-space: nowrap;
3117
+ }
3118
+
3119
+ small {
3120
+ color: var(--s-muted);
3121
+ font-size: 0.74rem;
3122
+ font-variant-numeric: tabular-nums;
3123
+ }
3124
+ }
3125
+
3126
+ // The rendered message. `pre-wrap` because a campaign body carries the line
3127
+ // breaks somebody typed, and a preview that folded them away would not be one.
3128
+ .campaignPreviewBody {
3129
+ margin: 0;
3130
+ color: var(--s-paragraph);
3131
+ font-size: 0.84rem;
3132
+ line-height: 1.5;
3133
+ white-space: pre-wrap;
3134
+ overflow-wrap: anywhere;
3135
+ }
3136
+
3137
+ .campaignFooter {
3138
+ color: var(--s-paragraph);
3139
+ font-style: italic;
3140
+ }
3141
+
3142
+ /* --------------------------------------------------------------- review */
3143
+
3144
+ .campaignReview,
3145
+ .campaignReport {
3146
+ display: flex;
3147
+ flex-direction: column;
3148
+ gap: 1rem;
3149
+ }
3150
+
3151
+ // The same card the settings status block is, and deliberately so: a row of
3152
+ // labelled figures is a row of labelled figures, and a second design for one
3153
+ // would be a second thing to keep in step.
3154
+ .campaignFacts {
3155
+ display: flex;
3156
+ flex-wrap: wrap;
3157
+ gap: 0.75rem 1.5rem;
3158
+ box-sizing: border-box;
3159
+ padding: 0.85rem 1rem;
3160
+ border: 1px solid var(--s-line);
3161
+ border-radius: var(--s-br);
3162
+ background: var(--s-surface);
3163
+ }
3164
+
3165
+ // The step's own buttons, always at the foot of it, always right-aligned —
3166
+ // Back on the left of the pair because that is the order they are read in.
3167
+ .campaignFoot {
3168
+ display: flex;
3169
+ gap: 0.6rem;
3170
+ align-items: center;
3171
+ justify-content: flex-end;
3172
+ padding-top: 0.75rem;
3173
+ border-top: 1px solid var(--s-line);
3174
+ }
3175
+
3176
+ /* --------------------------------------------------------------- detail */
3177
+
3178
+ .campaignHeading {
3179
+ display: flex;
3180
+ flex-direction: column;
3181
+ gap: 0.2rem;
3182
+ min-width: 0;
3183
+ }
3184
+
3185
+ // The message, folded. `details` rather than a button and a piece of state:
3186
+ // the browser owns the open/closed, it is keyboard operable for nothing, and
3187
+ // the summary is a real heading when it is printed.
3188
+ .campaignFold {
3189
+ box-sizing: border-box;
3190
+ padding: 0.6rem 0.85rem;
3191
+ border: 1px solid var(--s-line);
3192
+ border-radius: var(--s-br);
3193
+ background: var(--s-surface);
3194
+ font-size: 0.84rem;
3195
+
3196
+ > summary {
3197
+ color: var(--s-paragraph);
3198
+ font-weight: 600;
3199
+ cursor: pointer;
3200
+ }
3201
+ }
3202
+
3203
+ .campaignFoldBody {
3204
+ margin: 0.6rem 0 0;
3205
+ color: var(--s-paragraph);
3206
+ line-height: 1.5;
3207
+ white-space: pre-wrap;
3208
+ overflow-wrap: anywhere;
3209
+ }
3210
+
3211
+ .campaignPager {
3212
+ display: flex;
3213
+ gap: 0.75rem;
3214
+ align-items: center;
3215
+ justify-content: flex-end;
3216
+ }
3217
+
3218
+ /* ------------------------------------------------------------- opt-outs */
3219
+
3220
+ // Number, note, and the button, on one row. The note is given the slack
3221
+ // because "asked to stop, 4 September" is the part somebody actually types.
3222
+ .optOutAdd {
3223
+ display: flex;
3224
+ flex-wrap: wrap;
3225
+ gap: 0.6rem;
3226
+ align-items: flex-end;
3227
+ box-sizing: border-box;
3228
+ padding: 0.75rem 0.85rem;
3229
+ border: 1px solid var(--s-line);
3230
+ border-radius: var(--s-br);
3231
+ background: var(--s-surface);
3232
+
3233
+ > .field:first-child {
3234
+ flex: 0 1 12rem;
3235
+ }
3236
+
3237
+ > .field {
3238
+ flex: 1 1 14rem;
3239
+ min-width: 0;
3240
+ }
3241
+ }
3242
+
3243
+ @media (max-width: 900px) {
3244
+ // The preview stops being a column and becomes a panel under the message.
3245
+ // Below this width the two side by side leave the textarea about forty
3246
+ // characters wide, which is narrower than the message being written in it.
3247
+ .campaignCompose {
3248
+ grid-template-columns: minmax(0, 1fr);
3249
+ }
3250
+
3251
+ .campaignFoot {
3252
+ flex-wrap: wrap;
3253
+ }
3254
+
3255
+ .campaignPager {
3256
+ justify-content: space-between;
3257
+ }
3258
+ }