@brdgapp/ui 0.1.1 → 1.0.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/CHANGELOG.md +83 -0
- package/README.md +39 -85
- package/components/action-menu.d.ts +65 -0
- package/components/action-menu.js +48 -0
- package/components/avatar.d.ts +50 -0
- package/components/avatar.js +91 -0
- package/components/badge.d.ts +12 -0
- package/components/badge.js +23 -0
- package/components/bottom-sheet.d.ts +17 -0
- package/components/bottom-sheet.js +12 -0
- package/components/brand-mark.d.ts +20 -0
- package/components/brand-mark.js +44 -0
- package/components/button-group.d.ts +25 -0
- package/components/button-group.js +53 -0
- package/components/button.d.ts +33 -0
- package/components/button.js +82 -0
- package/components/card.d.ts +78 -0
- package/components/card.js +75 -0
- package/components/checkbox.d.ts +22 -0
- package/components/checkbox.js +35 -0
- package/components/collapsible.d.ts +29 -0
- package/components/collapsible.js +38 -0
- package/components/column-filter-menu.d.ts +15 -0
- package/components/column-filter-menu.js +30 -0
- package/components/column-sort-button.d.ts +18 -0
- package/components/column-sort-button.js +14 -0
- package/components/confirm-dialog.d.ts +20 -0
- package/components/confirm-dialog.js +17 -0
- package/components/copy-field.d.ts +17 -0
- package/components/copy-field.js +36 -0
- package/components/data-state.d.ts +44 -0
- package/components/data-state.js +25 -0
- package/components/data-table.d.ts +65 -0
- package/components/data-table.js +84 -0
- package/components/dialog.d.ts +19 -0
- package/components/dialog.js +10 -0
- package/components/drawer.d.ts +21 -0
- package/components/drawer.js +17 -0
- package/components/expandable-bio.d.ts +8 -0
- package/components/expandable-bio.js +26 -0
- package/components/field.d.ts +29 -0
- package/components/field.js +27 -0
- package/components/filter-chip.d.ts +11 -0
- package/components/filter-chip.js +8 -0
- package/components/filter-pill.d.ts +34 -0
- package/components/filter-pill.js +35 -0
- package/components/hover-card.d.ts +21 -0
- package/components/hover-card.js +20 -0
- package/components/icon-button.d.ts +16 -0
- package/components/icon-button.js +32 -0
- package/components/illustration.d.ts +33 -0
- package/components/illustration.js +70 -0
- package/components/info-hint.d.ts +29 -0
- package/components/info-hint.js +42 -0
- package/components/input.d.ts +9 -0
- package/components/input.js +15 -0
- package/components/network-graph.d.ts +48 -0
- package/components/network-graph.js +37 -0
- package/components/page-banner.d.ts +20 -0
- package/components/page-banner.js +29 -0
- package/components/scene.d.ts +12 -0
- package/components/scene.js +7 -0
- package/components/search-input.d.ts +8 -0
- package/components/search-input.js +14 -0
- package/components/segmented-control.d.ts +18 -0
- package/components/segmented-control.js +14 -0
- package/components/skeleton.d.ts +12 -0
- package/components/skeleton.js +21 -0
- package/components/skip-link.d.ts +8 -0
- package/components/skip-link.js +7 -0
- package/components/stat-box.d.ts +23 -0
- package/components/stat-box.js +29 -0
- package/components/switch.d.ts +13 -0
- package/components/switch.js +8 -0
- package/components/tabs.d.ts +6 -0
- package/components/tabs.js +25 -0
- package/components/tag-input.d.ts +15 -0
- package/components/tag-input.js +40 -0
- package/components/tag.d.ts +27 -0
- package/components/tag.js +35 -0
- package/components/textarea.d.ts +12 -0
- package/components/textarea.js +78 -0
- package/components/theme-switch.d.ts +6 -0
- package/components/theme-switch.js +16 -0
- package/components/toast.d.ts +25 -0
- package/components/toast.js +44 -0
- package/hooks/use-is-mobile.d.ts +2 -0
- package/hooks/use-is-mobile.js +16 -0
- package/hooks/use-mock-query.d.ts +23 -0
- package/hooks/use-mock-query.js +36 -0
- package/hooks/use-theme.d.ts +11 -0
- package/hooks/use-theme.js +34 -0
- package/lib/demo-controls.d.ts +24 -0
- package/lib/demo-controls.js +24 -0
- package/lib/format.d.ts +17 -0
- package/lib/format.js +55 -0
- package/lib/layout.d.ts +19 -0
- package/lib/layout.js +24 -0
- package/lib/motion.d.ts +16 -0
- package/lib/motion.js +18 -0
- package/lib/theme.d.ts +21 -0
- package/lib/theme.js +63 -0
- package/lib/utils.d.ts +22 -0
- package/lib/utils.js +27 -0
- package/package.json +32 -16
- package/styles/globals.css +595 -0
- package/src/index.ts +0 -18
- package/src/network-people-helper/README.md +0 -130
- package/src/network-people-helper/api.ts +0 -174
- package/src/network-people-helper/contact-avatar.tsx +0 -63
- package/src/network-people-helper/domain.ts +0 -14
- package/src/network-people-helper/icons.tsx +0 -130
- package/src/network-people-helper/index.ts +0 -6
- package/src/network-people-helper/network-people-helper.tsx +0 -397
- package/src/network-people-helper/network-people-nudge.tsx +0 -98
- package/src/network-people-helper/open-helper.ts +0 -22
- package/src/network-people-helper/person-menu.tsx +0 -211
- package/src/network-people-helper/styles.css +0 -848
- package/src/network-people-helper/types.ts +0 -23
- package/src/network-people-helper/use-network-people-list.ts +0 -122
- package/src/network-people-helper/use-network-preview.ts +0 -43
- package/src/provider.tsx +0 -59
- package/src/styles.css +0 -1
|
@@ -1,397 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { useEffect, useRef, useState, type CSSProperties } from "react"
|
|
4
|
-
import { ContactAvatar } from "./contact-avatar"
|
|
5
|
-
import { cleanDomain, organizationLabelFromDomain } from "./domain"
|
|
6
|
-
import {
|
|
7
|
-
BridgeHelperIcon,
|
|
8
|
-
ChevronDownIcon,
|
|
9
|
-
ChevronUpIcon,
|
|
10
|
-
EyeOffIcon,
|
|
11
|
-
SearchIcon,
|
|
12
|
-
} from "./icons"
|
|
13
|
-
import {
|
|
14
|
-
NETWORK_PEOPLE_HELPER_OPEN_EVENT,
|
|
15
|
-
dismissStorageKey,
|
|
16
|
-
type NetworkPeopleHelperOpenDetail,
|
|
17
|
-
} from "./open-helper"
|
|
18
|
-
import { PersonMenu, ShareLinkCopyControl } from "./person-menu"
|
|
19
|
-
import { useBridgeUI } from "../provider"
|
|
20
|
-
import type { HelperPlacement, NetworkPersonAction } from "./types"
|
|
21
|
-
import { useNetworkPeopleList } from "./use-network-people-list"
|
|
22
|
-
import { useNetworkPreview } from "./use-network-preview"
|
|
23
|
-
|
|
24
|
-
const ICON_HOLD_MS = 750
|
|
25
|
-
const REVEAL_HOLD_MS = 750
|
|
26
|
-
const SEARCH_DEBOUNCE_MS = 300
|
|
27
|
-
const DEFAULT_CHECK_DELAY_MS = 3000
|
|
28
|
-
|
|
29
|
-
type HelperPhase = "idle" | "icon" | "revealing" | "expanded" | "gone" | "dismissed"
|
|
30
|
-
|
|
31
|
-
export type NetworkPeopleHelperProps = {
|
|
32
|
-
domain: string
|
|
33
|
-
/** Shown after “at”. When omitted, the package uses the domain with www. removed. */
|
|
34
|
-
organizationLabel?: string | null
|
|
35
|
-
enabled?: boolean
|
|
36
|
-
/** Delay from page visit before the network check. Default 3s. */
|
|
37
|
-
checkDelayMs?: number
|
|
38
|
-
/** Fixed corner. Override the position with CSS variables on this root. */
|
|
39
|
-
placement?: HelperPlacement
|
|
40
|
-
className?: string
|
|
41
|
-
style?: CSSProperties
|
|
42
|
-
/**
|
|
43
|
-
* Extra person-menu actions. Built-in actions are Make intro, Copy intro link,
|
|
44
|
-
* and View Bridge profile. onSelect receives the contact for that row.
|
|
45
|
-
*/
|
|
46
|
-
customActions?: NetworkPersonAction[]
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function NetworkPeopleHelper({
|
|
50
|
-
domain,
|
|
51
|
-
organizationLabel,
|
|
52
|
-
enabled = true,
|
|
53
|
-
checkDelayMs = DEFAULT_CHECK_DELAY_MS,
|
|
54
|
-
placement = "bottom-end",
|
|
55
|
-
className,
|
|
56
|
-
style,
|
|
57
|
-
customActions = [],
|
|
58
|
-
}: NetworkPeopleHelperProps) {
|
|
59
|
-
const config = useBridgeUI()
|
|
60
|
-
const cleaned = cleanDomain(domain)
|
|
61
|
-
const [phase, setPhase] = useState<HelperPhase>("idle")
|
|
62
|
-
const [listOpen, setListOpen] = useState(false)
|
|
63
|
-
const [confirmDismiss, setConfirmDismiss] = useState(false)
|
|
64
|
-
const [prefersReducedMotion, setPrefersReducedMotion] = useState(false)
|
|
65
|
-
const [checkReady, setCheckReady] = useState(false)
|
|
66
|
-
const [listSearch, setListSearch] = useState("")
|
|
67
|
-
const [debouncedListSearch, setDebouncedListSearch] = useState("")
|
|
68
|
-
const loadMoreRef = useRef<HTMLDivElement | null>(null)
|
|
69
|
-
const searchInputRef = useRef<HTMLInputElement | null>(null)
|
|
70
|
-
const warnedRef = useRef(false)
|
|
71
|
-
|
|
72
|
-
const isDismissed = phase === "dismissed"
|
|
73
|
-
const delayBlocked = phase === "dismissed" || phase === "gone"
|
|
74
|
-
const preview = useNetworkPreview(cleaned, Boolean(config) && enabled && checkReady && !isDismissed)
|
|
75
|
-
const count = preview.count
|
|
76
|
-
const previewContacts = preview.contacts
|
|
77
|
-
const list = useNetworkPeopleList({
|
|
78
|
-
domain: cleaned,
|
|
79
|
-
enabled: listOpen && phase !== "dismissed" && phase !== "gone",
|
|
80
|
-
search: debouncedListSearch,
|
|
81
|
-
previewContacts,
|
|
82
|
-
totalCount: count,
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
const knowsPeople = preview.status === "ready" && count > 0
|
|
86
|
-
const label = organizationLabelFromDomain(cleaned, organizationLabel)
|
|
87
|
-
const hasActiveSearch = Boolean(debouncedListSearch.trim())
|
|
88
|
-
const showInitialListSkeleton = listOpen && list.people.length === 0 && list.isLoading && !list.isError
|
|
89
|
-
|
|
90
|
-
useEffect(() => {
|
|
91
|
-
if (config || warnedRef.current) return
|
|
92
|
-
warnedRef.current = true
|
|
93
|
-
console.error("NetworkPeopleHelper requires BridgeUIProvider.")
|
|
94
|
-
}, [config])
|
|
95
|
-
|
|
96
|
-
useEffect(() => {
|
|
97
|
-
if (typeof window === "undefined") return
|
|
98
|
-
const media = window.matchMedia("(prefers-reduced-motion: reduce)")
|
|
99
|
-
const sync = () => setPrefersReducedMotion(media.matches)
|
|
100
|
-
sync()
|
|
101
|
-
media.addEventListener("change", sync)
|
|
102
|
-
return () => media.removeEventListener("change", sync)
|
|
103
|
-
}, [])
|
|
104
|
-
|
|
105
|
-
useEffect(() => {
|
|
106
|
-
if (typeof window === "undefined") return
|
|
107
|
-
if (window.sessionStorage.getItem(dismissStorageKey(cleaned)) === "1") {
|
|
108
|
-
setPhase("dismissed")
|
|
109
|
-
} else {
|
|
110
|
-
setPhase("idle")
|
|
111
|
-
}
|
|
112
|
-
setListOpen(false)
|
|
113
|
-
setConfirmDismiss(false)
|
|
114
|
-
setListSearch("")
|
|
115
|
-
setDebouncedListSearch("")
|
|
116
|
-
setCheckReady(false)
|
|
117
|
-
}, [cleaned])
|
|
118
|
-
|
|
119
|
-
useEffect(() => {
|
|
120
|
-
if (!enabled || !cleaned || delayBlocked) return
|
|
121
|
-
if (checkDelayMs <= 0) {
|
|
122
|
-
setCheckReady(true)
|
|
123
|
-
return
|
|
124
|
-
}
|
|
125
|
-
const timeoutId = window.setTimeout(() => setCheckReady(true), checkDelayMs)
|
|
126
|
-
return () => window.clearTimeout(timeoutId)
|
|
127
|
-
}, [enabled, cleaned, checkDelayMs, delayBlocked])
|
|
128
|
-
|
|
129
|
-
useEffect(() => {
|
|
130
|
-
const timeoutId = window.setTimeout(() => setDebouncedListSearch(listSearch), SEARCH_DEBOUNCE_MS)
|
|
131
|
-
return () => window.clearTimeout(timeoutId)
|
|
132
|
-
}, [listSearch])
|
|
133
|
-
|
|
134
|
-
useEffect(() => {
|
|
135
|
-
if (!listOpen) {
|
|
136
|
-
setListSearch("")
|
|
137
|
-
setDebouncedListSearch("")
|
|
138
|
-
return
|
|
139
|
-
}
|
|
140
|
-
const timeoutId = window.setTimeout(() => searchInputRef.current?.focus(), 220)
|
|
141
|
-
return () => window.clearTimeout(timeoutId)
|
|
142
|
-
}, [listOpen])
|
|
143
|
-
|
|
144
|
-
useEffect(() => {
|
|
145
|
-
function onOpenRequest(event: Event) {
|
|
146
|
-
const detail = (event as CustomEvent<NetworkPeopleHelperOpenDetail>).detail
|
|
147
|
-
if (detail?.domain !== cleaned) return
|
|
148
|
-
if (typeof window !== "undefined") window.sessionStorage.removeItem(dismissStorageKey(cleaned))
|
|
149
|
-
setCheckReady(true)
|
|
150
|
-
setPhase("expanded")
|
|
151
|
-
setListOpen(true)
|
|
152
|
-
setConfirmDismiss(false)
|
|
153
|
-
}
|
|
154
|
-
window.addEventListener(NETWORK_PEOPLE_HELPER_OPEN_EVENT, onOpenRequest)
|
|
155
|
-
return () => window.removeEventListener(NETWORK_PEOPLE_HELPER_OPEN_EVENT, onOpenRequest)
|
|
156
|
-
}, [cleaned])
|
|
157
|
-
|
|
158
|
-
useEffect(() => {
|
|
159
|
-
if (phase !== "idle" || !checkReady || preview.status === "idle" || preview.status === "loading") return
|
|
160
|
-
if (knowsPeople) {
|
|
161
|
-
setPhase(prefersReducedMotion ? "expanded" : "icon")
|
|
162
|
-
return
|
|
163
|
-
}
|
|
164
|
-
if (preview.status === "ready" || preview.status === "error") setPhase("gone")
|
|
165
|
-
}, [phase, checkReady, preview.status, knowsPeople, prefersReducedMotion])
|
|
166
|
-
|
|
167
|
-
useEffect(() => {
|
|
168
|
-
if (phase !== "icon") return
|
|
169
|
-
const timeoutId = window.setTimeout(() => setPhase("revealing"), ICON_HOLD_MS)
|
|
170
|
-
return () => window.clearTimeout(timeoutId)
|
|
171
|
-
}, [phase])
|
|
172
|
-
|
|
173
|
-
useEffect(() => {
|
|
174
|
-
if (phase !== "revealing") return
|
|
175
|
-
const timeoutId = window.setTimeout(() => setPhase("expanded"), REVEAL_HOLD_MS)
|
|
176
|
-
return () => window.clearTimeout(timeoutId)
|
|
177
|
-
}, [phase])
|
|
178
|
-
|
|
179
|
-
useEffect(() => {
|
|
180
|
-
if (!listOpen || !list.hasNextPage || list.isFetchingNextPage) return
|
|
181
|
-
const node = loadMoreRef.current
|
|
182
|
-
if (!node) return
|
|
183
|
-
const observer = new IntersectionObserver(
|
|
184
|
-
(entries) => {
|
|
185
|
-
if (entries.some((entry) => entry.isIntersecting)) list.fetchNextPage()
|
|
186
|
-
},
|
|
187
|
-
{ root: node.closest(".bridge-network-helper__list-scroll"), rootMargin: "80px", threshold: 0 },
|
|
188
|
-
)
|
|
189
|
-
observer.observe(node)
|
|
190
|
-
return () => observer.disconnect()
|
|
191
|
-
}, [listOpen, list.hasNextPage, list.isFetchingNextPage, list.fetchNextPage, list.people.length])
|
|
192
|
-
|
|
193
|
-
function confirmAndDismiss() {
|
|
194
|
-
if (typeof window !== "undefined") window.sessionStorage.setItem(dismissStorageKey(cleaned), "1")
|
|
195
|
-
setConfirmDismiss(false)
|
|
196
|
-
setListOpen(false)
|
|
197
|
-
setPhase("dismissed")
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function openPeopleList() {
|
|
201
|
-
setPhase("expanded")
|
|
202
|
-
setListOpen(true)
|
|
203
|
-
setConfirmDismiss(false)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function togglePeopleList() {
|
|
207
|
-
if (listOpen) {
|
|
208
|
-
setListOpen(false)
|
|
209
|
-
return
|
|
210
|
-
}
|
|
211
|
-
openPeopleList()
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (!config || phase === "idle" || phase === "dismissed" || phase === "gone") return null
|
|
215
|
-
|
|
216
|
-
const peopleLabel = count === 1 ? "person" : "people"
|
|
217
|
-
const noteMounted = phase === "icon" || phase === "revealing" || phase === "expanded"
|
|
218
|
-
const showDismissBar = phase === "revealing" || phase === "expanded"
|
|
219
|
-
const shellState = phase === "icon" ? "icon" : phase === "revealing" ? "revealing" : "expanded"
|
|
220
|
-
const anchorClass = className
|
|
221
|
-
? `bridge-network-helper-anchor ${className}`
|
|
222
|
-
: "bridge-network-helper-anchor"
|
|
223
|
-
|
|
224
|
-
return (
|
|
225
|
-
<div className={anchorClass} data-placement={placement} style={style}>
|
|
226
|
-
<div
|
|
227
|
-
className={
|
|
228
|
-
listOpen
|
|
229
|
-
? `bridge-network-helper bridge-network-helper--${shellState} bridge-network-helper--list`
|
|
230
|
-
: `bridge-network-helper bridge-network-helper--${shellState}`
|
|
231
|
-
}
|
|
232
|
-
>
|
|
233
|
-
{showDismissBar ? (
|
|
234
|
-
<div className="bridge-network-helper__dismiss-bar">
|
|
235
|
-
{confirmDismiss ? (
|
|
236
|
-
<p className="bridge-network-helper__dismiss-confirm">
|
|
237
|
-
Hide for this visit?{" "}
|
|
238
|
-
<button type="button" className="bridge-network-helper__dismiss-action" onClick={confirmAndDismiss}>
|
|
239
|
-
Confirm
|
|
240
|
-
</button>
|
|
241
|
-
<span aria-hidden="true" className="bridge-network-helper__dismiss-sep">
|
|
242
|
-
·
|
|
243
|
-
</span>
|
|
244
|
-
<button type="button" className="bridge-network-helper__dismiss-cancel" onClick={() => setConfirmDismiss(false)}>
|
|
245
|
-
Cancel
|
|
246
|
-
</button>
|
|
247
|
-
</p>
|
|
248
|
-
) : (
|
|
249
|
-
<button type="button" className="bridge-network-helper__dismiss-link" onClick={() => setConfirmDismiss(true)}>
|
|
250
|
-
<EyeOffIcon />
|
|
251
|
-
Do not show again
|
|
252
|
-
</button>
|
|
253
|
-
)}
|
|
254
|
-
</div>
|
|
255
|
-
) : null}
|
|
256
|
-
|
|
257
|
-
<div className="bridge-network-helper__shell">
|
|
258
|
-
<div className="bridge-network-helper__row">
|
|
259
|
-
<button
|
|
260
|
-
type="button"
|
|
261
|
-
className="bridge-network-helper__icon-btn"
|
|
262
|
-
aria-label={listOpen ? `Collapse people you know at ${label}` : `You know ${count} ${peopleLabel} at ${label}`}
|
|
263
|
-
onClick={togglePeopleList}
|
|
264
|
-
>
|
|
265
|
-
<BridgeHelperIcon motion="nudge" />
|
|
266
|
-
</button>
|
|
267
|
-
|
|
268
|
-
<div className="bridge-network-helper__note-reveal" aria-hidden={phase === "icon"}>
|
|
269
|
-
<div className="bridge-network-helper__note-reveal-inner">
|
|
270
|
-
{noteMounted ? (
|
|
271
|
-
<>
|
|
272
|
-
<div className="bridge-network-helper__content" role="status" aria-live="polite">
|
|
273
|
-
<p className="bridge-network-helper__line">
|
|
274
|
-
You know{" "}
|
|
275
|
-
<button
|
|
276
|
-
type="button"
|
|
277
|
-
className="bridge-network-helper__count"
|
|
278
|
-
onClick={openPeopleList}
|
|
279
|
-
aria-expanded={listOpen}
|
|
280
|
-
aria-label={`View ${count} ${peopleLabel} you know at ${label}`}
|
|
281
|
-
>
|
|
282
|
-
{count}
|
|
283
|
-
</button>{" "}
|
|
284
|
-
{peopleLabel} at {label}
|
|
285
|
-
</p>
|
|
286
|
-
</div>
|
|
287
|
-
|
|
288
|
-
{previewContacts.length > 0 ? (
|
|
289
|
-
<button
|
|
290
|
-
type="button"
|
|
291
|
-
className="bridge-network-helper__avatars"
|
|
292
|
-
onClick={openPeopleList}
|
|
293
|
-
aria-expanded={listOpen}
|
|
294
|
-
aria-label={`View people you know at ${label}`}
|
|
295
|
-
>
|
|
296
|
-
{previewContacts.slice(0, 3).map((contact, index) => (
|
|
297
|
-
<ContactAvatar key={contact.id} contact={contact} style={{ zIndex: index + 1 }} />
|
|
298
|
-
))}
|
|
299
|
-
</button>
|
|
300
|
-
) : null}
|
|
301
|
-
|
|
302
|
-
<button
|
|
303
|
-
type="button"
|
|
304
|
-
className="bridge-network-helper__toggle"
|
|
305
|
-
onClick={togglePeopleList}
|
|
306
|
-
aria-expanded={listOpen}
|
|
307
|
-
aria-label={listOpen ? "Minimize people list" : "Expand people list"}
|
|
308
|
-
>
|
|
309
|
-
{listOpen ? <ChevronDownIcon /> : <ChevronUpIcon />}
|
|
310
|
-
</button>
|
|
311
|
-
</>
|
|
312
|
-
) : null}
|
|
313
|
-
</div>
|
|
314
|
-
</div>
|
|
315
|
-
</div>
|
|
316
|
-
|
|
317
|
-
<div className="bridge-network-helper__list" aria-hidden={!listOpen}>
|
|
318
|
-
<div className="bridge-network-helper__list-inner">
|
|
319
|
-
<div className="bridge-network-helper__list-panel">
|
|
320
|
-
<div className="bridge-network-helper__search">
|
|
321
|
-
<SearchIcon className="bridge-network-helper__search-icon" />
|
|
322
|
-
<input
|
|
323
|
-
ref={searchInputRef}
|
|
324
|
-
type="search"
|
|
325
|
-
value={listSearch}
|
|
326
|
-
onChange={(event) => setListSearch(event.target.value)}
|
|
327
|
-
placeholder="Search people…"
|
|
328
|
-
aria-label={`Search people you know at ${label}`}
|
|
329
|
-
className="bridge-network-helper__search-input"
|
|
330
|
-
autoComplete="off"
|
|
331
|
-
spellCheck={false}
|
|
332
|
-
/>
|
|
333
|
-
</div>
|
|
334
|
-
|
|
335
|
-
<div className="bridge-network-helper__list-scroll">
|
|
336
|
-
{showInitialListSkeleton ? (
|
|
337
|
-
<ul className="bridge-network-helper__people" aria-busy="true">
|
|
338
|
-
{Array.from({ length: 5 }).map((_, index) => (
|
|
339
|
-
<PersonRowSkeleton key={`skeleton-${index}`} />
|
|
340
|
-
))}
|
|
341
|
-
</ul>
|
|
342
|
-
) : list.isError ? (
|
|
343
|
-
<div className="bridge-network-helper__list-status is-error">Failed to load people.</div>
|
|
344
|
-
) : list.people.length === 0 ? (
|
|
345
|
-
<div className="bridge-network-helper__list-status">
|
|
346
|
-
{hasActiveSearch ? "No matches for that search." : "No people found."}
|
|
347
|
-
</div>
|
|
348
|
-
) : (
|
|
349
|
-
<ul className="bridge-network-helper__people">
|
|
350
|
-
{list.people.map((contact, index) => {
|
|
351
|
-
const roleMeta = [contact.position, contact.company].filter(Boolean).join(" · ")
|
|
352
|
-
return (
|
|
353
|
-
<li
|
|
354
|
-
key={contact.id}
|
|
355
|
-
className="bridge-network-helper__person"
|
|
356
|
-
style={{ animationDelay: `${Math.min(index, 12) * 35}ms` }}
|
|
357
|
-
>
|
|
358
|
-
<ContactAvatar contact={contact} size="md" />
|
|
359
|
-
<div className="bridge-network-helper__person-copy">
|
|
360
|
-
<p className="bridge-network-helper__person-name">{contact.name}</p>
|
|
361
|
-
{roleMeta ? <p className="bridge-network-helper__person-meta">{roleMeta}</p> : null}
|
|
362
|
-
{contact.email ? <p className="bridge-network-helper__person-meta">{contact.email}</p> : null}
|
|
363
|
-
{contact.shareLink ? <ShareLinkCopyControl shareLink={contact.shareLink} /> : null}
|
|
364
|
-
</div>
|
|
365
|
-
<PersonMenu contact={contact} customActions={customActions} config={config} />
|
|
366
|
-
</li>
|
|
367
|
-
)
|
|
368
|
-
})}
|
|
369
|
-
{list.isFetchingNextPage
|
|
370
|
-
? Array.from({ length: 3 }).map((_, index) => <PersonRowSkeleton key={`next-skeleton-${index}`} />)
|
|
371
|
-
: null}
|
|
372
|
-
</ul>
|
|
373
|
-
)}
|
|
374
|
-
{listOpen && list.hasNextPage ? <div ref={loadMoreRef} className="bridge-network-helper__load-more" aria-hidden="true" /> : null}
|
|
375
|
-
</div>
|
|
376
|
-
</div>
|
|
377
|
-
</div>
|
|
378
|
-
</div>
|
|
379
|
-
</div>
|
|
380
|
-
</div>
|
|
381
|
-
</div>
|
|
382
|
-
)
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function PersonRowSkeleton() {
|
|
386
|
-
return (
|
|
387
|
-
<li className="bridge-network-helper__person bridge-network-helper__person--skeleton">
|
|
388
|
-
<span className="bridge-network-helper__skeleton bridge-network-helper__skeleton--avatar" />
|
|
389
|
-
<div className="bridge-network-helper__person-copy">
|
|
390
|
-
<span className="bridge-network-helper__skeleton bridge-network-helper__skeleton--line is-name" />
|
|
391
|
-
<span className="bridge-network-helper__skeleton bridge-network-helper__skeleton--line" />
|
|
392
|
-
<span className="bridge-network-helper__skeleton bridge-network-helper__skeleton--line is-long" />
|
|
393
|
-
</div>
|
|
394
|
-
<span className="bridge-network-helper__skeleton bridge-network-helper__skeleton--menu" />
|
|
395
|
-
</li>
|
|
396
|
-
)
|
|
397
|
-
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import { useEffect, useRef, useState } from "react"
|
|
4
|
-
import { ContactAvatar } from "./contact-avatar"
|
|
5
|
-
import { cleanDomain, organizationLabelFromDomain } from "./domain"
|
|
6
|
-
import { BridgeHelperIcon, ChevronUpIcon } from "./icons"
|
|
7
|
-
import { openNetworkPeopleHelper } from "./open-helper"
|
|
8
|
-
import { useBridgeUI } from "../provider"
|
|
9
|
-
import { useNetworkPreview } from "./use-network-preview"
|
|
10
|
-
|
|
11
|
-
const DEFAULT_CHECK_DELAY_MS = 3000
|
|
12
|
-
|
|
13
|
-
export type NetworkPeopleNudgeProps = {
|
|
14
|
-
domain: string
|
|
15
|
-
organizationLabel?: string | null
|
|
16
|
-
enabled?: boolean
|
|
17
|
-
className?: string
|
|
18
|
-
/** Called instead of opening the floating helper on this page. */
|
|
19
|
-
onOpen?: () => void
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function NetworkPeopleNudge({
|
|
23
|
-
domain,
|
|
24
|
-
organizationLabel,
|
|
25
|
-
enabled = true,
|
|
26
|
-
className,
|
|
27
|
-
onOpen,
|
|
28
|
-
}: NetworkPeopleNudgeProps) {
|
|
29
|
-
const config = useBridgeUI()
|
|
30
|
-
const cleaned = cleanDomain(domain)
|
|
31
|
-
const checkReady = useDelayedReady(enabled && Boolean(config) && Boolean(cleaned), DEFAULT_CHECK_DELAY_MS)
|
|
32
|
-
const preview = useNetworkPreview(cleaned, Boolean(config) && enabled && checkReady)
|
|
33
|
-
const warnedRef = useRef(false)
|
|
34
|
-
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
if (config || warnedRef.current) return
|
|
37
|
-
warnedRef.current = true
|
|
38
|
-
console.error("NetworkPeopleNudge requires BridgeUIProvider.")
|
|
39
|
-
}, [config])
|
|
40
|
-
|
|
41
|
-
if (!config || preview.status !== "ready" || preview.count <= 0) return null
|
|
42
|
-
|
|
43
|
-
const peopleLabel = preview.count === 1 ? "person" : "people"
|
|
44
|
-
const label = organizationLabelFromDomain(cleaned, organizationLabel)
|
|
45
|
-
const classNames = className ? `bridge-network-nudge ${className}` : "bridge-network-nudge"
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<button
|
|
49
|
-
type="button"
|
|
50
|
-
className={classNames}
|
|
51
|
-
onClick={() => {
|
|
52
|
-
if (onOpen) {
|
|
53
|
-
onOpen()
|
|
54
|
-
return
|
|
55
|
-
}
|
|
56
|
-
openNetworkPeopleHelper(cleaned)
|
|
57
|
-
}}
|
|
58
|
-
aria-label={`View ${preview.count} ${peopleLabel} you know at ${label}`}
|
|
59
|
-
>
|
|
60
|
-
<span className="bridge-network-nudge__icon" aria-hidden="true">
|
|
61
|
-
<BridgeHelperIcon motion="none" />
|
|
62
|
-
</span>
|
|
63
|
-
<p className="bridge-network-nudge__line">
|
|
64
|
-
You know <span className="bridge-network-nudge__count">{preview.count}</span> {peopleLabel} at {label}
|
|
65
|
-
</p>
|
|
66
|
-
{preview.contacts.length > 0 ? (
|
|
67
|
-
<span className="bridge-network-nudge__avatars" aria-hidden="true">
|
|
68
|
-
{preview.contacts.slice(0, 3).map((contact, index) => (
|
|
69
|
-
<ContactAvatar key={contact.id} contact={contact} style={{ zIndex: index + 1 }} />
|
|
70
|
-
))}
|
|
71
|
-
</span>
|
|
72
|
-
) : null}
|
|
73
|
-
<span className="bridge-network-nudge__toggle" aria-hidden="true">
|
|
74
|
-
<ChevronUpIcon />
|
|
75
|
-
</span>
|
|
76
|
-
</button>
|
|
77
|
-
)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function useDelayedReady(gate: boolean, delayMs: number): boolean {
|
|
81
|
-
const [ready, setReady] = useState(false)
|
|
82
|
-
|
|
83
|
-
useEffect(() => {
|
|
84
|
-
if (!gate) {
|
|
85
|
-
setReady(false)
|
|
86
|
-
return
|
|
87
|
-
}
|
|
88
|
-
if (delayMs <= 0) {
|
|
89
|
-
setReady(true)
|
|
90
|
-
return
|
|
91
|
-
}
|
|
92
|
-
setReady(false)
|
|
93
|
-
const timeoutId = window.setTimeout(() => setReady(true), delayMs)
|
|
94
|
-
return () => window.clearTimeout(timeoutId)
|
|
95
|
-
}, [gate, delayMs])
|
|
96
|
-
|
|
97
|
-
return ready
|
|
98
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { cleanDomain } from "./domain"
|
|
2
|
-
|
|
3
|
-
export const NETWORK_PEOPLE_HELPER_OPEN_EVENT = "bridge-network-helper:open"
|
|
4
|
-
|
|
5
|
-
export type NetworkPeopleHelperOpenDetail = {
|
|
6
|
-
domain: string
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export function openNetworkPeopleHelper(domain: string): void {
|
|
10
|
-
if (typeof window === "undefined") return
|
|
11
|
-
const cleaned = cleanDomain(domain)
|
|
12
|
-
if (!cleaned) return
|
|
13
|
-
window.dispatchEvent(
|
|
14
|
-
new CustomEvent<NetworkPeopleHelperOpenDetail>(NETWORK_PEOPLE_HELPER_OPEN_EVENT, {
|
|
15
|
-
detail: { domain: cleaned },
|
|
16
|
-
}),
|
|
17
|
-
)
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function dismissStorageKey(domain: string): string {
|
|
21
|
-
return `bridge-network-helper-dismissed:${cleanDomain(domain)}`
|
|
22
|
-
}
|