@visns-studio/visns-components 6.20.1 → 6.23.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/package.json +1 -1
- package/src/components/DataGrid.jsx +15 -2
- package/src/components/Navigation.jsx +10 -1
- package/src/components/columns/SelectFilterEditor.jsx +372 -0
- package/src/components/controls/DataGridSearch.jsx +6 -1
- package/src/components/layout/AnchoredMenu.jsx +131 -0
- package/src/components/layout/BandedList.jsx +158 -0
- package/src/components/layout/FactsBand.jsx +150 -0
- package/src/components/layout/PageHeader.jsx +90 -0
- package/src/components/layout/ScrollRegion.jsx +53 -0
- package/src/components/layout/StatStrip.jsx +170 -0
- package/src/components/layout/useContainedHeight.js +82 -0
- package/src/components/navigation/TabStrip.jsx +156 -0
- package/src/components/phone/ZoomPhoneBadge.jsx +625 -0
- package/src/components/phone/phonePresenceEndpoints.js +34 -0
- package/src/components/phone/phonePresenceHelpers.js +298 -0
- package/src/components/phone/useZoomPhoneLive.js +165 -0
- package/src/components/sms/SmsClientConversations.jsx +8 -0
- package/src/components/sms/SmsInbox.jsx +11 -0
- package/src/components/sms/SmsThreadPanel.jsx +258 -29
- package/src/components/sms/smsHelpers.js +95 -0
- package/src/components/styles/AnchoredMenu.module.scss +43 -0
- package/src/components/styles/BandedList.module.scss +278 -0
- package/src/components/styles/DataGrid.module.scss +10 -0
- package/src/components/styles/FactsBand.module.scss +108 -0
- package/src/components/styles/GenericDetail.module.scss +12 -2
- package/src/components/styles/PageHeader.module.scss +98 -0
- package/src/components/styles/ScrollRegion.module.scss +20 -0
- package/src/components/styles/Sms.module.scss +131 -0
- package/src/components/styles/StatStrip.module.scss +183 -0
- package/src/components/styles/TabStrip.module.scss +96 -0
- package/src/components/styles/Vault.module.scss +46 -0
- package/src/components/styles/ZoomPhone.module.scss +550 -0
- package/src/components/styles/_surface.scss +49 -0
- package/src/components/styles/global-datagrid.css +23 -0
- package/src/components/vault/VaultAccessLog.jsx +24 -0
- package/src/components/vault/VaultShareModal.jsx +126 -5
- package/src/components/vault/vaultShares.js +67 -3
- package/src/index.js +88 -2
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* "Be as tall as whatever is left of the window, and scroll inside that."
|
|
5
|
+
*
|
|
6
|
+
* The problem it solves is not scrolling, it is LOSING THE CHROME. A list of
|
|
7
|
+
* four hundred clients scrolls the whole document, which takes the summary
|
|
8
|
+
* strip, the search box and the view tabs off the top of the screen — so
|
|
9
|
+
* finding a name and then narrowing the search means scrolling back up to
|
|
10
|
+
* controls that were in front of you a moment ago. Give the rows their own
|
|
11
|
+
* scroll region and everything above them stays put.
|
|
12
|
+
*
|
|
13
|
+
* WHY MEASURED RATHER THAN A CONSTANT. The obvious implementation is
|
|
14
|
+
* `max-height: calc(100vh - 22rem)` with the 22rem counted off the design. It
|
|
15
|
+
* is wrong on half the states of a real page: the strip has not loaded yet, a
|
|
16
|
+
* truncation notice appeared at a hundred rows, the header wrapped to two lines
|
|
17
|
+
* on a tablet. So the height is read from where the region actually starts —
|
|
18
|
+
* `getBoundingClientRect().top` — and recomputed whenever the window resizes or
|
|
19
|
+
* anything above it changes size.
|
|
20
|
+
*
|
|
21
|
+
* The `ResizeObserver` watches the region's PARENT rather than the region
|
|
22
|
+
* itself: the region's own height is the thing being set, and observing it
|
|
23
|
+
* would be a loop. What is wanted is "did the chrome above me move?", and the
|
|
24
|
+
* parent growing is the signal for that.
|
|
25
|
+
*
|
|
26
|
+
* Returns `null` until the first measurement, which is deliberate — a region
|
|
27
|
+
* rendered with no max-height for one frame is a page that is briefly too long,
|
|
28
|
+
* where one rendered at a guessed height is a page that visibly jumps.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} ref Ref on the scrolling element.
|
|
31
|
+
* @param {number} gutter Space to leave under it, in pixels.
|
|
32
|
+
* @param {*} deps Anything that changes what is above it.
|
|
33
|
+
* @param {boolean} enabled False leaves the height null and installs no
|
|
34
|
+
* listeners — for a component where the containment
|
|
35
|
+
* is opt-in and the hook still has to be called
|
|
36
|
+
* unconditionally.
|
|
37
|
+
*/
|
|
38
|
+
const useContainedHeight = (ref, gutter = 24, deps = null, enabled = true) => {
|
|
39
|
+
const [height, setHeight] = useState(null);
|
|
40
|
+
|
|
41
|
+
const measure = useCallback(() => {
|
|
42
|
+
const node = ref.current;
|
|
43
|
+
|
|
44
|
+
if (!enabled || !node || typeof window === 'undefined') return;
|
|
45
|
+
|
|
46
|
+
const top = node.getBoundingClientRect().top;
|
|
47
|
+
const available = window.innerHeight - top - gutter;
|
|
48
|
+
|
|
49
|
+
// A floor rather than a clamp to zero: on a short window the region
|
|
50
|
+
// still has to be usable, and 200px is about four rows.
|
|
51
|
+
setHeight(Math.max(available, 200));
|
|
52
|
+
}, [ref, gutter, enabled]);
|
|
53
|
+
|
|
54
|
+
useEffect(() => {
|
|
55
|
+
if (!enabled) return undefined;
|
|
56
|
+
|
|
57
|
+
measure();
|
|
58
|
+
|
|
59
|
+
window.addEventListener('resize', measure);
|
|
60
|
+
|
|
61
|
+
// Orientation changes on an iPad settle a beat after the event.
|
|
62
|
+
const settle = setTimeout(measure, 200);
|
|
63
|
+
|
|
64
|
+
let observer = null;
|
|
65
|
+
|
|
66
|
+
if (typeof ResizeObserver !== 'undefined' && ref.current?.parentElement) {
|
|
67
|
+
observer = new ResizeObserver(measure);
|
|
68
|
+
observer.observe(ref.current.parentElement);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return () => {
|
|
72
|
+
window.removeEventListener('resize', measure);
|
|
73
|
+
clearTimeout(settle);
|
|
74
|
+
if (observer) observer.disconnect();
|
|
75
|
+
};
|
|
76
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
77
|
+
}, [measure, deps, enabled]);
|
|
78
|
+
|
|
79
|
+
return height;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export default useContainedHeight;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import { useSearchParams } from 'react-router-dom';
|
|
3
|
+
|
|
4
|
+
import styles from '../styles/TabStrip.module.scss';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* react-router's search params, if there is a Router.
|
|
8
|
+
*
|
|
9
|
+
* Same shape and the same reasoning as `auth/useOptionalRouter`: the hook is
|
|
10
|
+
* called unconditionally on every render and only its throw is swallowed, which
|
|
11
|
+
* is what keeps this legal under the rules of hooks — whether a Router is above
|
|
12
|
+
* a component is fixed for that component's lifetime, so React never sees the
|
|
13
|
+
* hook count change.
|
|
14
|
+
*
|
|
15
|
+
* A strip used outside a Router simply cannot be URL-driven, and falls back to
|
|
16
|
+
* being controlled.
|
|
17
|
+
*/
|
|
18
|
+
const useOptionalSearchParams = () => {
|
|
19
|
+
try {
|
|
20
|
+
return useSearchParams();
|
|
21
|
+
} catch (error) {
|
|
22
|
+
// No Router above us. Expected wherever this is mounted outside an SPA.
|
|
23
|
+
return [null, null];
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The underline tabs, driven either by the URL or by the caller.
|
|
29
|
+
*
|
|
30
|
+
* TWO MODES, and which one a page wants depends on whether the tab is a place.
|
|
31
|
+
*
|
|
32
|
+
* CONTROLLED — pass `active` and `onChange`. The right answer when the tab is
|
|
33
|
+
* a filter over the same page ("Active / Retired / All"): those are not
|
|
34
|
+
* locations, and putting them in the URL invites somebody to bookmark a view
|
|
35
|
+
* of a list rather than the list.
|
|
36
|
+
*
|
|
37
|
+
* URL-DRIVEN — pass `param` (usually `'tab'`) and no `active`. The right
|
|
38
|
+
* answer when the tab is a SECTION of a record: a deep link, a refresh and a
|
|
39
|
+
* shared link all have to land in the same place. `replace` defaults to true
|
|
40
|
+
* so five tab clicks do not become five back-button steps through a page
|
|
41
|
+
* nobody ever left; a page whose tabs genuinely are separate destinations can
|
|
42
|
+
* turn it off.
|
|
43
|
+
*
|
|
44
|
+
* Real `<button>`s in a tablist rather than links, in both modes: even when the
|
|
45
|
+
* tab is reflected in the URL, switching it is a state change rather than a
|
|
46
|
+
* navigation, and a router push per click fills the back button with noise.
|
|
47
|
+
*
|
|
48
|
+
* @param {Array} tabs `[{key, label, badge, badgeTone, disabled}]`.
|
|
49
|
+
* @param {string} active Controlled mode: the current key.
|
|
50
|
+
* @param {func} onChange Called with the key. Fires in URL mode too, so a
|
|
51
|
+
* page can invalidate a cache on the way past.
|
|
52
|
+
* @param {string} param URL mode: the search param that owns the tab.
|
|
53
|
+
* @param {bool} replace URL mode: replace rather than push. Default true.
|
|
54
|
+
* @param {string} fallback What an unrecognised (or absent) param means.
|
|
55
|
+
* Defaults to the first tab.
|
|
56
|
+
* @param {'quiet'|'danger'} badgeTone Default tone for every badge.
|
|
57
|
+
*/
|
|
58
|
+
const TabStrip = ({
|
|
59
|
+
tabs = [],
|
|
60
|
+
active,
|
|
61
|
+
onChange,
|
|
62
|
+
param,
|
|
63
|
+
replace = true,
|
|
64
|
+
fallback,
|
|
65
|
+
ariaLabel = 'Sections',
|
|
66
|
+
badgeTone = 'quiet',
|
|
67
|
+
className,
|
|
68
|
+
tabClassName,
|
|
69
|
+
...rest
|
|
70
|
+
}) => {
|
|
71
|
+
const [searchParams, setSearchParams] = useOptionalSearchParams();
|
|
72
|
+
|
|
73
|
+
const urlDriven = Boolean(param) && searchParams !== null && active === undefined;
|
|
74
|
+
|
|
75
|
+
const known = (key) => tabs.some((tab) => tab.key === key);
|
|
76
|
+
const defaultKey = fallback ?? tabs[0]?.key;
|
|
77
|
+
|
|
78
|
+
let current = active;
|
|
79
|
+
|
|
80
|
+
if (urlDriven) {
|
|
81
|
+
const requested = searchParams.get(param);
|
|
82
|
+
|
|
83
|
+
current = known(requested) ? requested : defaultKey;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const select = useCallback(
|
|
87
|
+
(key) => {
|
|
88
|
+
if (urlDriven) {
|
|
89
|
+
setSearchParams(
|
|
90
|
+
(params) => {
|
|
91
|
+
const next = new URLSearchParams(params);
|
|
92
|
+
|
|
93
|
+
next.set(param, key);
|
|
94
|
+
|
|
95
|
+
return next;
|
|
96
|
+
},
|
|
97
|
+
{ replace }
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (onChange) onChange(key);
|
|
102
|
+
},
|
|
103
|
+
[urlDriven, setSearchParams, param, replace, onChange]
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (tabs.length === 0) return null;
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div
|
|
110
|
+
className={[styles.strip, className].filter(Boolean).join(' ')}
|
|
111
|
+
role="tablist"
|
|
112
|
+
aria-label={ariaLabel}
|
|
113
|
+
{...rest}
|
|
114
|
+
>
|
|
115
|
+
{tabs.map((tab) => {
|
|
116
|
+
const selected = current === tab.key;
|
|
117
|
+
const tone = tab.badgeTone ?? badgeTone;
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<button
|
|
121
|
+
key={tab.key}
|
|
122
|
+
type="button"
|
|
123
|
+
role="tab"
|
|
124
|
+
aria-selected={selected}
|
|
125
|
+
disabled={tab.disabled}
|
|
126
|
+
className={[
|
|
127
|
+
styles.tab,
|
|
128
|
+
selected ? styles.active : null,
|
|
129
|
+
tabClassName,
|
|
130
|
+
]
|
|
131
|
+
.filter(Boolean)
|
|
132
|
+
.join(' ')}
|
|
133
|
+
onClick={() => select(tab.key)}
|
|
134
|
+
>
|
|
135
|
+
{tab.label}
|
|
136
|
+
|
|
137
|
+
{tab.badge ? (
|
|
138
|
+
<span
|
|
139
|
+
className={[
|
|
140
|
+
styles.badge,
|
|
141
|
+
tone === 'danger'
|
|
142
|
+
? styles.badgeDanger
|
|
143
|
+
: styles.badgeQuiet,
|
|
144
|
+
].join(' ')}
|
|
145
|
+
>
|
|
146
|
+
{tab.badge}
|
|
147
|
+
</span>
|
|
148
|
+
) : null}
|
|
149
|
+
</button>
|
|
150
|
+
);
|
|
151
|
+
})}
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
export default TabStrip;
|