@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.
Files changed (121) hide show
  1. package/README.md +111 -2
  2. package/dist/ecosystem.js +45 -0
  3. package/dist/ecosystem.js.map +1 -0
  4. package/dist/elements.js +12871 -11737
  5. package/dist/elements.js.map +1 -1
  6. package/dist/index.css +469 -0
  7. package/dist/index.css.map +1 -1
  8. package/dist/index.min.css +1 -1
  9. package/dist/index.min.css.map +1 -1
  10. package/lib/seam/SeamProvider.d.ts +10 -3
  11. package/lib/seam/SeamProvider.js +5 -3
  12. package/lib/seam/SeamProvider.js.map +1 -1
  13. package/lib/seam/components/CompatibleApps/CompatibleApps.d.ts +26 -0
  14. package/lib/seam/components/CompatibleApps/CompatibleApps.js +126 -0
  15. package/lib/seam/components/CompatibleApps/CompatibleApps.js.map +1 -0
  16. package/lib/seam/components/SupportedDevices/DeviceModelRow.d.ts +8 -0
  17. package/lib/seam/components/SupportedDevices/DeviceModelRow.js +31 -0
  18. package/lib/seam/components/SupportedDevices/DeviceModelRow.js.map +1 -0
  19. package/lib/seam/components/SupportedDevices/FilterSelect.d.ts +12 -0
  20. package/lib/seam/components/SupportedDevices/FilterSelect.js +9 -0
  21. package/lib/seam/components/SupportedDevices/FilterSelect.js.map +1 -0
  22. package/lib/seam/components/SupportedDevices/ManufacturerSection.d.ts +10 -0
  23. package/lib/seam/components/SupportedDevices/ManufacturerSection.js +35 -0
  24. package/lib/seam/components/SupportedDevices/ManufacturerSection.js.map +1 -0
  25. package/lib/seam/components/SupportedDevices/SupportedDevices.d.ts +24 -0
  26. package/lib/seam/components/SupportedDevices/SupportedDevices.js +151 -0
  27. package/lib/seam/components/SupportedDevices/SupportedDevices.js.map +1 -0
  28. package/lib/seam/components/SupportedDevices/device-models.d.ts +47 -0
  29. package/lib/seam/components/SupportedDevices/device-models.js +217 -0
  30. package/lib/seam/components/SupportedDevices/device-models.js.map +1 -0
  31. package/lib/seam/components/index.d.ts +2 -0
  32. package/lib/seam/components/index.js +2 -0
  33. package/lib/seam/components/index.js.map +1 -1
  34. package/lib/seam/ecosystem/EcosystemProvider.d.ts +37 -0
  35. package/lib/seam/ecosystem/EcosystemProvider.js +56 -0
  36. package/lib/seam/ecosystem/EcosystemProvider.js.map +1 -0
  37. package/lib/seam/ecosystem/client.d.ts +28 -0
  38. package/lib/seam/ecosystem/client.js +74 -0
  39. package/lib/seam/ecosystem/client.js.map +1 -0
  40. package/lib/seam/ecosystem/index.d.ts +9 -0
  41. package/lib/seam/ecosystem/index.js +8 -0
  42. package/lib/seam/ecosystem/index.js.map +1 -0
  43. package/lib/seam/ecosystem/types.d.ts +253 -0
  44. package/lib/seam/ecosystem/types.js +10 -0
  45. package/lib/seam/ecosystem/types.js.map +1 -0
  46. package/lib/seam/ecosystem/use-compatible-partners.d.ts +21 -0
  47. package/lib/seam/ecosystem/use-compatible-partners.js +36 -0
  48. package/lib/seam/ecosystem/use-compatible-partners.js.map +1 -0
  49. package/lib/seam/ecosystem/use-ecosystem-device-models.d.ts +20 -0
  50. package/lib/seam/ecosystem/use-ecosystem-device-models.js +51 -0
  51. package/lib/seam/ecosystem/use-ecosystem-device-models.js.map +1 -0
  52. package/lib/seam/ecosystem/use-ecosystem-manufacturers.d.ts +11 -0
  53. package/lib/seam/ecosystem/use-ecosystem-manufacturers.js +49 -0
  54. package/lib/seam/ecosystem/use-ecosystem-manufacturers.js.map +1 -0
  55. package/lib/seam/ecosystem/use-ecosystem-query-result.d.ts +7 -0
  56. package/lib/seam/ecosystem/use-ecosystem-query-result.js +7 -0
  57. package/lib/seam/ecosystem/use-ecosystem-query-result.js.map +1 -0
  58. package/lib/seam/ecosystem/use-embed-key-resolution.d.ts +20 -0
  59. package/lib/seam/ecosystem/use-embed-key-resolution.js +51 -0
  60. package/lib/seam/ecosystem/use-embed-key-resolution.js.map +1 -0
  61. package/lib/seam/index.d.ts +1 -0
  62. package/lib/seam/index.js +1 -0
  63. package/lib/seam/index.js.map +1 -1
  64. package/lib/seam/seam-components-class-name.d.ts +1 -0
  65. package/lib/seam/seam-components-class-name.js +2 -0
  66. package/lib/seam/seam-components-class-name.js.map +1 -0
  67. package/lib/ui/ecosystem/SearchField.d.ts +9 -0
  68. package/lib/ui/ecosystem/SearchField.js +8 -0
  69. package/lib/ui/ecosystem/SearchField.js.map +1 -0
  70. package/lib/ui/ecosystem/StateMessage.d.ts +8 -0
  71. package/lib/ui/ecosystem/StateMessage.js +5 -0
  72. package/lib/ui/ecosystem/StateMessage.js.map +1 -0
  73. package/lib/ui/ecosystem/errors.d.ts +5 -0
  74. package/lib/ui/ecosystem/errors.js +25 -0
  75. package/lib/ui/ecosystem/errors.js.map +1 -0
  76. package/lib/ui/ecosystem/storybook.d.ts +15 -0
  77. package/lib/ui/ecosystem/storybook.js +16 -0
  78. package/lib/ui/ecosystem/storybook.js.map +1 -0
  79. package/lib/ui/ecosystem/theme.d.ts +6 -0
  80. package/lib/ui/ecosystem/theme.js +11 -0
  81. package/lib/ui/ecosystem/theme.js.map +1 -0
  82. package/lib/version.d.ts +1 -1
  83. package/lib/version.js +1 -1
  84. package/package.json +11 -3
  85. package/src/ecosystem.d.ts +2 -0
  86. package/src/ecosystem.ts +12 -0
  87. package/src/elements.ts +4 -0
  88. package/src/lib/ecosystem-element.tsx +154 -0
  89. package/src/lib/element.tsx +6 -0
  90. package/src/lib/seam/SeamProvider.tsx +18 -2
  91. package/src/lib/seam/components/CompatibleApps/CompatibleApps.element.ts +19 -0
  92. package/src/lib/seam/components/CompatibleApps/CompatibleApps.tsx +344 -0
  93. package/src/lib/seam/components/SupportedDevices/DeviceModelRow.tsx +105 -0
  94. package/src/lib/seam/components/SupportedDevices/FilterSelect.tsx +43 -0
  95. package/src/lib/seam/components/SupportedDevices/ManufacturerSection.tsx +114 -0
  96. package/src/lib/seam/components/SupportedDevices/SupportedDevices.element.ts +17 -0
  97. package/src/lib/seam/components/SupportedDevices/SupportedDevices.tsx +382 -0
  98. package/src/lib/seam/components/SupportedDevices/device-models.ts +326 -0
  99. package/src/lib/seam/components/ecosystem-elements.ts +2 -0
  100. package/src/lib/seam/components/index.ts +2 -0
  101. package/src/lib/seam/ecosystem/EcosystemProvider.tsx +133 -0
  102. package/src/lib/seam/ecosystem/client.ts +116 -0
  103. package/src/lib/seam/ecosystem/index.ts +19 -0
  104. package/src/lib/seam/ecosystem/types.ts +334 -0
  105. package/src/lib/seam/ecosystem/use-compatible-partners.ts +80 -0
  106. package/src/lib/seam/ecosystem/use-ecosystem-device-models.ts +96 -0
  107. package/src/lib/seam/ecosystem/use-ecosystem-manufacturers.ts +86 -0
  108. package/src/lib/seam/ecosystem/use-ecosystem-query-result.ts +19 -0
  109. package/src/lib/seam/ecosystem/use-embed-key-resolution.ts +80 -0
  110. package/src/lib/seam/index.ts +1 -0
  111. package/src/lib/seam/seam-components-class-name.ts +1 -0
  112. package/src/lib/ui/ecosystem/SearchField.tsx +32 -0
  113. package/src/lib/ui/ecosystem/StateMessage.tsx +29 -0
  114. package/src/lib/ui/ecosystem/errors.ts +30 -0
  115. package/src/lib/ui/ecosystem/storybook.tsx +40 -0
  116. package/src/lib/ui/ecosystem/theme.ts +14 -0
  117. package/src/lib/version.ts +1 -1
  118. package/src/styles/_compatible-apps.scss +102 -0
  119. package/src/styles/_ecosystem.scss +295 -0
  120. package/src/styles/_main.scss +6 -0
  121. package/src/styles/_supported-devices.scss +153 -0
@@ -0,0 +1,96 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+
3
+ import {
4
+ type EcosystemApiError,
5
+ fetchEcosystem,
6
+ } from 'lib/seam/ecosystem/client.js'
7
+ import { useEcosystemContext } from 'lib/seam/ecosystem/EcosystemProvider.js'
8
+ import type {
9
+ DeviceModel,
10
+ DeviceModelsListResponse,
11
+ } from 'lib/seam/ecosystem/types.js'
12
+ import {
13
+ retryEcosystemQuery,
14
+ type UseEcosystemQueryResult,
15
+ } from 'lib/seam/ecosystem/use-ecosystem-query-result.js'
16
+
17
+ export interface UseEcosystemDeviceModelsParams {
18
+ /** DeviceDB manufacturer slug, e.g., `schlage`. */
19
+ manufacturer?: string | undefined
20
+ /** DeviceDB device type slug, e.g., `smartlock`. */
21
+ deviceType?: string | undefined
22
+ /** DeviceDB provider slug, e.g., `salto-ks`. */
23
+ provider?: string | undefined
24
+ /** Only device models with a computed capability of this name. */
25
+ capability?: string | undefined
26
+ /** Set to false to wait before fetching. */
27
+ enabled?: boolean | undefined
28
+ }
29
+
30
+ export type UseEcosystemDeviceModelsData = DeviceModel[]
31
+
32
+ const pageSize = 500
33
+ const maxPages = 20
34
+
35
+ /**
36
+ * Published device models from Ecosystem `GET /v1/device_models/list`,
37
+ * following pagination to return every matching model.
38
+ */
39
+ export function useEcosystemDeviceModels({
40
+ manufacturer = '',
41
+ deviceType = '',
42
+ provider = '',
43
+ capability = '',
44
+ enabled = true,
45
+ }: UseEcosystemDeviceModelsParams = {}): UseEcosystemQueryResult<
46
+ 'deviceModels',
47
+ UseEcosystemDeviceModelsData
48
+ > {
49
+ const { embedKey, endpoint } = useEcosystemContext()
50
+
51
+ const params = {
52
+ manufacturer: manufacturer.trim(),
53
+ device_type: deviceType.trim(),
54
+ provider: provider.trim(),
55
+ capability: capability.trim(),
56
+ }
57
+
58
+ const { data, ...rest } = useQuery<
59
+ UseEcosystemDeviceModelsData,
60
+ EcosystemApiError
61
+ >({
62
+ enabled,
63
+ queryKey: [
64
+ 'ecosystem',
65
+ endpoint,
66
+ embedKey ?? null,
67
+ 'device_models',
68
+ 'list',
69
+ params,
70
+ ],
71
+ queryFn: async ({ signal }) => {
72
+ const deviceModels: DeviceModel[] = []
73
+ let pageCursor: string | undefined
74
+ for (let page = 0; page < maxPages; page++) {
75
+ const res = await fetchEcosystem<DeviceModelsListResponse>(
76
+ 'device_models/list',
77
+ {
78
+ endpoint,
79
+ embedKey,
80
+ params: { ...params, limit: pageSize, page_cursor: pageCursor },
81
+ signal,
82
+ }
83
+ )
84
+ deviceModels.push(...res.device_models)
85
+ const { has_next_page: hasNextPage, next_page_cursor: nextPageCursor } =
86
+ res.pagination
87
+ if (!hasNextPage || nextPageCursor == null) break
88
+ pageCursor = nextPageCursor
89
+ }
90
+ return deviceModels
91
+ },
92
+ retry: retryEcosystemQuery,
93
+ })
94
+
95
+ return { ...rest, deviceModels: data }
96
+ }
@@ -0,0 +1,86 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+
3
+ import {
4
+ type EcosystemApiError,
5
+ fetchEcosystem,
6
+ } from 'lib/seam/ecosystem/client.js'
7
+ import { useEcosystemContext } from 'lib/seam/ecosystem/EcosystemProvider.js'
8
+ import type {
9
+ Manufacturer,
10
+ ManufacturersListResponse,
11
+ } from 'lib/seam/ecosystem/types.js'
12
+ import {
13
+ retryEcosystemQuery,
14
+ type UseEcosystemQueryResult,
15
+ } from 'lib/seam/ecosystem/use-ecosystem-query-result.js'
16
+
17
+ export interface UseEcosystemManufacturersParams {
18
+ /** Only manufacturers with these slugs. */
19
+ slugs?: string[] | undefined
20
+ /** Set to false to wait before fetching. */
21
+ enabled?: boolean | undefined
22
+ }
23
+
24
+ export type UseEcosystemManufacturersData = Manufacturer[]
25
+
26
+ const pageSize = 500
27
+ const maxPages = 10
28
+
29
+ /** Published manufacturers from Ecosystem `GET /v1/manufacturers/list`. */
30
+ export function useEcosystemManufacturers({
31
+ slugs,
32
+ enabled = true,
33
+ }: UseEcosystemManufacturersParams = {}): UseEcosystemQueryResult<
34
+ 'manufacturers',
35
+ UseEcosystemManufacturersData
36
+ > {
37
+ const { embedKey, endpoint } = useEcosystemContext()
38
+
39
+ const sortedSlugs =
40
+ slugs == null
41
+ ? null
42
+ : [...new Set(slugs)].sort((a, b) => a.localeCompare(b))
43
+
44
+ const { data, ...rest } = useQuery<
45
+ UseEcosystemManufacturersData,
46
+ EcosystemApiError
47
+ >({
48
+ enabled: enabled && (sortedSlugs == null || sortedSlugs.length > 0),
49
+ queryKey: [
50
+ 'ecosystem',
51
+ endpoint,
52
+ embedKey ?? null,
53
+ 'manufacturers',
54
+ 'list',
55
+ { slugs: sortedSlugs },
56
+ ],
57
+ queryFn: async ({ signal }) => {
58
+ const manufacturers: Manufacturer[] = []
59
+ let pageCursor: string | undefined
60
+ for (let page = 0; page < maxPages; page++) {
61
+ const res = await fetchEcosystem<ManufacturersListResponse>(
62
+ 'manufacturers/list',
63
+ {
64
+ endpoint,
65
+ embedKey,
66
+ params: {
67
+ slugs: sortedSlugs ?? undefined,
68
+ limit: pageSize,
69
+ page_cursor: pageCursor,
70
+ },
71
+ signal,
72
+ }
73
+ )
74
+ manufacturers.push(...res.manufacturers)
75
+ const { has_next_page: hasNextPage, next_page_cursor: nextPageCursor } =
76
+ res.pagination
77
+ if (!hasNextPage || nextPageCursor == null) break
78
+ pageCursor = nextPageCursor
79
+ }
80
+ return manufacturers
81
+ },
82
+ retry: retryEcosystemQuery,
83
+ })
84
+
85
+ return { ...rest, manufacturers: data }
86
+ }
@@ -0,0 +1,19 @@
1
+ import type { UseQueryResult } from '@tanstack/react-query'
2
+
3
+ import type { EcosystemApiError } from 'lib/seam/ecosystem/client.js'
4
+
5
+ export type UseEcosystemQueryResult<Field extends string, Data> = Omit<
6
+ UseQueryResult<Data, EcosystemApiError>,
7
+ 'data'
8
+ > & {
9
+ [key in Field]?: Data
10
+ }
11
+
12
+ /** Retry server and network errors, but not client errors such as an invalid embed key. */
13
+ export const retryEcosystemQuery = (
14
+ failureCount: number,
15
+ error: EcosystemApiError
16
+ ): boolean => {
17
+ if (error.status >= 400 && error.status < 500) return false
18
+ return failureCount < 2
19
+ }
@@ -0,0 +1,80 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+
3
+ import {
4
+ type EcosystemApiError,
5
+ fetchEcosystem,
6
+ } from 'lib/seam/ecosystem/client.js'
7
+ import { useEcosystemContext } from 'lib/seam/ecosystem/EcosystemProvider.js'
8
+ import type { EmbedResolution } from 'lib/seam/ecosystem/types.js'
9
+ import {
10
+ retryEcosystemQuery,
11
+ type UseEcosystemQueryResult,
12
+ } from 'lib/seam/ecosystem/use-ecosystem-query-result.js'
13
+
14
+ export interface UseEmbedKeyResolutionParams {
15
+ /** Resolve the partner's capability exclusions for this capability. */
16
+ capability?: string | undefined
17
+ }
18
+
19
+ export type UseEmbedKeyResolutionData = EmbedResolution
20
+
21
+ /**
22
+ * Validates the provider's embed key against the page origin with
23
+ * Ecosystem `GET /v1/embed/resolve` and returns the key's public record
24
+ * and, for partner keys, what the partner's widgets must not show.
25
+ * Disabled when there is no embed key.
26
+ */
27
+ export function useEmbedKeyResolution({
28
+ capability = '',
29
+ }: UseEmbedKeyResolutionParams = {}): UseEcosystemQueryResult<
30
+ 'embedResolution',
31
+ UseEmbedKeyResolutionData
32
+ > {
33
+ const { embedKey, endpoint } = useEcosystemContext()
34
+
35
+ const params = { capability: capability.trim() }
36
+
37
+ const { data, ...rest } = useQuery<
38
+ UseEmbedKeyResolutionData,
39
+ EcosystemApiError
40
+ >({
41
+ enabled: embedKey != null,
42
+ queryKey: [
43
+ 'ecosystem',
44
+ endpoint,
45
+ embedKey ?? null,
46
+ 'embed',
47
+ 'resolve',
48
+ params,
49
+ ],
50
+ queryFn: async ({ signal }) =>
51
+ await fetchEcosystem<EmbedResolution>('embed/resolve', {
52
+ endpoint,
53
+ embedKey,
54
+ params,
55
+ signal,
56
+ }),
57
+ retry: retryEcosystemQuery,
58
+ })
59
+
60
+ return { ...rest, embedResolution: data }
61
+ }
62
+
63
+ /**
64
+ * The manufacturer an embed key is scoped to, if any.
65
+ * Prefers the `manufacturer_context` Ecosystem returns for manufacturer keys (DEVDB-439),
66
+ * and falls back to the key's own `manufacturer_slug`.
67
+ */
68
+ export const getEmbedKeyManufacturer = (
69
+ resolution: EmbedResolution | null | undefined
70
+ ): string | null => {
71
+ if (resolution == null) return null
72
+ const contextSlug = resolution.manufacturer_context?.manufacturer_slug
73
+ if (contextSlug != null && contextSlug.length > 0) return contextSlug
74
+ const { owner_kind: ownerKind, manufacturer_slug: slug } =
75
+ resolution.embed_key
76
+ if (ownerKind === 'manufacturer' && slug != null && slug.length > 0) {
77
+ return slug
78
+ }
79
+ return null
80
+ }
@@ -10,5 +10,6 @@ export * from './connected-accounts/use-connected-account.js'
10
10
  export * from './devices/use-device.js'
11
11
  export * from './devices/use-device-providers.js'
12
12
  export * from './devices/use-devices.js'
13
+ export * from './ecosystem/index.js'
13
14
  export * from './SeamProvider.js'
14
15
  export * from './use-seam-query-result.js'
@@ -0,0 +1 @@
1
+ export const seamComponentsClassName = 'seam-components'
@@ -0,0 +1,32 @@
1
+ import { SearchIcon } from 'lib/icons/Search.js'
2
+
3
+ interface SearchFieldProps {
4
+ value: string
5
+ onChange: (value: string) => void
6
+ label: string
7
+ disabled?: boolean
8
+ }
9
+
10
+ export function SearchField({
11
+ value,
12
+ onChange,
13
+ label,
14
+ disabled = false,
15
+ }: SearchFieldProps): JSX.Element {
16
+ return (
17
+ <label className='seam-ecosystem-search'>
18
+ <SearchIcon aria-hidden className='seam-ecosystem-search-icon' />
19
+ <input
20
+ type='search'
21
+ className='seam-ecosystem-search-input'
22
+ placeholder={label}
23
+ aria-label={label}
24
+ value={value}
25
+ disabled={disabled}
26
+ onChange={(event) => {
27
+ onChange(event.target.value)
28
+ }}
29
+ />
30
+ </label>
31
+ )
32
+ }
@@ -0,0 +1,29 @@
1
+ import type { PropsWithChildren } from 'react'
2
+
3
+ interface StateMessageProps extends PropsWithChildren {
4
+ actionLabel?: string
5
+ onAction?: () => void
6
+ role?: 'status' | 'alert'
7
+ }
8
+
9
+ export function StateMessage({
10
+ children,
11
+ actionLabel,
12
+ onAction,
13
+ role = 'status',
14
+ }: StateMessageProps): JSX.Element {
15
+ return (
16
+ <div className='seam-ecosystem-message' role={role}>
17
+ <p>{children}</p>
18
+ {actionLabel != null && onAction != null && (
19
+ <button
20
+ type='button'
21
+ className='seam-ecosystem-button'
22
+ onClick={onAction}
23
+ >
24
+ {actionLabel}
25
+ </button>
26
+ )}
27
+ </div>
28
+ )
29
+ }
@@ -0,0 +1,30 @@
1
+ import { EcosystemApiError } from 'lib/seam/ecosystem/client.js'
2
+
3
+ /** A message for an Ecosystem error, and whether retrying could help. */
4
+ export const getEcosystemErrorMessage = (
5
+ error: unknown,
6
+ fallback: string
7
+ ): { message: string; canRetry: boolean } => {
8
+ if (error instanceof EcosystemApiError) {
9
+ if (error.type === 'embed_key_required') {
10
+ return { message: t.embedKeyRequired, canRetry: false }
11
+ }
12
+ if (error.type === 'origin_not_allowed') {
13
+ return { message: t.originNotAllowed, canRetry: false }
14
+ }
15
+ if (error.status === 401) {
16
+ return { message: t.invalidEmbedKey, canRetry: false }
17
+ }
18
+ if (error.status === 403) {
19
+ return { message: t.originNotAllowed, canRetry: false }
20
+ }
21
+ }
22
+ return { message: fallback, canRetry: true }
23
+ }
24
+
25
+ const t = {
26
+ embedKeyRequired:
27
+ 'An embed key is required to show this list on this website.',
28
+ invalidEmbedKey: 'The embed key is invalid or has been revoked.',
29
+ originNotAllowed: 'The embed key is not allowed on this website.',
30
+ }
@@ -0,0 +1,40 @@
1
+ import type { Decorator } from '@storybook/react'
2
+
3
+ import { EcosystemProvider } from 'lib/seam/ecosystem/EcosystemProvider.js'
4
+
5
+ // Embed keys accepted by the fake Ecosystem API served at /api/ecosystem
6
+ // (.storybook/fake-ecosystem.js). The Storybook never uses a real key or production data.
7
+ export const storybookEmbedKeys = {
8
+ seam: 'eco_live_storybook',
9
+ partner: 'eco_live_storybook_partner',
10
+ manufacturer: 'eco_live_storybook_schlage',
11
+ wrongOrigin: 'eco_live_storybook_wrong_origin',
12
+ invalid: 'eco_live_invalid',
13
+ } as const
14
+
15
+ export const storybookEcosystemEndpoint = '/api/ecosystem/v1/'
16
+
17
+ export interface EcosystemStoryArgs {
18
+ embedKey?: string | undefined
19
+ }
20
+
21
+ /** Wraps a story in an EcosystemProvider pointed at the fake Ecosystem API. */
22
+ export const withFakeEcosystem: Decorator<EcosystemStoryArgs> = (
23
+ Story,
24
+ context
25
+ ) => (
26
+ <EcosystemProvider
27
+ embedKey={context.args.embedKey}
28
+ ecosystemEndpoint={storybookEcosystemEndpoint}
29
+ disableCssInjection
30
+ disableFontInjection
31
+ >
32
+ <Story />
33
+ </EcosystemProvider>
34
+ )
35
+
36
+ export const withDarkBackground: Decorator = (Story) => (
37
+ <div style={{ background: '#0b0d10', padding: 24, borderRadius: 12 }}>
38
+ <Story />
39
+ </div>
40
+ )
@@ -0,0 +1,14 @@
1
+ export type EcosystemTheme = 'light' | 'dark' | 'auto'
2
+
3
+ /**
4
+ * The class name for an Ecosystem component theme.
5
+ * `auto` follows the visitor's `prefers-color-scheme`.
6
+ */
7
+ export const getEcosystemThemeClassName = (
8
+ theme: string | undefined
9
+ ): string => {
10
+ if (theme === 'dark' || theme === 'auto') {
11
+ return `seam-ecosystem-theme-${theme}`
12
+ }
13
+ return 'seam-ecosystem-theme-light'
14
+ }
@@ -1,3 +1,3 @@
1
- const seamapiReactVersion = '5.0.0'
1
+ const seamapiReactVersion = '5.1.0'
2
2
 
3
3
  export default seamapiReactVersion
@@ -0,0 +1,102 @@
1
+ @mixin all {
2
+ .seam-compatible-apps {
3
+ .seam-compatible-apps-grid {
4
+ display: grid;
5
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
6
+ gap: 12px;
7
+ margin: 0;
8
+ padding: 0;
9
+ list-style: none;
10
+ }
11
+
12
+ .seam-compatible-apps-card {
13
+ border: 1px solid var(--seam-ecosystem-line-1);
14
+ border-radius: 12px;
15
+ background: var(--seam-ecosystem-surface-1);
16
+ transition: border-color 0.15s ease;
17
+
18
+ &:hover,
19
+ &:focus-within {
20
+ border-color: var(--seam-ecosystem-line-2);
21
+ }
22
+ }
23
+
24
+ .seam-compatible-apps-card-body {
25
+ display: flex;
26
+ flex-direction: row;
27
+ align-items: center;
28
+ gap: 12px;
29
+ height: 100%;
30
+ padding: 12px;
31
+ border-radius: 12px;
32
+
33
+ &:focus-visible {
34
+ outline: 2px solid var(--seam-ecosystem-accent);
35
+ outline-offset: 2px;
36
+ }
37
+ }
38
+
39
+ .seam-compatible-apps-logo {
40
+ display: flex;
41
+ flex-shrink: 0;
42
+ align-items: center;
43
+ justify-content: center;
44
+ width: 48px;
45
+ height: 48px;
46
+ overflow: hidden;
47
+ border: 1px solid var(--seam-ecosystem-line-1);
48
+ border-radius: 8px;
49
+ background: var(--seam-ecosystem-surface-2);
50
+
51
+ img {
52
+ width: 100%;
53
+ height: 100%;
54
+ padding: 4px;
55
+ object-fit: contain;
56
+ object-position: center;
57
+ }
58
+ }
59
+
60
+ .seam-compatible-apps-logo-initial {
61
+ color: var(--seam-ecosystem-fg-2);
62
+ font-size: 18px;
63
+ font-weight: 600;
64
+ }
65
+
66
+ .seam-compatible-apps-text {
67
+ display: flex;
68
+ flex-direction: column;
69
+ gap: 2px;
70
+ min-width: 0;
71
+ flex: 1;
72
+ }
73
+
74
+ .seam-compatible-apps-name {
75
+ overflow: hidden;
76
+ color: var(--seam-ecosystem-fg-1);
77
+ font-weight: 600;
78
+ white-space: nowrap;
79
+ text-overflow: ellipsis;
80
+ }
81
+
82
+ .seam-compatible-apps-subtitle {
83
+ overflow: hidden;
84
+ color: var(--seam-ecosystem-fg-2);
85
+ font-size: 14px;
86
+ white-space: nowrap;
87
+ text-overflow: ellipsis;
88
+ }
89
+
90
+ .seam-compatible-apps-note {
91
+ padding-top: 4px;
92
+ color: var(--seam-ecosystem-fg-2);
93
+ font-size: 12px;
94
+ }
95
+
96
+ .seam-compatible-apps-footer {
97
+ display: flex;
98
+ justify-content: center;
99
+ padding-top: 16px;
100
+ }
101
+ }
102
+ }