@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.
- package/README.md +483 -0
- package/package.json +1 -1
- package/src/components/DataGrid.jsx +44 -2
- package/src/components/Field.jsx +32 -0
- package/src/components/Form.jsx +39 -2
- package/src/components/Navigation.jsx +274 -15
- package/src/components/Notification.jsx +13 -2
- package/src/components/TableFilter.jsx +14 -2
- package/src/components/auth/Login.jsx +11 -4
- package/src/components/auth/PasskeyEnrolPrompt.jsx +269 -0
- package/src/components/auth/Profile.jsx +422 -26
- package/src/components/auth/Reset.jsx +1 -1
- package/src/components/auth/TwoFactorAuth.jsx +1 -1
- package/src/components/auth/Verify.jsx +2 -2
- package/src/components/auth/authEndpoints.js +43 -0
- package/src/components/auth/passkeyClient.js +102 -0
- package/src/components/auth/passkeyPrompt.js +408 -0
- package/src/components/auth/profileLayout.js +45 -0
- package/src/components/controls/DataGridSearch.jsx +16 -2
- package/src/components/controls/DataGridSortSheet.jsx +2 -0
- package/src/components/emailCampaigns/BlockEditor.jsx +486 -0
- package/src/components/emailCampaigns/CampaignEditor.jsx +614 -0
- package/src/components/emailCampaigns/CampaignReport.jsx +98 -0
- package/src/components/emailCampaigns/EmailCampaigns.jsx +422 -0
- package/src/components/emailCampaigns/EmailLists.jsx +505 -0
- package/src/components/emailCampaigns/emailCampaignApi.js +135 -0
- package/src/components/generic/ActionButtons.jsx +1 -1
- package/src/components/generic/GenericAuth.jsx +197 -21
- package/src/components/generic/GenericDetail.jsx +95 -3
- package/src/components/generic/GenericMain.jsx +7 -0
- package/src/components/generic/StandardModal.jsx +71 -158
- package/src/components/sms/SmsCampaigns.jsx +2517 -0
- package/src/components/sms/SmsInbox.jsx +20 -5
- package/src/components/sms/SmsLineSettings.jsx +2 -5
- package/src/components/sms/SmsThreadPanel.jsx +20 -0
- package/src/components/sms/smsEndpoints.js +23 -0
- package/src/components/styles/DataGrid.module.scss +22 -0
- package/src/components/styles/EmailCampaigns.module.scss +922 -0
- package/src/components/styles/Form.module.scss +100 -2
- package/src/components/styles/Navigation.module.scss +322 -5
- package/src/components/styles/Notification.module.scss +140 -8
- package/src/components/styles/PasskeyEnrolPrompt.module.scss +105 -0
- package/src/components/styles/Profile.module.scss +157 -0
- package/src/components/styles/Sms.module.scss +484 -4
- package/src/components/styles/StandardModal.module.scss +273 -0
- package/src/components/styles/Vault.module.scss +5 -3
- package/src/components/styles/global.css +38 -7
- package/src/components/utils/navCollapsed.js +201 -0
- package/src/components/utils/rowActionSettings.js +76 -0
- package/src/components/utils/usePasskeysEnabled.js +84 -0
- package/src/components/utils/useShellLayout.js +131 -0
- package/src/index.js +107 -1
- package/src/utils/rememberTab.js +70 -0
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useEffect, useCallback, useId, useRef } from 'react';
|
|
2
2
|
import { X } from 'lucide-react';
|
|
3
3
|
import formStyles from '../styles/Form.module.scss';
|
|
4
|
+
// The overlay's and the panel's own geometry. It was an inline style object
|
|
5
|
+
// built from a user-agent sniff; that file says why that had to stop, and it
|
|
6
|
+
// is also where the z-index order is written down.
|
|
7
|
+
import styles from '../styles/StandardModal.module.scss';
|
|
4
8
|
|
|
5
9
|
/**
|
|
6
10
|
* The sheet's entry animation.
|
|
@@ -24,6 +28,9 @@ const SHEET_KEYFRAMES = `
|
|
|
24
28
|
}
|
|
25
29
|
`;
|
|
26
30
|
|
|
31
|
+
/** The three panel widths the stylesheet defines. Anything else is 'medium'. */
|
|
32
|
+
const PANEL_SIZES = ['small', 'medium', 'large'];
|
|
33
|
+
|
|
27
34
|
/**
|
|
28
35
|
* StandardModal Component
|
|
29
36
|
*
|
|
@@ -71,29 +78,6 @@ const StandardModal = ({
|
|
|
71
78
|
showHeader = false,
|
|
72
79
|
customStyles = {}
|
|
73
80
|
}) => {
|
|
74
|
-
// Device detection state
|
|
75
|
-
const [windowWidth, setWindowWidth] = useState(typeof window !== 'undefined' ? window.innerWidth : 1024);
|
|
76
|
-
|
|
77
|
-
// Device detection utility
|
|
78
|
-
const getDeviceType = () => {
|
|
79
|
-
const hasTouch = typeof window !== 'undefined' && ('ontouchstart' in window || navigator.maxTouchPoints > 0);
|
|
80
|
-
const userAgent = typeof window !== 'undefined' ? navigator.userAgent : '';
|
|
81
|
-
|
|
82
|
-
// Mobile detection: width + user agent + touch
|
|
83
|
-
const isMobileUA = /Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent);
|
|
84
|
-
const isMobileWidth = windowWidth <= 768;
|
|
85
|
-
const isMobile = isMobileUA || (isMobileWidth && hasTouch);
|
|
86
|
-
|
|
87
|
-
// Tablet detection: iPad or Android tablets or medium width with touch
|
|
88
|
-
const isTabletUA = /iPad|Android(?!.*Mobile)/i.test(userAgent);
|
|
89
|
-
const isTabletWidth = windowWidth > 768 && windowWidth <= 1024;
|
|
90
|
-
const isTablet = isTabletUA || (isTabletWidth && hasTouch && !isMobile);
|
|
91
|
-
|
|
92
|
-
return { isMobile, isTablet, isDesktop: !isMobile && !isTablet };
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
const { isMobile, isTablet } = getDeviceType();
|
|
96
|
-
|
|
97
81
|
const titleId = useId();
|
|
98
82
|
const panelRef = useRef(null);
|
|
99
83
|
const returnFocusRef = useRef(null);
|
|
@@ -157,18 +141,6 @@ const StandardModal = ({
|
|
|
157
141
|
}
|
|
158
142
|
}, []);
|
|
159
143
|
|
|
160
|
-
// Handle window resize
|
|
161
|
-
useEffect(() => {
|
|
162
|
-
if (typeof window === 'undefined') return;
|
|
163
|
-
|
|
164
|
-
const handleResize = () => {
|
|
165
|
-
setWindowWidth(window.innerWidth);
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
window.addEventListener('resize', handleResize);
|
|
169
|
-
return () => window.removeEventListener('resize', handleResize);
|
|
170
|
-
}, []);
|
|
171
|
-
|
|
172
144
|
// Handle keyboard events
|
|
173
145
|
const handleKeyDown = useCallback((e) => {
|
|
174
146
|
if (!isOpen) return;
|
|
@@ -178,17 +150,36 @@ const StandardModal = ({
|
|
|
178
150
|
}
|
|
179
151
|
}, [isOpen, closeOnEscape, onClose]);
|
|
180
152
|
|
|
153
|
+
/*
|
|
154
|
+
* Escape, and the page-scroll lock.
|
|
155
|
+
*
|
|
156
|
+
* The lock used to put `overflow: unset` back rather than what it found,
|
|
157
|
+
* which is not the same thing whenever something else is already holding
|
|
158
|
+
* the page still — the navigation drawer's own lock, most obviously, since
|
|
159
|
+
* a record form can be opened from inside it on a phone. Closing the
|
|
160
|
+
* dialog released the drawer's lock too. The previous VALUE is captured
|
|
161
|
+
* and restored.
|
|
162
|
+
*/
|
|
181
163
|
useEffect(() => {
|
|
182
|
-
if (isOpen) {
|
|
183
|
-
|
|
184
|
-
document.body.style.overflow = 'hidden';
|
|
185
|
-
} else {
|
|
186
|
-
document.body.style.overflow = 'unset';
|
|
164
|
+
if (!isOpen) {
|
|
165
|
+
return undefined;
|
|
187
166
|
}
|
|
188
167
|
|
|
168
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
169
|
+
|
|
170
|
+
const previousOverflow = document.body.style.overflow;
|
|
171
|
+
const previousOverscroll = document.body.style.overscrollBehavior;
|
|
172
|
+
|
|
173
|
+
document.body.style.overflow = 'hidden';
|
|
174
|
+
// iOS keeps scrolling the document once a scroll inside the panel
|
|
175
|
+
// reaches its end. `overscroll-behavior: contain` on the panel stops
|
|
176
|
+
// the chain going up; this stops the document passing it on.
|
|
177
|
+
document.body.style.overscrollBehavior = 'none';
|
|
178
|
+
|
|
189
179
|
return () => {
|
|
190
180
|
document.removeEventListener('keydown', handleKeyDown);
|
|
191
|
-
document.body.style.overflow =
|
|
181
|
+
document.body.style.overflow = previousOverflow;
|
|
182
|
+
document.body.style.overscrollBehavior = previousOverscroll;
|
|
192
183
|
};
|
|
193
184
|
}, [isOpen, handleKeyDown]);
|
|
194
185
|
|
|
@@ -210,136 +201,57 @@ const StandardModal = ({
|
|
|
210
201
|
|
|
211
202
|
const isSheet = variant === 'sheet';
|
|
212
203
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
// list the sheet exists to keep visible, so the sheet dims less.
|
|
223
|
-
backgroundColor: isSheet
|
|
224
|
-
? 'rgba(15, 23, 32, 0.32)'
|
|
225
|
-
: 'rgba(0, 0, 0, 0.5)',
|
|
226
|
-
display: 'flex',
|
|
227
|
-
alignItems: isSheet
|
|
228
|
-
? 'stretch'
|
|
229
|
-
: verticalAlign === 'top'
|
|
230
|
-
? 'flex-start'
|
|
231
|
-
: 'center',
|
|
232
|
-
// A sheet is pinned to the right edge; a modal is centred.
|
|
233
|
-
justifyContent: isSheet ? 'flex-end' : 'center',
|
|
234
|
-
zIndex: 1000,
|
|
235
|
-
padding: isSheet ? 0 : '20px',
|
|
236
|
-
...customStyles.overlay
|
|
237
|
-
};
|
|
238
|
-
|
|
239
|
-
if (!isSheet && verticalAlign === 'top') {
|
|
240
|
-
baseStyle.paddingTop = '5%';
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return baseStyle;
|
|
244
|
-
};
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* A sheet's width is FIXED, not a share of the window.
|
|
204
|
+
/*
|
|
205
|
+
* The overlay and the panel are CLASSED, not styled inline.
|
|
206
|
+
*
|
|
207
|
+
* Everything that was in `getOverlayStyle` / `getContentStyle` /
|
|
208
|
+
* `getSheetStyle` lives in StandardModal.module.scss now, which is what
|
|
209
|
+
* lets a phone get `100dvh` (with `100vh` behind it), a real bottom-sheet
|
|
210
|
+
* shape, and the right geometry on the FIRST paint rather than one resize
|
|
211
|
+
* listener later. That file carries the full reasoning, the phone and
|
|
212
|
+
* tablet breakpoints, and the z-index order.
|
|
248
213
|
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
* on a wide screen. Below the tablet breakpoint the sheet becomes
|
|
252
|
-
* full-width, which is the only sensible shape on a phone.
|
|
214
|
+
* `customStyles` is still spread inline and therefore still wins over all
|
|
215
|
+
* of it, which is the contract the SMS composer and the gallery depend on.
|
|
253
216
|
*/
|
|
254
|
-
const
|
|
255
|
-
|
|
217
|
+
const overlayClasses = [
|
|
218
|
+
styles.overlay,
|
|
219
|
+
isSheet ? styles['overlay--sheet'] : styles['overlay--modal'],
|
|
220
|
+
!isSheet && verticalAlign === 'top' ? styles['overlay--top'] : '',
|
|
221
|
+
]
|
|
222
|
+
.filter(Boolean)
|
|
223
|
+
.join(' ');
|
|
256
224
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
display: 'flex',
|
|
264
|
-
flexDirection: 'column',
|
|
265
|
-
borderRadius: 0,
|
|
266
|
-
boxShadow: '-12px 0 40px -12px rgba(0, 0, 0, 0.28)',
|
|
267
|
-
overflowY: 'auto',
|
|
268
|
-
// Honours prefers-reduced-motion via the media query below; the
|
|
269
|
-
// transform is the entry, and it is deliberately short.
|
|
270
|
-
animation: 'visnsSheetIn 180ms cubic-bezier(0.32, 0.72, 0, 1)',
|
|
271
|
-
...customStyles.modal,
|
|
272
|
-
};
|
|
273
|
-
};
|
|
225
|
+
/* A size that is not one of the three is 'medium', the documented default.
|
|
226
|
+
Checked against the list rather than trusting `styles[...]` to be
|
|
227
|
+
undefined for an unknown key: a config typo would otherwise produce a
|
|
228
|
+
class name that exists in the markup and in no stylesheet, which is an
|
|
229
|
+
unsized panel — full-bleed on a desktop — and nothing to grep for. */
|
|
230
|
+
const sizeKey = PANEL_SIZES.includes(size) ? size : 'medium';
|
|
274
231
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
position: 'relative',
|
|
283
|
-
backgroundColor: 'white',
|
|
284
|
-
borderRadius: '8px',
|
|
285
|
-
boxShadow: '0 10px 30px rgba(0, 0, 0, 0.3)',
|
|
286
|
-
maxHeight: isMobile || isTablet ? '85vh' : '90vh',
|
|
287
|
-
overflowY: 'auto'
|
|
288
|
-
};
|
|
289
|
-
|
|
290
|
-
/*
|
|
291
|
-
* customStyles.modal is spread LAST, after the size preset, because a
|
|
292
|
-
* caller that bothers to pass one is overriding the default rather
|
|
293
|
-
* than asking to be overridden by it. Spread before the preset (as it
|
|
294
|
-
* once was) the width/minWidth/maxWidth a size sets could never be
|
|
295
|
-
* touched - which is what the SMS compose modal needs: a form-shaped
|
|
296
|
-
* width, and a visible overflow so its templates menu can open past
|
|
297
|
-
* the modal's own edge.
|
|
298
|
-
*/
|
|
299
|
-
|
|
300
|
-
// Apply size-based styles with device considerations
|
|
301
|
-
switch (size) {
|
|
302
|
-
case 'small':
|
|
303
|
-
return {
|
|
304
|
-
...baseStyle,
|
|
305
|
-
width: isMobile ? '95vw' : '400px',
|
|
306
|
-
minWidth: isMobile ? '300px' : '400px',
|
|
307
|
-
maxWidth: isMobile ? '95vw' : '600px',
|
|
308
|
-
...customStyles.modal
|
|
309
|
-
};
|
|
310
|
-
case 'large':
|
|
311
|
-
return {
|
|
312
|
-
...baseStyle,
|
|
313
|
-
width: '95vw',
|
|
314
|
-
minWidth: isMobile ? '300px' : '800px',
|
|
315
|
-
maxWidth: isMobile ? '95vw' : '1600px',
|
|
316
|
-
...customStyles.modal
|
|
317
|
-
};
|
|
318
|
-
case 'medium':
|
|
319
|
-
default:
|
|
320
|
-
return {
|
|
321
|
-
...baseStyle,
|
|
322
|
-
width: isMobile ? '95vw' : '80vw',
|
|
323
|
-
minWidth: isMobile ? '300px' : '600px',
|
|
324
|
-
maxWidth: isMobile ? '95vw' : '1200px',
|
|
325
|
-
...customStyles.modal
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
};
|
|
232
|
+
const panelClasses = [
|
|
233
|
+
styles.panel,
|
|
234
|
+
isSheet ? styles['panel--sheet'] : styles['panel--modal'],
|
|
235
|
+
styles[`panel--${sizeKey}`],
|
|
236
|
+
]
|
|
237
|
+
.filter(Boolean)
|
|
238
|
+
.join(' ');
|
|
329
239
|
|
|
330
240
|
if (!isOpen) {
|
|
331
241
|
return null;
|
|
332
242
|
}
|
|
333
243
|
|
|
334
244
|
return (
|
|
335
|
-
<div
|
|
336
|
-
|
|
245
|
+
<div
|
|
246
|
+
className={overlayClasses}
|
|
247
|
+
style={customStyles.overlay}
|
|
337
248
|
onClick={handleBackdropClick}
|
|
338
249
|
>
|
|
339
250
|
<style>{SHEET_KEYFRAMES}</style>
|
|
340
|
-
<div
|
|
251
|
+
<div
|
|
341
252
|
ref={panelRef}
|
|
342
|
-
|
|
253
|
+
className={panelClasses}
|
|
254
|
+
style={customStyles.modal}
|
|
343
255
|
onClick={(e) => e.stopPropagation()}
|
|
344
256
|
/* This had none of these. Without `role="dialog"` and
|
|
345
257
|
`aria-modal` a screen reader treats the panel as ordinary
|
|
@@ -363,6 +275,7 @@ const StandardModal = ({
|
|
|
363
275
|
onClick={onClose}
|
|
364
276
|
style={customStyles.closeButton}
|
|
365
277
|
type="button"
|
|
278
|
+
title="Close this dialog without saving anything"
|
|
366
279
|
aria-label="Close modal"
|
|
367
280
|
>
|
|
368
281
|
<X strokeWidth={1} size={24} />
|