@seamapi/react 5.0.0 → 5.1.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 +111 -2
- package/dist/ecosystem.js +45 -0
- package/dist/ecosystem.js.map +1 -0
- package/dist/elements.js +12871 -11737
- package/dist/elements.js.map +1 -1
- package/dist/index.css +469 -0
- package/dist/index.css.map +1 -1
- package/dist/index.min.css +1 -1
- package/dist/index.min.css.map +1 -1
- package/lib/seam/SeamProvider.d.ts +10 -3
- package/lib/seam/SeamProvider.js +5 -3
- package/lib/seam/SeamProvider.js.map +1 -1
- package/lib/seam/components/CompatibleApps/CompatibleApps.d.ts +26 -0
- package/lib/seam/components/CompatibleApps/CompatibleApps.js +126 -0
- package/lib/seam/components/CompatibleApps/CompatibleApps.js.map +1 -0
- package/lib/seam/components/SupportedDevices/DeviceModelRow.d.ts +8 -0
- package/lib/seam/components/SupportedDevices/DeviceModelRow.js +31 -0
- package/lib/seam/components/SupportedDevices/DeviceModelRow.js.map +1 -0
- package/lib/seam/components/SupportedDevices/FilterSelect.d.ts +12 -0
- package/lib/seam/components/SupportedDevices/FilterSelect.js +9 -0
- package/lib/seam/components/SupportedDevices/FilterSelect.js.map +1 -0
- package/lib/seam/components/SupportedDevices/ManufacturerSection.d.ts +10 -0
- package/lib/seam/components/SupportedDevices/ManufacturerSection.js +35 -0
- package/lib/seam/components/SupportedDevices/ManufacturerSection.js.map +1 -0
- package/lib/seam/components/SupportedDevices/SupportedDevices.d.ts +24 -0
- package/lib/seam/components/SupportedDevices/SupportedDevices.js +151 -0
- package/lib/seam/components/SupportedDevices/SupportedDevices.js.map +1 -0
- package/lib/seam/components/SupportedDevices/device-models.d.ts +47 -0
- package/lib/seam/components/SupportedDevices/device-models.js +217 -0
- package/lib/seam/components/SupportedDevices/device-models.js.map +1 -0
- package/lib/seam/components/index.d.ts +2 -0
- package/lib/seam/components/index.js +2 -0
- package/lib/seam/components/index.js.map +1 -1
- package/lib/seam/ecosystem/EcosystemProvider.d.ts +37 -0
- package/lib/seam/ecosystem/EcosystemProvider.js +56 -0
- package/lib/seam/ecosystem/EcosystemProvider.js.map +1 -0
- package/lib/seam/ecosystem/client.d.ts +28 -0
- package/lib/seam/ecosystem/client.js +74 -0
- package/lib/seam/ecosystem/client.js.map +1 -0
- package/lib/seam/ecosystem/index.d.ts +9 -0
- package/lib/seam/ecosystem/index.js +8 -0
- package/lib/seam/ecosystem/index.js.map +1 -0
- package/lib/seam/ecosystem/types.d.ts +253 -0
- package/lib/seam/ecosystem/types.js +10 -0
- package/lib/seam/ecosystem/types.js.map +1 -0
- package/lib/seam/ecosystem/use-compatible-partners.d.ts +21 -0
- package/lib/seam/ecosystem/use-compatible-partners.js +36 -0
- package/lib/seam/ecosystem/use-compatible-partners.js.map +1 -0
- package/lib/seam/ecosystem/use-ecosystem-device-models.d.ts +20 -0
- package/lib/seam/ecosystem/use-ecosystem-device-models.js +51 -0
- package/lib/seam/ecosystem/use-ecosystem-device-models.js.map +1 -0
- package/lib/seam/ecosystem/use-ecosystem-manufacturers.d.ts +11 -0
- package/lib/seam/ecosystem/use-ecosystem-manufacturers.js +49 -0
- package/lib/seam/ecosystem/use-ecosystem-manufacturers.js.map +1 -0
- package/lib/seam/ecosystem/use-ecosystem-query-result.d.ts +7 -0
- package/lib/seam/ecosystem/use-ecosystem-query-result.js +7 -0
- package/lib/seam/ecosystem/use-ecosystem-query-result.js.map +1 -0
- package/lib/seam/ecosystem/use-embed-key-resolution.d.ts +20 -0
- package/lib/seam/ecosystem/use-embed-key-resolution.js +51 -0
- package/lib/seam/ecosystem/use-embed-key-resolution.js.map +1 -0
- package/lib/seam/index.d.ts +1 -0
- package/lib/seam/index.js +1 -0
- package/lib/seam/index.js.map +1 -1
- package/lib/seam/seam-components-class-name.d.ts +1 -0
- package/lib/seam/seam-components-class-name.js +2 -0
- package/lib/seam/seam-components-class-name.js.map +1 -0
- package/lib/ui/ecosystem/SearchField.d.ts +9 -0
- package/lib/ui/ecosystem/SearchField.js +8 -0
- package/lib/ui/ecosystem/SearchField.js.map +1 -0
- package/lib/ui/ecosystem/StateMessage.d.ts +8 -0
- package/lib/ui/ecosystem/StateMessage.js +5 -0
- package/lib/ui/ecosystem/StateMessage.js.map +1 -0
- package/lib/ui/ecosystem/errors.d.ts +5 -0
- package/lib/ui/ecosystem/errors.js +25 -0
- package/lib/ui/ecosystem/errors.js.map +1 -0
- package/lib/ui/ecosystem/storybook.d.ts +15 -0
- package/lib/ui/ecosystem/storybook.js +16 -0
- package/lib/ui/ecosystem/storybook.js.map +1 -0
- package/lib/ui/ecosystem/theme.d.ts +6 -0
- package/lib/ui/ecosystem/theme.js +11 -0
- package/lib/ui/ecosystem/theme.js.map +1 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +11 -3
- package/src/ecosystem.d.ts +2 -0
- package/src/ecosystem.ts +12 -0
- package/src/elements.ts +4 -0
- package/src/lib/ecosystem-element.tsx +154 -0
- package/src/lib/element.tsx +6 -0
- package/src/lib/seam/SeamProvider.tsx +18 -2
- package/src/lib/seam/components/CompatibleApps/CompatibleApps.element.ts +19 -0
- package/src/lib/seam/components/CompatibleApps/CompatibleApps.tsx +344 -0
- package/src/lib/seam/components/SupportedDevices/DeviceModelRow.tsx +105 -0
- package/src/lib/seam/components/SupportedDevices/FilterSelect.tsx +43 -0
- package/src/lib/seam/components/SupportedDevices/ManufacturerSection.tsx +114 -0
- package/src/lib/seam/components/SupportedDevices/SupportedDevices.element.ts +17 -0
- package/src/lib/seam/components/SupportedDevices/SupportedDevices.tsx +382 -0
- package/src/lib/seam/components/SupportedDevices/device-models.ts +326 -0
- package/src/lib/seam/components/ecosystem-elements.ts +2 -0
- package/src/lib/seam/components/index.ts +2 -0
- package/src/lib/seam/ecosystem/EcosystemProvider.tsx +133 -0
- package/src/lib/seam/ecosystem/client.ts +116 -0
- package/src/lib/seam/ecosystem/index.ts +19 -0
- package/src/lib/seam/ecosystem/types.ts +334 -0
- package/src/lib/seam/ecosystem/use-compatible-partners.ts +80 -0
- package/src/lib/seam/ecosystem/use-ecosystem-device-models.ts +96 -0
- package/src/lib/seam/ecosystem/use-ecosystem-manufacturers.ts +86 -0
- package/src/lib/seam/ecosystem/use-ecosystem-query-result.ts +19 -0
- package/src/lib/seam/ecosystem/use-embed-key-resolution.ts +80 -0
- package/src/lib/seam/index.ts +1 -0
- package/src/lib/seam/seam-components-class-name.ts +1 -0
- package/src/lib/ui/ecosystem/SearchField.tsx +32 -0
- package/src/lib/ui/ecosystem/StateMessage.tsx +29 -0
- package/src/lib/ui/ecosystem/errors.ts +30 -0
- package/src/lib/ui/ecosystem/storybook.tsx +40 -0
- package/src/lib/ui/ecosystem/theme.ts +14 -0
- package/src/lib/version.ts +1 -1
- package/src/styles/_compatible-apps.scss +102 -0
- package/src/styles/_ecosystem.scss +295 -0
- package/src/styles/_main.scss +6 -0
- package/src/styles/_supported-devices.scss +153 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
import { useDeferredValue, useMemo, useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
CompatiblePartner,
|
|
6
|
+
PartnerAsset,
|
|
7
|
+
PartnerAssetKind,
|
|
8
|
+
} from 'lib/seam/ecosystem/types.js'
|
|
9
|
+
import { useCompatiblePartners } from 'lib/seam/ecosystem/use-compatible-partners.js'
|
|
10
|
+
import { getEcosystemErrorMessage } from 'lib/ui/ecosystem/errors.js'
|
|
11
|
+
import { SearchField } from 'lib/ui/ecosystem/SearchField.js'
|
|
12
|
+
import { StateMessage } from 'lib/ui/ecosystem/StateMessage.js'
|
|
13
|
+
import {
|
|
14
|
+
type EcosystemTheme,
|
|
15
|
+
getEcosystemThemeClassName,
|
|
16
|
+
} from 'lib/ui/ecosystem/theme.js'
|
|
17
|
+
|
|
18
|
+
export interface CompatibleAppsProps {
|
|
19
|
+
/** Device category, e.g., `smartlock`, `thermostat`, `sensor`, `intercom`. */
|
|
20
|
+
deviceType: string
|
|
21
|
+
/** DeviceDB manufacturer slug. Lists the apps that support this brand. */
|
|
22
|
+
manufacturer?: string | undefined
|
|
23
|
+
/** DeviceDB provider slug, i.e., the cloud the devices connect through. */
|
|
24
|
+
provider?: string | undefined
|
|
25
|
+
/** DeviceDB capability, e.g., `can_provision_mobile_keys`. Shows per-app notes. */
|
|
26
|
+
capability?: string | undefined
|
|
27
|
+
/** Industry slug to restrict the apps, e.g., `short-term-rental`. */
|
|
28
|
+
industry?: string | undefined
|
|
29
|
+
/** Apps shown before the show more button. */
|
|
30
|
+
maxVisible?: number | undefined
|
|
31
|
+
hideSearch?: boolean | undefined
|
|
32
|
+
hideTitle?: boolean | undefined
|
|
33
|
+
/** Color theme. `auto` follows the visitor's color scheme. */
|
|
34
|
+
theme?: EcosystemTheme | undefined
|
|
35
|
+
className?: string | undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const defaultMaxVisible = 24
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The live apps (Seam partners) that work with a device category,
|
|
42
|
+
* manufacturer, provider or capability through Seam.
|
|
43
|
+
*/
|
|
44
|
+
export function CompatibleApps({
|
|
45
|
+
deviceType,
|
|
46
|
+
manufacturer = '',
|
|
47
|
+
provider = '',
|
|
48
|
+
capability = '',
|
|
49
|
+
industry = '',
|
|
50
|
+
maxVisible = defaultMaxVisible,
|
|
51
|
+
hideSearch = false,
|
|
52
|
+
hideTitle = false,
|
|
53
|
+
theme = 'light',
|
|
54
|
+
className,
|
|
55
|
+
}: CompatibleAppsProps): JSX.Element {
|
|
56
|
+
const { partners, isPending, isError, error, refetch } =
|
|
57
|
+
useCompatiblePartners({
|
|
58
|
+
deviceType: deviceType ?? '',
|
|
59
|
+
manufacturer,
|
|
60
|
+
provider,
|
|
61
|
+
capability,
|
|
62
|
+
industry,
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const [searchValue, setSearchValue] = useState('')
|
|
66
|
+
const [showAll, setShowAll] = useState(false)
|
|
67
|
+
const query = useDeferredValue(searchValue.toLowerCase().trim())
|
|
68
|
+
|
|
69
|
+
const sortedPartners = useMemo(
|
|
70
|
+
() => [...(partners ?? [])].sort(comparePartners),
|
|
71
|
+
[partners]
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const filteredPartners = useMemo(
|
|
75
|
+
() =>
|
|
76
|
+
query.length === 0
|
|
77
|
+
? sortedPartners
|
|
78
|
+
: sortedPartners.filter((partner) => matchesSearch(partner, query)),
|
|
79
|
+
[sortedPartners, query]
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
const visibleCount = getVisibleCount(maxVisible)
|
|
83
|
+
const visiblePartners = showAll
|
|
84
|
+
? filteredPartners
|
|
85
|
+
: filteredPartners.slice(0, visibleCount)
|
|
86
|
+
const hiddenCount = filteredPartners.length - visiblePartners.length
|
|
87
|
+
const showNotes = capability.length > 0 || provider.length > 0
|
|
88
|
+
const isMissingDeviceType = (deviceType ?? '').trim().length === 0
|
|
89
|
+
|
|
90
|
+
const content = (() => {
|
|
91
|
+
if (isMissingDeviceType) {
|
|
92
|
+
return <StateMessage role='alert'>{t.missingDeviceType}</StateMessage>
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (isError) {
|
|
96
|
+
const { message, canRetry } = getEcosystemErrorMessage(error, t.error)
|
|
97
|
+
return (
|
|
98
|
+
<StateMessage
|
|
99
|
+
role='alert'
|
|
100
|
+
{...(canRetry
|
|
101
|
+
? {
|
|
102
|
+
actionLabel: t.retry,
|
|
103
|
+
onAction: () => {
|
|
104
|
+
void refetch()
|
|
105
|
+
},
|
|
106
|
+
}
|
|
107
|
+
: {})}
|
|
108
|
+
>
|
|
109
|
+
{message}
|
|
110
|
+
</StateMessage>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (isPending) {
|
|
115
|
+
return <LoadingGrid count={Math.min(visibleCount, 8)} />
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (sortedPartners.length === 0) {
|
|
119
|
+
return <StateMessage>{t.noApps}</StateMessage>
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (filteredPartners.length === 0) {
|
|
123
|
+
return (
|
|
124
|
+
<StateMessage
|
|
125
|
+
actionLabel={t.clearSearch}
|
|
126
|
+
onAction={() => {
|
|
127
|
+
setSearchValue('')
|
|
128
|
+
}}
|
|
129
|
+
>
|
|
130
|
+
{t.noSearchResults(searchValue.trim())}
|
|
131
|
+
</StateMessage>
|
|
132
|
+
)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<>
|
|
137
|
+
<ul className='seam-compatible-apps-grid'>
|
|
138
|
+
{visiblePartners.map((partner) => (
|
|
139
|
+
<PartnerCard
|
|
140
|
+
key={partner.partner_id}
|
|
141
|
+
partner={partner}
|
|
142
|
+
showNote={showNotes}
|
|
143
|
+
/>
|
|
144
|
+
))}
|
|
145
|
+
</ul>
|
|
146
|
+
{hiddenCount > 0 && (
|
|
147
|
+
<div className='seam-compatible-apps-footer'>
|
|
148
|
+
<button
|
|
149
|
+
type='button'
|
|
150
|
+
className='seam-ecosystem-button'
|
|
151
|
+
onClick={() => {
|
|
152
|
+
setShowAll(true)
|
|
153
|
+
}}
|
|
154
|
+
>
|
|
155
|
+
{t.showMore(hiddenCount)}
|
|
156
|
+
</button>
|
|
157
|
+
</div>
|
|
158
|
+
)}
|
|
159
|
+
</>
|
|
160
|
+
)
|
|
161
|
+
})()
|
|
162
|
+
|
|
163
|
+
const showHeader = !hideTitle || !hideSearch
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<div
|
|
167
|
+
className={classNames(
|
|
168
|
+
'seam-ecosystem',
|
|
169
|
+
'seam-compatible-apps',
|
|
170
|
+
getEcosystemThemeClassName(theme),
|
|
171
|
+
className
|
|
172
|
+
)}
|
|
173
|
+
>
|
|
174
|
+
{showHeader && (
|
|
175
|
+
<div className='seam-ecosystem-header'>
|
|
176
|
+
{!hideTitle && <h2 className='seam-ecosystem-title'>{t.title}</h2>}
|
|
177
|
+
{!hideSearch && (
|
|
178
|
+
<SearchField
|
|
179
|
+
label={t.search}
|
|
180
|
+
value={searchValue}
|
|
181
|
+
onChange={setSearchValue}
|
|
182
|
+
disabled={isPending || isError || isMissingDeviceType}
|
|
183
|
+
/>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
)}
|
|
187
|
+
{content}
|
|
188
|
+
</div>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function PartnerCard({
|
|
193
|
+
partner,
|
|
194
|
+
showNote,
|
|
195
|
+
}: {
|
|
196
|
+
partner: CompatiblePartner
|
|
197
|
+
showNote: boolean
|
|
198
|
+
}): JSX.Element {
|
|
199
|
+
const logo = getPartnerLogo(partner, lightLogoKinds)
|
|
200
|
+
const darkLogo = getPartnerLogo(partner, darkLogoKinds)
|
|
201
|
+
const hasDarkLogo = darkLogo != null && darkLogo.url !== logo?.url
|
|
202
|
+
const note = showNote ? partner.support.note : null
|
|
203
|
+
const subtitle = partner.industries.map((i) => i.display_name).join(' · ')
|
|
204
|
+
|
|
205
|
+
const body = (
|
|
206
|
+
<>
|
|
207
|
+
<div className='seam-compatible-apps-logo'>
|
|
208
|
+
{logo == null ? (
|
|
209
|
+
<span className='seam-compatible-apps-logo-initial' aria-hidden>
|
|
210
|
+
{partner.display_name.slice(0, 1)}
|
|
211
|
+
</span>
|
|
212
|
+
) : (
|
|
213
|
+
<>
|
|
214
|
+
<img
|
|
215
|
+
loading='lazy'
|
|
216
|
+
src={logo.url}
|
|
217
|
+
alt={t.logoAlt(partner.display_name)}
|
|
218
|
+
width={48}
|
|
219
|
+
height={48}
|
|
220
|
+
className={classNames({
|
|
221
|
+
'seam-ecosystem-light-only': hasDarkLogo,
|
|
222
|
+
})}
|
|
223
|
+
/>
|
|
224
|
+
{hasDarkLogo && (
|
|
225
|
+
<img
|
|
226
|
+
loading='lazy'
|
|
227
|
+
src={darkLogo.url}
|
|
228
|
+
alt=''
|
|
229
|
+
width={48}
|
|
230
|
+
height={48}
|
|
231
|
+
className='seam-ecosystem-dark-only'
|
|
232
|
+
/>
|
|
233
|
+
)}
|
|
234
|
+
</>
|
|
235
|
+
)}
|
|
236
|
+
</div>
|
|
237
|
+
<div className='seam-compatible-apps-text'>
|
|
238
|
+
<span className='seam-compatible-apps-name'>
|
|
239
|
+
{partner.display_name}
|
|
240
|
+
</span>
|
|
241
|
+
{subtitle.length > 0 && (
|
|
242
|
+
<span className='seam-compatible-apps-subtitle'>{subtitle}</span>
|
|
243
|
+
)}
|
|
244
|
+
{note != null && note.length > 0 && (
|
|
245
|
+
<span className='seam-compatible-apps-note'>{note}</span>
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
</>
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
return (
|
|
252
|
+
<li className='seam-compatible-apps-card'>
|
|
253
|
+
{partner.website == null ? (
|
|
254
|
+
<div className='seam-compatible-apps-card-body'>{body}</div>
|
|
255
|
+
) : (
|
|
256
|
+
<a
|
|
257
|
+
href={partner.website}
|
|
258
|
+
target='_blank'
|
|
259
|
+
rel='noopener noreferrer'
|
|
260
|
+
className='seam-compatible-apps-card-body'
|
|
261
|
+
>
|
|
262
|
+
{body}
|
|
263
|
+
</a>
|
|
264
|
+
)}
|
|
265
|
+
</li>
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function LoadingGrid({ count }: { count: number }): JSX.Element {
|
|
270
|
+
return (
|
|
271
|
+
<ul className='seam-compatible-apps-grid' aria-busy='true'>
|
|
272
|
+
{Array.from({ length: count }, (_, index) => (
|
|
273
|
+
<li key={index} className='seam-compatible-apps-card'>
|
|
274
|
+
<div className='seam-compatible-apps-card-body'>
|
|
275
|
+
<div className='seam-ecosystem-skeleton seam-compatible-apps-logo' />
|
|
276
|
+
<div className='seam-compatible-apps-text'>
|
|
277
|
+
<div className='seam-ecosystem-skeleton seam-ecosystem-skeleton-line' />
|
|
278
|
+
<div className='seam-ecosystem-skeleton seam-ecosystem-skeleton-line-short' />
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
</li>
|
|
282
|
+
))}
|
|
283
|
+
</ul>
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const lightLogoKinds: PartnerAssetKind[] = [
|
|
288
|
+
'square_logo',
|
|
289
|
+
'horizontal_logo',
|
|
290
|
+
'square_logo_mono',
|
|
291
|
+
'horizontal_logo_mono',
|
|
292
|
+
]
|
|
293
|
+
|
|
294
|
+
const darkLogoKinds: PartnerAssetKind[] = [
|
|
295
|
+
'square_logo_mono',
|
|
296
|
+
'square_logo',
|
|
297
|
+
'horizontal_logo_mono',
|
|
298
|
+
'horizontal_logo',
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
const getPartnerLogo = (
|
|
302
|
+
partner: CompatiblePartner,
|
|
303
|
+
kinds: PartnerAssetKind[]
|
|
304
|
+
): PartnerAsset | null => {
|
|
305
|
+
for (const kind of kinds) {
|
|
306
|
+
const asset = partner.assets.find((a) => a.kind === kind)
|
|
307
|
+
if (asset != null) return asset
|
|
308
|
+
}
|
|
309
|
+
return null
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const comparePartners = (
|
|
313
|
+
a: CompatiblePartner,
|
|
314
|
+
b: CompatiblePartner
|
|
315
|
+
): number => {
|
|
316
|
+
if (a.is_featured !== b.is_featured) return a.is_featured ? -1 : 1
|
|
317
|
+
return a.display_name.localeCompare(b.display_name)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const matchesSearch = (partner: CompatiblePartner, query: string): boolean =>
|
|
321
|
+
partner.display_name.toLowerCase().includes(query) ||
|
|
322
|
+
partner.industries.some((i) =>
|
|
323
|
+
i.display_name.toLowerCase().includes(query)
|
|
324
|
+
) ||
|
|
325
|
+
partner.solutions.some((s) => s.display_name.toLowerCase().includes(query))
|
|
326
|
+
|
|
327
|
+
const getVisibleCount = (maxVisible: number): number =>
|
|
328
|
+
Number.isFinite(maxVisible) && maxVisible > 0
|
|
329
|
+
? Math.floor(maxVisible)
|
|
330
|
+
: defaultMaxVisible
|
|
331
|
+
|
|
332
|
+
const t = {
|
|
333
|
+
title: 'Works with these apps',
|
|
334
|
+
search: 'Search apps',
|
|
335
|
+
error: 'An error occurred while loading compatible apps.',
|
|
336
|
+
retry: 'Retry',
|
|
337
|
+
noApps: 'No apps are listed for this selection yet.',
|
|
338
|
+
noSearchResults: (query: string) => `No apps found for "${query}".`,
|
|
339
|
+
clearSearch: 'Clear search',
|
|
340
|
+
showMore: (count: number) =>
|
|
341
|
+
`Show ${count} more ${count === 1 ? 'app' : 'apps'}`,
|
|
342
|
+
logoAlt: (name: string) => `${name} logo`,
|
|
343
|
+
missingDeviceType: 'Set a device type to list compatible apps.',
|
|
344
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getCapabilityLabel,
|
|
5
|
+
getDeviceModelImageUrl,
|
|
6
|
+
getDeviceModelName,
|
|
7
|
+
getSupportedCapabilities,
|
|
8
|
+
} from 'lib/seam/components/SupportedDevices/device-models.js'
|
|
9
|
+
import type { DeviceModel, Manufacturer } from 'lib/seam/ecosystem/types.js'
|
|
10
|
+
|
|
11
|
+
interface DeviceModelRowProps {
|
|
12
|
+
deviceModel: DeviceModel
|
|
13
|
+
manufacturer: Manufacturer | null
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function DeviceModelRow({
|
|
17
|
+
deviceModel,
|
|
18
|
+
manufacturer,
|
|
19
|
+
}: DeviceModelRowProps): JSX.Element {
|
|
20
|
+
const name = getDeviceModelName(deviceModel)
|
|
21
|
+
const imageUrl = getDeviceModelImageUrl(deviceModel, manufacturer)
|
|
22
|
+
const modelIds = [
|
|
23
|
+
...new Set(
|
|
24
|
+
deviceModel.variants
|
|
25
|
+
.map((v) => v.manufacturer_model_id)
|
|
26
|
+
.filter((id): id is string => id != null && id.length > 0)
|
|
27
|
+
),
|
|
28
|
+
]
|
|
29
|
+
const providers = deviceModel.providers
|
|
30
|
+
.map((p) => p.display_name ?? p.slug)
|
|
31
|
+
.join(', ')
|
|
32
|
+
const capabilities = getSupportedCapabilities(deviceModel).filter(
|
|
33
|
+
(c) => c.name !== 'device_connectivity'
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const meta = [
|
|
37
|
+
modelIds.length > 0 ? modelIds.slice(0, 2).join(', ') : null,
|
|
38
|
+
providers.length > 0 ? t.via(providers) : null,
|
|
39
|
+
].filter((value): value is string => value != null)
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<li className='seam-supported-devices-row'>
|
|
43
|
+
<div className='seam-supported-devices-image'>
|
|
44
|
+
{imageUrl == null ? (
|
|
45
|
+
<span aria-hidden>{name.slice(0, 1)}</span>
|
|
46
|
+
) : (
|
|
47
|
+
<img loading='lazy' src={imageUrl} alt='' width={48} height={48} />
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
<div className='seam-supported-devices-info'>
|
|
51
|
+
{deviceModel.product_url == null ? (
|
|
52
|
+
<span className='seam-supported-devices-name'>{name}</span>
|
|
53
|
+
) : (
|
|
54
|
+
<a
|
|
55
|
+
className='seam-supported-devices-name'
|
|
56
|
+
href={deviceModel.product_url}
|
|
57
|
+
target='_blank'
|
|
58
|
+
rel='noopener noreferrer'
|
|
59
|
+
>
|
|
60
|
+
{name}
|
|
61
|
+
</a>
|
|
62
|
+
)}
|
|
63
|
+
{meta.length > 0 && (
|
|
64
|
+
<span className='seam-supported-devices-meta'>
|
|
65
|
+
{meta.join(' · ')}
|
|
66
|
+
</span>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
<ul className='seam-supported-devices-badges' aria-label={t.badges}>
|
|
70
|
+
{deviceModel.device_type != null && (
|
|
71
|
+
<li className='seam-ecosystem-badge seam-ecosystem-badge-type'>
|
|
72
|
+
{deviceModel.device_type.display_name}
|
|
73
|
+
</li>
|
|
74
|
+
)}
|
|
75
|
+
{capabilities.map((capability) => (
|
|
76
|
+
<li
|
|
77
|
+
key={capability.name}
|
|
78
|
+
className={classNames('seam-ecosystem-badge', {
|
|
79
|
+
'seam-ecosystem-badge-conditional':
|
|
80
|
+
capability.supported === 'conditionally',
|
|
81
|
+
})}
|
|
82
|
+
title={
|
|
83
|
+
capability.supported === 'conditionally'
|
|
84
|
+
? (capability.details ?? t.conditional)
|
|
85
|
+
: undefined
|
|
86
|
+
}
|
|
87
|
+
>
|
|
88
|
+
{getCapabilityLabel(capability.name)}
|
|
89
|
+
{capability.supported === 'conditionally' && (
|
|
90
|
+
<span className='seam-ecosystem-visually-hidden'>
|
|
91
|
+
{` (${t.conditional})`}
|
|
92
|
+
</span>
|
|
93
|
+
)}
|
|
94
|
+
</li>
|
|
95
|
+
))}
|
|
96
|
+
</ul>
|
|
97
|
+
</li>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const t = {
|
|
102
|
+
via: (providers: string) => `via ${providers}`,
|
|
103
|
+
badges: 'Device type and capabilities',
|
|
104
|
+
conditional: 'conditionally supported',
|
|
105
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ChevronDownIcon } from 'lib/icons/ChevronDown.js'
|
|
2
|
+
import type { FilterOption } from 'lib/seam/components/SupportedDevices/device-models.js'
|
|
3
|
+
|
|
4
|
+
interface FilterSelectProps {
|
|
5
|
+
label: string
|
|
6
|
+
allLabel: string
|
|
7
|
+
options: FilterOption[]
|
|
8
|
+
value: string | null
|
|
9
|
+
onChange: (value: string | null) => void
|
|
10
|
+
disabled?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function FilterSelect({
|
|
14
|
+
label,
|
|
15
|
+
allLabel,
|
|
16
|
+
options,
|
|
17
|
+
value,
|
|
18
|
+
onChange,
|
|
19
|
+
disabled = false,
|
|
20
|
+
}: FilterSelectProps): JSX.Element {
|
|
21
|
+
return (
|
|
22
|
+
<label className='seam-ecosystem-select'>
|
|
23
|
+
<span className='seam-ecosystem-visually-hidden'>{label}</span>
|
|
24
|
+
<select
|
|
25
|
+
className='seam-ecosystem-select-input'
|
|
26
|
+
value={value ?? ''}
|
|
27
|
+
disabled={disabled}
|
|
28
|
+
onChange={(event) => {
|
|
29
|
+
const { value } = event.target
|
|
30
|
+
onChange(value.length === 0 ? null : value)
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
<option value=''>{allLabel}</option>
|
|
34
|
+
{options.map((option) => (
|
|
35
|
+
<option key={option.value} value={option.value}>
|
|
36
|
+
{option.label}
|
|
37
|
+
</option>
|
|
38
|
+
))}
|
|
39
|
+
</select>
|
|
40
|
+
<ChevronDownIcon aria-hidden className='seam-ecosystem-select-icon' />
|
|
41
|
+
</label>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
import { useState } from 'react'
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
getManufacturerLogoUrl,
|
|
6
|
+
getManufacturerName,
|
|
7
|
+
type ManufacturerGroup,
|
|
8
|
+
} from 'lib/seam/components/SupportedDevices/device-models.js'
|
|
9
|
+
import { DeviceModelRow } from 'lib/seam/components/SupportedDevices/DeviceModelRow.js'
|
|
10
|
+
import type { Manufacturer } from 'lib/seam/ecosystem/types.js'
|
|
11
|
+
|
|
12
|
+
/** Device models shown per manufacturer before the show all button. */
|
|
13
|
+
const collapsedCount = 5
|
|
14
|
+
|
|
15
|
+
interface ManufacturerSectionProps {
|
|
16
|
+
group: ManufacturerGroup
|
|
17
|
+
manufacturer: Manufacturer | null
|
|
18
|
+
collapsible: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ManufacturerSection({
|
|
22
|
+
group,
|
|
23
|
+
manufacturer,
|
|
24
|
+
collapsible,
|
|
25
|
+
}: ManufacturerSectionProps): JSX.Element {
|
|
26
|
+
const [expanded, setExpanded] = useState(false)
|
|
27
|
+
|
|
28
|
+
const { deviceModels } = group
|
|
29
|
+
const canExpand = collapsible && deviceModels.length > collapsedCount
|
|
30
|
+
const visibleDeviceModels =
|
|
31
|
+
canExpand && !expanded
|
|
32
|
+
? deviceModels.slice(0, collapsedCount)
|
|
33
|
+
: deviceModels
|
|
34
|
+
|
|
35
|
+
const name =
|
|
36
|
+
manufacturer != null
|
|
37
|
+
? getManufacturerName(manufacturer)
|
|
38
|
+
: group.manufacturer != null
|
|
39
|
+
? getManufacturerName(group.manufacturer)
|
|
40
|
+
: t.other
|
|
41
|
+
|
|
42
|
+
const logoUrl = getManufacturerLogoUrl(manufacturer)
|
|
43
|
+
const darkLogoUrl = getManufacturerLogoUrl(manufacturer, { mono: true })
|
|
44
|
+
const hasDarkLogo = darkLogoUrl != null && darkLogoUrl !== logoUrl
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<section className='seam-supported-devices-section'>
|
|
48
|
+
<div className='seam-supported-devices-section-header'>
|
|
49
|
+
<div className='seam-supported-devices-logo'>
|
|
50
|
+
{logoUrl == null ? (
|
|
51
|
+
<span aria-hidden>{name.slice(0, 1)}</span>
|
|
52
|
+
) : (
|
|
53
|
+
<>
|
|
54
|
+
<img
|
|
55
|
+
loading='lazy'
|
|
56
|
+
src={logoUrl}
|
|
57
|
+
alt=''
|
|
58
|
+
width={40}
|
|
59
|
+
height={40}
|
|
60
|
+
className={classNames({
|
|
61
|
+
'seam-ecosystem-light-only': hasDarkLogo,
|
|
62
|
+
})}
|
|
63
|
+
/>
|
|
64
|
+
{hasDarkLogo && (
|
|
65
|
+
<img
|
|
66
|
+
loading='lazy'
|
|
67
|
+
src={darkLogoUrl}
|
|
68
|
+
alt=''
|
|
69
|
+
width={40}
|
|
70
|
+
height={40}
|
|
71
|
+
className='seam-ecosystem-dark-only'
|
|
72
|
+
/>
|
|
73
|
+
)}
|
|
74
|
+
</>
|
|
75
|
+
)}
|
|
76
|
+
</div>
|
|
77
|
+
<h3 className='seam-supported-devices-section-title'>{name}</h3>
|
|
78
|
+
<span className='seam-supported-devices-count'>
|
|
79
|
+
{t.count(deviceModels.length)}
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
<ul className='seam-supported-devices-rows'>
|
|
83
|
+
{visibleDeviceModels.map((deviceModel) => (
|
|
84
|
+
<DeviceModelRow
|
|
85
|
+
key={deviceModel.device_model_id}
|
|
86
|
+
deviceModel={deviceModel}
|
|
87
|
+
manufacturer={manufacturer}
|
|
88
|
+
/>
|
|
89
|
+
))}
|
|
90
|
+
</ul>
|
|
91
|
+
{canExpand && (
|
|
92
|
+
<div className='seam-supported-devices-section-footer'>
|
|
93
|
+
<button
|
|
94
|
+
type='button'
|
|
95
|
+
className='seam-ecosystem-button'
|
|
96
|
+
aria-expanded={expanded}
|
|
97
|
+
onClick={() => {
|
|
98
|
+
setExpanded((expanded) => !expanded)
|
|
99
|
+
}}
|
|
100
|
+
>
|
|
101
|
+
{expanded ? t.showLess : t.showAll(name, deviceModels.length)}
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
)}
|
|
105
|
+
</section>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const t = {
|
|
110
|
+
other: 'Other devices',
|
|
111
|
+
count: (count: number) => `${count} ${count === 1 ? 'device' : 'devices'}`,
|
|
112
|
+
showAll: (name: string, count: number) => `Show all ${count} ${name} devices`,
|
|
113
|
+
showLess: 'Show less',
|
|
114
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { EcosystemElementProps } from 'lib/ecosystem-element.js'
|
|
2
|
+
|
|
3
|
+
import type { SupportedDevicesProps } from './SupportedDevices.js'
|
|
4
|
+
|
|
5
|
+
export const name = 'seam-supported-devices'
|
|
6
|
+
|
|
7
|
+
export const props: EcosystemElementProps<SupportedDevicesProps> = {
|
|
8
|
+
manufacturer: 'string',
|
|
9
|
+
deviceType: 'string',
|
|
10
|
+
capability: 'string',
|
|
11
|
+
hideSearch: 'boolean',
|
|
12
|
+
hideFilters: 'boolean',
|
|
13
|
+
hideTitle: 'boolean',
|
|
14
|
+
theme: 'string',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { SupportedDevices as Component } from './SupportedDevices.js'
|