@brdgapp/ui 0.1.2 → 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.
Files changed (123) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.md +37 -201
  3. package/components/action-menu.d.ts +65 -0
  4. package/components/action-menu.js +48 -0
  5. package/components/avatar.d.ts +50 -0
  6. package/components/avatar.js +91 -0
  7. package/components/badge.d.ts +12 -0
  8. package/components/badge.js +23 -0
  9. package/components/bottom-sheet.d.ts +17 -0
  10. package/components/bottom-sheet.js +12 -0
  11. package/components/brand-mark.d.ts +20 -0
  12. package/components/brand-mark.js +44 -0
  13. package/components/button-group.d.ts +25 -0
  14. package/components/button-group.js +53 -0
  15. package/components/button.d.ts +33 -0
  16. package/components/button.js +82 -0
  17. package/components/card.d.ts +78 -0
  18. package/components/card.js +75 -0
  19. package/components/checkbox.d.ts +22 -0
  20. package/components/checkbox.js +35 -0
  21. package/components/collapsible.d.ts +29 -0
  22. package/components/collapsible.js +38 -0
  23. package/components/column-filter-menu.d.ts +15 -0
  24. package/components/column-filter-menu.js +30 -0
  25. package/components/column-sort-button.d.ts +18 -0
  26. package/components/column-sort-button.js +14 -0
  27. package/components/confirm-dialog.d.ts +20 -0
  28. package/components/confirm-dialog.js +17 -0
  29. package/components/copy-field.d.ts +17 -0
  30. package/components/copy-field.js +36 -0
  31. package/components/data-state.d.ts +44 -0
  32. package/components/data-state.js +25 -0
  33. package/components/data-table.d.ts +65 -0
  34. package/components/data-table.js +84 -0
  35. package/components/dialog.d.ts +19 -0
  36. package/components/dialog.js +10 -0
  37. package/components/drawer.d.ts +21 -0
  38. package/components/drawer.js +17 -0
  39. package/components/expandable-bio.d.ts +8 -0
  40. package/components/expandable-bio.js +26 -0
  41. package/components/field.d.ts +29 -0
  42. package/components/field.js +27 -0
  43. package/components/filter-chip.d.ts +11 -0
  44. package/components/filter-chip.js +8 -0
  45. package/components/filter-pill.d.ts +34 -0
  46. package/components/filter-pill.js +35 -0
  47. package/components/hover-card.d.ts +21 -0
  48. package/components/hover-card.js +20 -0
  49. package/components/icon-button.d.ts +16 -0
  50. package/components/icon-button.js +32 -0
  51. package/components/illustration.d.ts +33 -0
  52. package/components/illustration.js +70 -0
  53. package/components/info-hint.d.ts +29 -0
  54. package/components/info-hint.js +42 -0
  55. package/components/input.d.ts +9 -0
  56. package/components/input.js +15 -0
  57. package/components/network-graph.d.ts +48 -0
  58. package/components/network-graph.js +37 -0
  59. package/components/page-banner.d.ts +20 -0
  60. package/components/page-banner.js +29 -0
  61. package/components/scene.d.ts +12 -0
  62. package/components/scene.js +7 -0
  63. package/components/search-input.d.ts +8 -0
  64. package/components/search-input.js +14 -0
  65. package/components/segmented-control.d.ts +18 -0
  66. package/components/segmented-control.js +14 -0
  67. package/components/skeleton.d.ts +12 -0
  68. package/components/skeleton.js +21 -0
  69. package/components/skip-link.d.ts +8 -0
  70. package/components/skip-link.js +7 -0
  71. package/components/stat-box.d.ts +23 -0
  72. package/components/stat-box.js +29 -0
  73. package/components/switch.d.ts +13 -0
  74. package/components/switch.js +8 -0
  75. package/components/tabs.d.ts +6 -0
  76. package/components/tabs.js +25 -0
  77. package/components/tag-input.d.ts +15 -0
  78. package/components/tag-input.js +40 -0
  79. package/components/tag.d.ts +27 -0
  80. package/components/tag.js +35 -0
  81. package/components/textarea.d.ts +12 -0
  82. package/components/textarea.js +78 -0
  83. package/components/theme-switch.d.ts +6 -0
  84. package/components/theme-switch.js +16 -0
  85. package/components/toast.d.ts +25 -0
  86. package/components/toast.js +44 -0
  87. package/hooks/use-is-mobile.d.ts +2 -0
  88. package/hooks/use-is-mobile.js +16 -0
  89. package/hooks/use-mock-query.d.ts +23 -0
  90. package/hooks/use-mock-query.js +36 -0
  91. package/hooks/use-theme.d.ts +11 -0
  92. package/hooks/use-theme.js +34 -0
  93. package/lib/demo-controls.d.ts +24 -0
  94. package/lib/demo-controls.js +24 -0
  95. package/lib/format.d.ts +17 -0
  96. package/lib/format.js +55 -0
  97. package/lib/layout.d.ts +19 -0
  98. package/lib/layout.js +24 -0
  99. package/lib/motion.d.ts +16 -0
  100. package/lib/motion.js +18 -0
  101. package/lib/theme.d.ts +21 -0
  102. package/lib/theme.js +63 -0
  103. package/lib/utils.d.ts +22 -0
  104. package/lib/utils.js +27 -0
  105. package/package.json +32 -16
  106. package/styles/globals.css +595 -0
  107. package/src/index.ts +0 -18
  108. package/src/network-people-helper/README.md +0 -130
  109. package/src/network-people-helper/api.ts +0 -174
  110. package/src/network-people-helper/contact-avatar.tsx +0 -63
  111. package/src/network-people-helper/domain.ts +0 -14
  112. package/src/network-people-helper/icons.tsx +0 -130
  113. package/src/network-people-helper/index.ts +0 -6
  114. package/src/network-people-helper/network-people-helper.tsx +0 -397
  115. package/src/network-people-helper/network-people-nudge.tsx +0 -98
  116. package/src/network-people-helper/open-helper.ts +0 -22
  117. package/src/network-people-helper/person-menu.tsx +0 -211
  118. package/src/network-people-helper/styles.css +0 -848
  119. package/src/network-people-helper/types.ts +0 -23
  120. package/src/network-people-helper/use-network-people-list.ts +0 -122
  121. package/src/network-people-helper/use-network-preview.ts +0 -43
  122. package/src/provider.tsx +0 -59
  123. 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
- }