@visns-studio/visns-components 6.15.2 → 6.16.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.
package/README.md CHANGED
@@ -300,6 +300,7 @@ connected* rather than as an error, because it is not one.
300
300
  | `echo` | `null` | An Echo instance **or** a factory (`() => getEcho()`). A factory is called inside the effect, so a user without messaging never opens a Pusher connection. |
301
301
  | `channelFor` | `` (id) => `sms-line.${id}` `` | Return the environment-suffixed name if the deployment shares a Pusher app. |
302
302
  | `inboxUrl` | `'/sms'` | A row navigates to `` `${inboxUrl}?thread=<id>` ``. |
303
+ | `shortcut` | `'mod+shift+m'` | Opens/closes the popover from anywhere - ⌘⇧M on a Mac, Ctrl+Shift+M elsewhere (⌘M minimises the window on macOS). A string or a list, like the vault's. |
303
304
  | `limit` | `8` | Sent as `per_page`. |
304
305
 
305
306
  The trigger is the same 40px chip as `Notification` and `VaultQuickSearch`,
package/package.json CHANGED
@@ -95,7 +95,7 @@
95
95
  "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
96
96
  },
97
97
  "name": "@visns-studio/visns-components",
98
- "version": "6.15.2",
98
+ "version": "6.16.0",
99
99
  "description": "Various packages to assist in the development of our Custom Applications.",
100
100
  "main": "src/index.js",
101
101
  "files": [
@@ -3141,9 +3141,9 @@ const DataGrid = forwardRef(
3141
3141
  backgroundColor:
3142
3142
  'var(--light-bg-color, #f5f5f5)',
3143
3143
  borderRadius: '4px',
3144
- padding: '4px',
3145
- width: '28px',
3146
- height: '28px',
3144
+ padding: '3px',
3145
+ width: '26px',
3146
+ height: '26px',
3147
3147
  boxShadow: '0 1px 2px rgba(0,0,0,0.05)',
3148
3148
  margin: 0,
3149
3149
  cursor: 'pointer',
@@ -3190,9 +3190,9 @@ const DataGrid = forwardRef(
3190
3190
  backgroundColor:
3191
3191
  'var(--light-bg-color, #f5f5f5)',
3192
3192
  borderRadius: '4px',
3193
- padding: '4px',
3194
- width: '28px',
3195
- height: '28px',
3193
+ padding: '3px',
3194
+ width: '26px',
3195
+ height: '26px',
3196
3196
  boxShadow: '0 1px 2px rgba(0,0,0,0.05)',
3197
3197
  margin: 0,
3198
3198
  cursor: 'pointer',
@@ -3283,8 +3283,8 @@ const DataGrid = forwardRef(
3283
3283
  key={`coding-slot-${config.id[0]}-${configKey}`}
3284
3284
  style={{
3285
3285
  display: 'inline-flex',
3286
- width: '28px',
3287
- height: '28px',
3286
+ width: '26px',
3287
+ height: '26px',
3288
3288
  margin: '0 1px',
3289
3289
  }}
3290
3290
  />
@@ -3296,13 +3296,14 @@ const DataGrid = forwardRef(
3296
3296
  return (
3297
3297
  <div
3298
3298
  style={{
3299
- // Same shape as the action column's cluster: one row,
3300
- // a small even gap, centred in the cell.
3299
+ // One row of fixed slots, left-aligned so every slot
3300
+ // lands in the same place on every row (the other
3301
+ // coding columns - clients, workflow - read that way).
3301
3302
  display: 'flex',
3302
3303
  flexWrap: 'nowrap',
3303
3304
  alignItems: 'center',
3304
- justifyContent: 'center',
3305
- gap: '4px',
3305
+ justifyContent: 'flex-start',
3306
+ gap: '2px',
3306
3307
  width: '100%',
3307
3308
  padding: 0,
3308
3309
  minHeight: '24px',
@@ -9,6 +9,11 @@ import styles from '../styles/Sms.module.scss';
9
9
  // works inside a Router and outside one. Imported rather than copied so there
10
10
  // is one implementation of that decision in the library.
11
11
  import withVaultNavigation from '../vault/vaultNavigation';
12
+ import {
13
+ matchesShortcut,
14
+ shortcutLabel,
15
+ shouldIgnoreShortcut,
16
+ } from '../vault/vaultHelpers';
12
17
  import { resolveSmsEndpoints } from './smsEndpoints';
13
18
  import useSmsLive, { defaultChannelFor } from './useSmsLive';
14
19
  import {
@@ -56,10 +61,35 @@ const SmsInboxBadgeInner = ({
56
61
  inboxUrl = '/sms',
57
62
  limit = 8,
58
63
  navigate,
64
+ // ⌘⇧M / Ctrl+Shift+M. Not ⌘M: macOS minimises the window on that. A
65
+ // string or a list, like the vault's; the first one is what the tooltip
66
+ // prints.
67
+ shortcut = 'mod+shift+m',
59
68
  }) => {
60
69
  const routes = useMemo(() => resolveSmsEndpoints(endpoints), [endpoints]);
61
70
 
62
71
  const [open, setOpen] = useState(false);
72
+
73
+ // Global, captured: same reasoning as the vault - the point of a chord is
74
+ // not reaching for the mouse, and a data grid or rich editor that stops
75
+ // keydown propagation must not swallow it.
76
+ const shortcutRef = useRef(shortcut);
77
+ shortcutRef.current = shortcut;
78
+
79
+ useEffect(() => {
80
+ const onKeyDown = (event) => {
81
+ if (!matchesShortcut(event, shortcutRef.current)) return;
82
+ if (shouldIgnoreShortcut(event)) return;
83
+
84
+ event.preventDefault();
85
+ event.stopPropagation();
86
+ setOpen((prev) => !prev);
87
+ };
88
+
89
+ window.addEventListener('keydown', onKeyDown, true);
90
+
91
+ return () => window.removeEventListener('keydown', onKeyDown, true);
92
+ }, []);
63
93
  const [status, setStatus] = useState(null);
64
94
  const [lines, setLines] = useState([]);
65
95
  const [threads, setThreads] = useState([]);
@@ -293,6 +323,7 @@ const SmsInboxBadgeInner = ({
293
323
  /* --------------------------------------------------------------- render */
294
324
 
295
325
  const label = setting?.label || 'Messages';
326
+ const hint = shortcutLabel(shortcut);
296
327
  const count = badgeCount(unread);
297
328
 
298
329
  // Two different sentences, because they mean two different things: one is
@@ -368,8 +399,9 @@ const SmsInboxBadgeInner = ({
368
399
  onClick={() => (open ? close() : setOpen(true))}
369
400
  data-tooltip-id="system-tooltip"
370
401
  data-tooltip-place="bottom"
371
- data-tooltip-content={label}
402
+ data-tooltip-content={hint ? `${label} · ${hint}` : label}
372
403
  aria-label={unread > 0 ? `${label} (${unread} unread)` : label}
404
+ aria-keyshortcuts="Meta+Shift+M Control+Shift+M"
373
405
  aria-expanded={open}
374
406
  aria-haspopup="dialog"
375
407
  >
@@ -23,46 +23,11 @@ import {
23
23
  hostFromUrl,
24
24
  isMacPlatform,
25
25
  matchesShortcut,
26
+ shouldIgnoreShortcut,
26
27
  openableUrl,
27
28
  shortcutLabel,
28
29
  } from './vaultHelpers';
29
30
 
30
- /** What the shortcut must not interrupt. */
31
- const MODAL_SELECTOR = [
32
- 'dialog[open]',
33
- '[aria-modal="true"]',
34
- // SweetAlert, which the library uses for confirmations, and which takes
35
- // the keyboard for itself while it is up.
36
- '.swal2-container',
37
- ].join(', ');
38
-
39
- /**
40
- * Should this keystroke be left alone?
41
- *
42
- * ⌘K is a cheap chord, which is the point of it and also the risk: it is the
43
- * kind of key an editor or a spreadsheet grid binds for itself. So the vault
44
- * declines it when something modal is already on screen, and when the caret is
45
- * in rich text — a TinyMCE body is `contenteditable` inside an iframe, and an
46
- * editor toolbar's own ⌘K (insert link) has a better claim on the keystroke
47
- * than a header popover does.
48
- */
49
- const shouldIgnoreShortcut = (event) => {
50
- if (typeof document === 'undefined') return false;
51
-
52
- if (document.querySelector(MODAL_SELECTOR)) return true;
53
-
54
- const target = event.target;
55
-
56
- // A keydown inside an iframe never reaches this window in the first place;
57
- // this catches the case where the iframe itself holds focus.
58
- if (target?.tagName === 'IFRAME' || document.activeElement?.tagName === 'IFRAME') {
59
- return true;
60
- }
61
-
62
- if (target?.isContentEditable) return true;
63
-
64
- return Boolean(target?.closest?.('[contenteditable="true"], [contenteditable=""]'));
65
- };
66
31
 
67
32
  /**
68
33
  * The vault, from the header.
@@ -545,3 +545,40 @@ export const parseTags = (input) => {
545
545
 
546
546
  return out;
547
547
  };
548
+
549
+ /** What the shortcut must not interrupt. */
550
+ export const MODAL_SELECTOR = [
551
+ 'dialog[open]',
552
+ '[aria-modal="true"]',
553
+ // SweetAlert, which the library uses for confirmations, and which takes
554
+ // the keyboard for itself while it is up.
555
+ '.swal2-container',
556
+ ].join(', ');
557
+
558
+ /**
559
+ * Should this keystroke be left alone?
560
+ *
561
+ * ⌘K is a cheap chord, which is the point of it and also the risk: it is the
562
+ * kind of key an editor or a spreadsheet grid binds for itself. So the vault
563
+ * declines it when something modal is already on screen, and when the caret is
564
+ * in rich text — a TinyMCE body is `contenteditable` inside an iframe, and an
565
+ * editor toolbar's own ⌘K (insert link) has a better claim on the keystroke
566
+ * than a header popover does.
567
+ */
568
+ export const shouldIgnoreShortcut = (event) => {
569
+ if (typeof document === 'undefined') return false;
570
+
571
+ if (document.querySelector(MODAL_SELECTOR)) return true;
572
+
573
+ const target = event.target;
574
+
575
+ // A keydown inside an iframe never reaches this window in the first place;
576
+ // this catches the case where the iframe itself holds focus.
577
+ if (target?.tagName === 'IFRAME' || document.activeElement?.tagName === 'IFRAME') {
578
+ return true;
579
+ }
580
+
581
+ if (target?.isContentEditable) return true;
582
+
583
+ return Boolean(target?.closest?.('[contenteditable="true"], [contenteditable=""]'));
584
+ };