@visns-studio/visns-components 6.32.3 → 6.34.0

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