diff --git a/examples/supported-devices-prototypes/index.html b/examples/supported-devices-prototypes/index.html
new file mode 100644
index 000000000..d9d700df0
--- /dev/null
+++ b/examples/supported-devices-prototypes/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ Compact DeviceDB · Design review
+
+
+ Loading design review…
+
+
+
diff --git a/examples/supported-devices-prototypes/src/App.tsx b/examples/supported-devices-prototypes/src/App.tsx
new file mode 100644
index 000000000..672d9eca8
--- /dev/null
+++ b/examples/supported-devices-prototypes/src/App.tsx
@@ -0,0 +1,282 @@
+import { useEffect, useState } from 'react'
+
+import {
+ getCapabilityLabel,
+ getProvidersWithMissingRequirements,
+ resolveProviderRequirements,
+ scopeDeviceModelsToProvider,
+} from 'lib/seam/components/SupportedDevices/device-models.js'
+import { useEcosystemDeviceModelSummaries } from 'lib/seam/ecosystem/use-ecosystem-device-model-summaries.js'
+import { useEcosystemProviderRequirements } from 'lib/seam/ecosystem/use-ecosystem-provider-requirements.js'
+
+import { Chips } from './Chips.js'
+import { Columns } from './Columns.js'
+import { reviewDevice } from './model.js'
+import { Picker } from './Picker.js'
+import { Legend } from './shared.js'
+import { Stacked } from './Stacked.js'
+
+const variants = [
+ {
+ name: 'Chips',
+ title: 'Compact rows · capability chips',
+ description:
+ 'The original image tiles and row styling, with labeled capability chips and one requirements disclosure.',
+ component: Chips,
+ },
+ {
+ name: 'Columns',
+ title: 'Compact rows · aligned feature icons',
+ description:
+ 'A small icon in each capability column makes the list easy to scan across models. Labels stay above the columns.',
+ component: Columns,
+ },
+ {
+ name: 'Stacked',
+ title: 'Compact rows · features below the name',
+ description:
+ 'Two compact lines: device name above, supported capabilities below. Requirements open from the right.',
+ component: Stacked,
+ },
+]
+const brands = [
+ { slug: 'nuki', label: 'Nuki', provider: 'nuki-web' },
+ { slug: 'august', label: 'August', provider: 'august-access' },
+ { slug: 'keyincode', label: 'KeyInCode', provider: 'keyincode' },
+]
+const params = new URLSearchParams(window.location.search)
+const requestedVariant = Number(params.get('v')) - 1
+const initialVariant =
+ Number.isInteger(requestedVariant) &&
+ requestedVariant >= 0 &&
+ requestedVariant < variants.length
+ ? requestedVariant
+ : 0
+
+export function App(): JSX.Element {
+ const [active, setActive] = useState(initialVariant)
+ const [replay, setReplay] = useState(0)
+ const [brandSlug, setBrandSlug] = useState(params.get('brand') ?? 'nuki')
+ const [dark, setDark] = useState(params.get('theme') === 'dark')
+ const [search, setSearch] = useState('')
+ const [capability, setCapability] = useState('')
+ const brand = brands.find((entry) => entry.slug === brandSlug) ?? brands[0]
+ const variant = variants[active] ?? variants[0]
+
+ useEffect(() => {
+ const url = new URL(window.location.href)
+ url.searchParams.set('v', String(active + 1))
+ url.searchParams.set('brand', brandSlug)
+ url.searchParams.set('theme', dark ? 'dark' : 'light')
+ window.history.replaceState(null, '', url)
+ document.documentElement.style.colorScheme = dark ? 'dark' : 'light'
+ }, [active, brandSlug, dark])
+
+ const modelsQuery = useEcosystemDeviceModelSummaries({
+ manufacturer: brand?.slug,
+ provider: brand?.provider,
+ deviceType: 'smartlock',
+ })
+ const scopedModels = scopeDeviceModelsToProvider(
+ modelsQuery.deviceModels ?? [],
+ brand?.provider
+ )
+ const missing = getProvidersWithMissingRequirements(scopedModels)
+ const requirementsQuery = useEcosystemProviderRequirements(missing)
+ const devices = resolveProviderRequirements(
+ scopedModels,
+ requirementsQuery.providers ?? []
+ ).map(reviewDevice)
+ const allCapabilities = [
+ ...new Set(
+ devices.flatMap((device) => device.capabilities.map((item) => item.name))
+ ),
+ ]
+ const filtered = devices.filter((device) => {
+ const text = [
+ device.name,
+ ...device.source.variants.map((item) => item.manufacturer_model_id),
+ ]
+ .join(' ')
+ .toLowerCase()
+ return (
+ text.includes(search.toLowerCase().trim()) &&
+ (capability === '' ||
+ device.capabilities.some((item) => item.name === capability))
+ )
+ })
+ const loading =
+ modelsQuery.isLoading || (missing.length > 0 && requirementsQuery.isLoading)
+ const error = modelsQuery.isError || requirementsQuery.isError
+ if (variant == null || brand == null) return Design review unavailable.
+ const Variant = variant.component
+
+ return (
+
+
+
+
+
+
+ Supported devices
+
+ Supported capabilities at a glance. Open a row for setup
+ requirements and model details.
+
+
+
+
+
+
+ {brand.label}
+
+ {loading ? 'Loading…' : `${filtered.length} devices`}
+
+
+
+ {loading ? (
+
+ Loading capabilities and requirements…
+
+ ) : error ? (
+
+
We couldn’t load the complete device information.
+
+
+ ) : filtered.length === 0 ? (
+
+
No devices match these filters.
+
+
+ ) : (
+
+
+
+ )}
+
+ View {brand.label} in the device catalog ↗
+
+
+
+ Review prototype · Live Ecosystem catalog ·{' '}
+
+ Dawn’s design feedback ↗
+
+
+
+
item.name)}
+ active={active}
+ onChange={setActive}
+ onReplay={() => {
+ setReplay((value) => value + 1)
+ }}
+ />
+
+ )
+}
diff --git a/examples/supported-devices-prototypes/src/Chips.tsx b/examples/supported-devices-prototypes/src/Chips.tsx
new file mode 100644
index 000000000..0c4e0595b
--- /dev/null
+++ b/examples/supported-devices-prototypes/src/Chips.tsx
@@ -0,0 +1,23 @@
+import type { VariantProps } from './model.js'
+import { CompactRow, DeviceIdentity, Features } from './shared.js'
+
+export function Chips({ devices }: VariantProps): JSX.Element {
+ return (
+
+
+ Device
+ Supported capabilities
+
+ {devices.map((device) => (
+
+
+
+
+ ))}
+
+ )
+}
diff --git a/examples/supported-devices-prototypes/src/Columns.tsx b/examples/supported-devices-prototypes/src/Columns.tsx
new file mode 100644
index 000000000..02f2c99eb
--- /dev/null
+++ b/examples/supported-devices-prototypes/src/Columns.tsx
@@ -0,0 +1,72 @@
+import type { CSSProperties } from 'react'
+
+import { getCapabilityLabel } from 'lib/seam/components/SupportedDevices/device-models.js'
+
+import type { VariantProps } from './model.js'
+import { CompactRow, DeviceIdentity, Feature } from './shared.js'
+
+export function Columns({ devices }: VariantProps): JSX.Element {
+ const names = [
+ ...new Set(
+ devices.flatMap((device) =>
+ device.capabilities.map((capability) => capability.name)
+ )
+ ),
+ ]
+ const style: CSSProperties & { '--seam-compact-column-count': number } = {
+ '--seam-compact-column-count': names.length,
+ }
+ return (
+
+
+ Device
+
+ {names.map((name) => (
+ {getCapabilityLabel(name)}
+ ))}
+
+
+
+ {devices.map((device) => (
+
+
+
+ {names.map((name) => {
+ const capability = device.capabilities.find(
+ (item) => item.name === name
+ )
+ return (
+
+ {capability == null ? (
+
+ —
+
+ {getCapabilityLabel(name)}: not listed as supported
+
+
+ ) : (
+
+ )}
+
+ )
+ })}
+
+
+ ))}
+
+ )
+}
diff --git a/examples/supported-devices-prototypes/src/Picker.tsx b/examples/supported-devices-prototypes/src/Picker.tsx
new file mode 100644
index 000000000..3b3390680
--- /dev/null
+++ b/examples/supported-devices-prototypes/src/Picker.tsx
@@ -0,0 +1,96 @@
+import { useEffect, useLayoutEffect, useRef } from 'react'
+
+export function Picker({
+ names,
+ active,
+ onChange,
+ onReplay,
+}: {
+ names: string[]
+ active: number
+ onChange: (index: number) => void
+ onReplay: () => void
+}): JSX.Element {
+ const nav = useRef(null)
+ useLayoutEffect(() => {
+ const element = nav.current
+ if (element == null) return
+ const measure = (): void => {
+ const button = element.querySelector('[data-active]')
+ const highlight = element.querySelector(
+ '.proto-picker-highlight'
+ )
+ if (button == null || highlight == null) return
+ highlight.style.transform = `translateX(${button.offsetLeft}px)`
+ highlight.style.width = `${button.offsetWidth}px`
+ }
+ measure()
+ const observer = new ResizeObserver(measure)
+ observer.observe(element)
+ let secondFrame = 0
+ const frame = requestAnimationFrame(() => {
+ secondFrame = requestAnimationFrame(() => {
+ element.dataset['ready'] = ''
+ })
+ })
+ return () => {
+ observer.disconnect()
+ cancelAnimationFrame(frame)
+ cancelAnimationFrame(secondFrame)
+ }
+ }, [active])
+
+ useEffect(() => {
+ const onKey = (event: KeyboardEvent): void => {
+ const target = event.target
+ if (
+ event.metaKey ||
+ event.ctrlKey ||
+ event.altKey ||
+ event.shiftKey ||
+ (target instanceof HTMLElement &&
+ (target.closest('input, textarea, select') != null ||
+ target.isContentEditable))
+ )
+ return
+ const number = Number(event.key)
+ if (number >= 1 && number <= names.length) {
+ event.preventDefault()
+ onChange(number - 1)
+ } else if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
+ event.preventDefault()
+ onChange(
+ (active + (event.key === 'ArrowRight' ? 1 : names.length - 1)) %
+ names.length
+ )
+ } else if (event.key.toLowerCase() === 'r') {
+ event.preventDefault()
+ onReplay()
+ }
+ }
+ window.addEventListener('keydown', onKey)
+ return () => {
+ window.removeEventListener('keydown', onKey)
+ }
+ }, [active, names.length, onChange, onReplay])
+
+ return (
+
+ )
+}
diff --git a/examples/supported-devices-prototypes/src/Stacked.tsx b/examples/supported-devices-prototypes/src/Stacked.tsx
new file mode 100644
index 000000000..0ced2f213
--- /dev/null
+++ b/examples/supported-devices-prototypes/src/Stacked.tsx
@@ -0,0 +1,23 @@
+import type { VariantProps } from './model.js'
+import { CompactRow, DeviceIdentity, Features } from './shared.js'
+
+export function Stacked({ devices }: VariantProps): JSX.Element {
+ return (
+
+
+ Device & supported capabilities
+
+ {devices.map((device) => (
+
+
+
+
+
+ ))}
+
+ )
+}
diff --git a/examples/supported-devices-prototypes/src/main.tsx b/examples/supported-devices-prototypes/src/main.tsx
new file mode 100644
index 000000000..34ce7ef17
--- /dev/null
+++ b/examples/supported-devices-prototypes/src/main.tsx
@@ -0,0 +1,20 @@
+import './theme.scss'
+import './style.css'
+
+import { createRoot } from 'react-dom/client'
+
+import { EcosystemProvider } from 'lib/seam/ecosystem/EcosystemProvider.js'
+
+import { App } from './App.js'
+
+const root = document.getElementById('root')
+if (root != null)
+ createRoot(root).render(
+
+
+
+ )
diff --git a/examples/supported-devices-prototypes/src/model.ts b/examples/supported-devices-prototypes/src/model.ts
new file mode 100644
index 000000000..0c81508e3
--- /dev/null
+++ b/examples/supported-devices-prototypes/src/model.ts
@@ -0,0 +1,50 @@
+import {
+ getCapabilityLabel,
+ getCapabilityRequirementMessages,
+ getDeviceModelName,
+ getSupportedCapabilities,
+} from 'lib/seam/components/SupportedDevices/device-models.js'
+import type { DeviceModelSummary } from 'lib/seam/ecosystem/types.js'
+
+export interface Capability {
+ name: string
+ label: string
+ messages: string[]
+}
+
+export interface ReviewDevice {
+ source: DeviceModelSummary
+ name: string
+ capabilities: Capability[]
+ requirements: Array<{ message: string; capabilities: string[] }>
+}
+
+export interface VariantProps {
+ devices: ReviewDevice[]
+}
+
+export function reviewDevice(source: DeviceModelSummary): ReviewDevice {
+ const capabilities = getSupportedCapabilities(source)
+ .filter((capability) => capability.name !== 'device_connectivity')
+ .map((capability) => ({
+ name: capability.name,
+ label: `${getCapabilityLabel(capability.name)}${capability.integrationSupportLevel === 'beta' ? ' (beta)' : ''}`,
+ messages: getCapabilityRequirementMessages(capability),
+ }))
+ const requirements = new Map()
+ for (const capability of capabilities)
+ for (const message of capability.messages)
+ requirements.set(message, [
+ ...(requirements.get(message) ?? []),
+ capability.label,
+ ])
+ return {
+ source,
+ name: getDeviceModelName(source),
+ capabilities,
+ requirements: [...requirements].map(([message, labels]) => ({
+ message,
+ capabilities: labels,
+ })),
+ }
+}
diff --git a/examples/supported-devices-prototypes/src/shared.tsx b/examples/supported-devices-prototypes/src/shared.tsx
new file mode 100644
index 000000000..e1ff88ce7
--- /dev/null
+++ b/examples/supported-devices-prototypes/src/shared.tsx
@@ -0,0 +1,279 @@
+import type { PropsWithChildren } from 'react'
+
+import { getDeviceModelImageUrl } from 'lib/seam/components/SupportedDevices/device-models.js'
+import { RequirementIcon } from 'lib/seam/components/SupportedDevices/RequirementIcon.js'
+
+import type { Capability, ReviewDevice } from './model.js'
+
+export function CapabilityIcon({ name }: { name: string }): JSX.Element {
+ return (
+
+ )
+}
+
+export function InfoMark(): JSX.Element {
+ return (
+
+ )
+}
+
+export function Feature({
+ capability,
+ iconOnly = false,
+}: {
+ capability: Capability
+ iconOnly?: boolean
+}): JSX.Element {
+ const status = capability.messages.length > 0 ? 'Requires setup' : 'Supported'
+ return (
+ 0 ? '' : undefined}
+ title={`${capability.label} · ${status}`}
+ >
+
+
+ {capability.label}
+
+ · {status}
+
+ )
+}
+
+export function Features({ device }: { device: ReviewDevice }): JSX.Element {
+ return (
+
+ {device.capabilities.map((capability) => (
+
+
+
+ ))}
+
+ )
+}
+
+export function DeviceIdentity({
+ device,
+ children,
+ showType = true,
+}: PropsWithChildren<{
+ device: ReviewDevice
+ showType?: boolean
+}>): JSX.Element {
+ const image = getDeviceModelImageUrl(device.source, null)
+ return (
+
+
+ {image == null ? (
+ {device.name.slice(0, 1)}
+ ) : (
+
+ )}
+
+
+ {showType && (
+
+ {device.source.device_type?.display_name ?? 'Device'}
+
+ )}
+ {device.name}
+ {device.source.capabilities_stale ? (
+
+ Compatibility is being refreshed.
+
+ ) : device.capabilities.length === 0 ? (
+
+ Compatibility not confirmed.
+
+ ) : null}
+ {children}
+
+
+ )
+}
+
+export function CompactRow({
+ device,
+ layout,
+ children,
+}: PropsWithChildren<{ device: ReviewDevice; layout: string }>): JSX.Element {
+ const conditional = device.requirements.length > 0
+ return (
+
+
+ {children}
+
+ {conditional && }
+ {conditional ? 'Requirements' : 'Details'}
+
+
+
+
+
+ )
+}
+
+function ExpandedDetails({ device }: { device: ReviewDevice }): JSX.Element {
+ const modelIds = [
+ ...new Set(
+ device.source.variants
+ .map((variant) => variant.manufacturer_model_id)
+ .filter((id): id is string => id != null && id.length > 0)
+ ),
+ ]
+ const integration = device.source.providers
+ .map((provider) => provider.display_name ?? provider.slug)
+ .join(', ')
+ return (
+
+
+
+ {integration.slice(0, 1)}
+
+
+
{integration}
+
+ {device.requirements.length > 0
+ ? 'Additional setup for these capabilities'
+ : 'Model and integration details'}
+
+
+
+ {device.requirements.length > 0 && (
+
+ {device.requirements.map((requirement) => (
+ -
+
+
+
{requirement.message}
+
For {requirement.capabilities.join(', ')}
+
+
+ ))}
+
+ )}
+
+ {modelIds.length > 0 && (
+
+
- Model numbers
+ - {modelIds.join(', ')}
+
+ )}
+
+
- Integration
+ - {integration}
+
+
+ {device.source.product_url != null && (
+
+ Manufacturer’s product page ↗
+
+ )}
+
+ )
+}
+
+export function Legend(): JSX.Element {
+ return (
+
+
+
+ Supported
+
+
+
+ Requires setup · open requirements
+
+
+ )
+}
diff --git a/examples/supported-devices-prototypes/src/style.css b/examples/supported-devices-prototypes/src/style.css
new file mode 100644
index 000000000..f96b1a0eb
--- /dev/null
+++ b/examples/supported-devices-prototypes/src/style.css
@@ -0,0 +1,751 @@
+* {
+ box-sizing: border-box;
+}
+body {
+ margin: 0;
+}
+.seam-proto-page {
+ min-height: 100vh;
+ background: var(--seam-ecosystem-surface-2);
+ font-family:
+ Roboto,
+ -apple-system,
+ BlinkMacSystemFont,
+ 'Segoe UI',
+ sans-serif;
+}
+.seam-proto-page :is(button, input, select) {
+ font: inherit;
+}
+.seam-proto-page :is(button, select, summary) {
+ cursor: pointer;
+}
+.seam-proto-page :focus-visible {
+ outline: 2px solid var(--seam-ecosystem-accent);
+ outline-offset: 3px;
+}
+.seam-proto-page :is(h1, h2, h3, h4, p) {
+ margin: 0;
+}
+.seam-proto-page h1 {
+ font-size: 21px;
+ line-height: 1.3;
+ letter-spacing: -0.4px;
+ font-weight: 550;
+}
+.seam-proto-page h2 {
+ font-size: 22px;
+ font-weight: 550;
+ letter-spacing: -0.5px;
+}
+.seam-proto-page strong {
+ font-weight: 550;
+}
+.seam-proto-muted {
+ color: var(--seam-ecosystem-fg-2);
+ font-size: 13px;
+}
+.seam-proto-page a {
+ color: var(--seam-ecosystem-accent);
+ text-underline-offset: 3px;
+}
+.seam-proto-review-bar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10px 24px;
+ gap: 16px;
+ border-bottom: 1px solid var(--seam-ecosystem-line-1);
+ background: var(--seam-ecosystem-surface-1);
+ font-size: 14px;
+}
+.seam-proto-review-controls {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+.seam-proto-review-controls label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 12px;
+ color: var(--seam-ecosystem-fg-2);
+}
+.seam-proto-page :is(input, select),
+.seam-proto-review-controls button,
+.seam-proto-state button {
+ color: var(--seam-ecosystem-fg-1);
+ background: var(--seam-ecosystem-surface-1);
+ border: 1px solid var(--seam-ecosystem-line-2);
+ border-radius: 7px;
+ min-height: 36px;
+ padding: 6px 10px;
+}
+.seam-proto-review-controls button[aria-pressed='true'] {
+ background: var(--seam-ecosystem-surface-2);
+}
+.seam-proto-workspace {
+ max-width: 1010px;
+ margin: 0 auto;
+ padding: 24px 24px 100px;
+}
+.seam-proto-intro {
+ margin-bottom: 20px;
+}
+.seam-proto-eyebrow {
+ display: block;
+ font-size: 11px;
+ font-weight: 550;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: var(--seam-ecosystem-fg-2);
+ margin-bottom: 6px;
+}
+.seam-proto-intro p {
+ font-size: 13px;
+ color: var(--seam-ecosystem-fg-2);
+ margin-top: 6px;
+}
+.seam-proto-intro .seam-proto-shortcuts {
+ font-size: 11px;
+}
+.seam-proto-stage {
+ background: var(--seam-ecosystem-surface-1);
+ border: 1px solid var(--seam-ecosystem-line-1);
+ border-radius: 10px;
+ padding: 24px 20px;
+}
+.seam-proto-context {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ gap: 8px;
+ font-size: 11px;
+ color: var(--seam-ecosystem-fg-2);
+ margin-bottom: 12px;
+}
+.seam-proto-stage-description {
+ font-size: 13px;
+ color: var(--seam-ecosystem-fg-2);
+ margin-top: 4px !important;
+}
+.seam-proto-filters {
+ display: flex;
+ align-items: end;
+ gap: 12px;
+ margin-top: 16px;
+}
+.seam-proto-filters label {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+ font-size: 11px;
+ color: var(--seam-ecosystem-fg-2);
+ min-width: 0;
+}
+.seam-proto-filters :is(input, select) {
+ width: 100%;
+ font-size: 13px;
+}
+.seam-proto-search {
+ flex: 1;
+}
+.seam-proto-catalog-heading {
+ display: flex;
+ justify-content: space-between;
+ padding: 18px 0 10px;
+ font-size: 16px;
+}
+.seam-proto-catalog-heading > span {
+ font-size: 12px;
+ color: var(--seam-ecosystem-fg-2);
+}
+.seam-proto-catalog-link {
+ display: inline-block;
+ font-size: 12px;
+ margin-top: 18px;
+}
+.seam-proto-review-note {
+ font-size: 11px;
+ color: var(--seam-ecosystem-fg-2);
+ text-align: center;
+ margin-top: 16px !important;
+}
+.seam-proto-state {
+ padding: 28px 0;
+ font-size: 14px;
+ color: var(--seam-ecosystem-fg-2);
+}
+.seam-proto-state button {
+ margin-top: 12px;
+}
+.seam-compact-legend {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px 18px;
+ margin-bottom: 14px;
+ font-size: 11px;
+ color: var(--seam-ecosystem-fg-2);
+}
+.seam-compact-legend > span {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+}
+.seam-compact-legend > span:first-child svg {
+ color: var(--seam-compact-green);
+}
+.seam-compact-legend > span:last-child svg {
+ color: var(--seam-compact-orange);
+}
+.seam-compact-list-heading {
+ display: grid;
+ grid-template-columns: minmax(230px, 1fr) minmax(0, 1.5fr);
+ padding: 10px 12px;
+ font-size: 11px;
+ color: var(--seam-ecosystem-fg-2);
+ border-block: 1px solid var(--seam-ecosystem-line-1);
+}
+.seam-compact-row {
+ border-bottom: 1px solid var(--seam-ecosystem-line-1);
+}
+.seam-compact-row-summary {
+ display: grid;
+ grid-template-columns: minmax(210px, 1fr) auto auto;
+ align-items: center;
+ gap: 14px;
+ min-height: 89px;
+ padding: 16px 12px;
+ list-style: none;
+}
+.seam-compact-row-summary::-webkit-details-marker {
+ display: none;
+}
+.seam-compact-row-summary > * {
+ min-width: 0;
+}
+.seam-compact-identity {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ min-width: 0;
+}
+.seam-compact-image {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 56px;
+ height: 56px;
+ padding: 6px;
+ flex-shrink: 0;
+ border-radius: 8px;
+ background: var(--seam-compact-tile);
+ box-shadow: var(--seam-compact-tile-shadow);
+}
+.seam-compact-image img {
+ width: 44px;
+ height: 44px;
+ object-fit: contain;
+}
+.seam-compact-identity-text {
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+ min-width: 0;
+}
+.seam-compact-device-type {
+ font-size: 11px;
+ color: var(--seam-ecosystem-fg-2);
+}
+.seam-compact-device-name {
+ font-size: 15px;
+ line-height: 1.4;
+ font-weight: 500;
+ overflow-wrap: anywhere;
+}
+.seam-compact-device-status {
+ font-size: 11px;
+ color: var(--seam-ecosystem-fg-2);
+}
+.seam-compact-features {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 5px;
+ align-items: center;
+}
+.seam-compact-feature {
+ display: inline-flex;
+ align-items: center;
+ gap: 5px;
+ padding: 4px 7px;
+ border-radius: 6px;
+ color: var(--seam-compact-green);
+ background: var(--seam-compact-green-bg);
+ box-shadow: inset 0 0 0 0.5px rgb(9 10 11 / 8%);
+ font-size: 12px;
+ line-height: 16px;
+ white-space: nowrap;
+}
+.seam-compact-feature svg {
+ width: 15px;
+ height: 15px;
+ flex-shrink: 0;
+}
+.seam-compact-feature[data-conditional] {
+ color: var(--seam-compact-orange);
+ background: var(--seam-compact-orange-bg);
+}
+.seam-compact-disclosure {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 5px;
+ justify-self: end;
+ padding: 5px 7px;
+ min-height: 30px;
+ border-radius: 6px;
+ font-size: 12px;
+ color: var(--seam-ecosystem-fg-2);
+ white-space: nowrap;
+}
+.seam-compact-disclosure[data-conditional] {
+ color: var(--seam-compact-orange);
+ background: var(--seam-compact-orange-bg);
+ box-shadow: inset 0 0 0 0.5px rgb(9 10 11 / 8%);
+}
+.seam-compact-chevron {
+ flex-shrink: 0;
+}
+.seam-compact-row[open] > summary .seam-compact-chevron {
+ transform: rotate(180deg);
+}
+.seam-compact-row[open] > summary {
+ background: var(--seam-ecosystem-surface-2);
+}
+.seam-compact-expanded {
+ position: relative;
+ margin: 0 12px 20px 84px;
+ padding: 18px 20px 2px;
+ border-left: 1px dashed var(--seam-ecosystem-line-2);
+}
+.seam-compact-expanded::before {
+ position: absolute;
+ content: '';
+ top: 0;
+ left: -37px;
+ width: 36px;
+ height: 36px;
+ border-left: 1px dashed var(--seam-ecosystem-line-2);
+ border-bottom: 1px dashed var(--seam-ecosystem-line-2);
+ border-bottom-left-radius: 12px;
+}
+.seam-compact-detail-heading {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ font-size: 14px;
+}
+.seam-compact-detail-heading p {
+ font-size: 12px;
+ color: var(--seam-ecosystem-fg-2);
+ margin-top: 2px;
+}
+.seam-compact-provider-mark {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ flex-shrink: 0;
+ border-radius: 7px;
+ background: var(--seam-compact-tile);
+ box-shadow: var(--seam-compact-tile-shadow);
+ color: var(--seam-ecosystem-fg-2);
+ font-size: 15px;
+ font-weight: 600;
+}
+.seam-compact-requirements {
+ list-style: none;
+ margin: 16px 0;
+ padding: 0;
+ display: grid;
+ gap: 12px;
+}
+.seam-compact-requirements li {
+ display: flex;
+ align-items: start;
+ gap: 9px;
+ font-size: 14px;
+ line-height: 1.5;
+}
+.seam-compact-requirements .seam-supported-devices-requirement-icon {
+ width: 17px;
+ height: 19px;
+ color: var(--seam-compact-orange);
+ flex-shrink: 0;
+}
+.seam-compact-requirements li span {
+ display: block;
+ color: var(--seam-ecosystem-fg-2);
+ font-size: 11px;
+ margin-top: 2px;
+}
+.seam-compact-model-info {
+ display: grid;
+ grid-template-columns: 1.5fr 1fr;
+ gap: 18px;
+ margin: 16px 0 8px;
+ padding-top: 12px;
+ border-top: 1px solid var(--seam-ecosystem-line-1);
+ font-size: 12px;
+ line-height: 1.5;
+}
+.seam-compact-model-info dt {
+ color: var(--seam-ecosystem-fg-2);
+ font-size: 11px;
+ margin-bottom: 4px;
+}
+.seam-compact-model-info dd {
+ margin: 0;
+ overflow-wrap: anywhere;
+}
+.seam-compact-product-link {
+ display: inline-block;
+ font-size: 12px;
+ margin: 8px 0;
+}
+.seam-compact-columns > summary,
+.seam-compact-column-heading {
+ grid-template-columns: minmax(210px, 1fr) auto 120px;
+}
+.seam-compact-column-heading {
+ display: grid;
+ align-items: center;
+ gap: 14px;
+ padding: 10px 12px;
+ font-size: 11px;
+ color: var(--seam-ecosystem-fg-2);
+ border-block: 1px solid var(--seam-ecosystem-line-1);
+}
+.seam-compact-column-labels,
+.seam-compact-column-cells {
+ display: grid;
+ grid-template-columns: repeat(var(--seam-compact-column-count), 76px);
+ align-items: center;
+ text-align: center;
+}
+.seam-compact-column-labels > span {
+ padding: 0 6px;
+}
+.seam-compact-column-cell {
+ display: inline-flex;
+ justify-content: center;
+}
+.seam-compact-feature-icon-only {
+ width: 32px;
+ height: 30px;
+ padding: 0;
+ justify-content: center;
+}
+.seam-compact-feature-icon-only svg {
+ width: 18px;
+ height: 18px;
+}
+.seam-compact-icon-label,
+.seam-compact-sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ clip-path: inset(50%);
+ white-space: nowrap;
+}
+.seam-compact-stacked > summary {
+ grid-template-columns: minmax(0, 1fr) auto;
+}
+.seam-compact-stacked .seam-compact-identity-text {
+ gap: 7px;
+}
+.seam-compact-stacked .seam-compact-features {
+ gap: 14px;
+}
+.seam-compact-stacked .seam-compact-feature {
+ background: none;
+ box-shadow: none;
+ padding: 0;
+ font-size: 12px;
+}
+@media (max-width: 850px) {
+ .seam-compact-chips > summary {
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 8px 12px;
+ }
+ .seam-compact-chips .seam-compact-features {
+ grid-row: 2;
+ grid-column: 1 / -1;
+ padding-left: 72px;
+ }
+ .seam-compact-chips .seam-compact-disclosure {
+ grid-column: 2;
+ grid-row: 1;
+ }
+ .seam-compact-list-heading {
+ grid-template-columns: 1fr;
+ }
+ .seam-compact-list-heading > span:nth-child(2) {
+ display: none;
+ }
+}
+@media (max-width: 600px) {
+ .seam-proto-review-bar {
+ padding: 10px 14px;
+ flex-wrap: wrap;
+ }
+ .seam-proto-review-controls {
+ width: 100%;
+ justify-content: space-between;
+ }
+ .seam-proto-workspace {
+ padding: 20px 10px 100px;
+ }
+ .seam-proto-intro {
+ padding: 0 4px;
+ }
+ .seam-proto-page h1 {
+ font-size: 19px;
+ }
+ .seam-proto-stage {
+ padding: 20px 0;
+ border-radius: 8px;
+ }
+ .seam-proto-stage > :not(.seam-proto-variant) {
+ margin-left: 14px;
+ margin-right: 14px;
+ }
+ .seam-proto-filters {
+ flex-wrap: wrap;
+ gap: 8px;
+ }
+ .seam-proto-filters label {
+ width: 100%;
+ flex-basis: 100%;
+ }
+ .seam-proto-filters :is(input, select),
+ .seam-proto-review-controls select {
+ font-size: 16px;
+ min-height: 40px;
+ }
+ .seam-compact-row-summary {
+ min-height: 81px;
+ gap: 10px;
+ padding: 12px 14px;
+ }
+ .seam-compact-image {
+ width: 48px;
+ height: 48px;
+ padding: 6px;
+ }
+ .seam-compact-image img {
+ width: 36px;
+ height: 36px;
+ }
+ .seam-compact-identity {
+ gap: 12px;
+ }
+ .seam-compact-device-name {
+ font-size: 14px;
+ }
+ .seam-compact-disclosure {
+ font-size: 11px;
+ gap: 4px;
+ padding: 5px;
+ }
+ .seam-compact-list-heading {
+ padding: 9px 14px;
+ }
+ .seam-compact-chips .seam-compact-features {
+ padding-left: 60px;
+ }
+ .seam-compact-feature {
+ padding: 3px 5px;
+ font-size: 11px;
+ gap: 4px;
+ }
+ .seam-compact-feature svg {
+ width: 14px;
+ height: 14px;
+ }
+ .seam-compact-expanded {
+ margin: 0 14px 16px 38px;
+ padding: 16px 12px 0;
+ }
+ .seam-compact-expanded::before {
+ display: none;
+ }
+ .seam-compact-model-info {
+ grid-template-columns: 1fr;
+ gap: 12px;
+ }
+ .seam-compact-column-heading {
+ grid-template-columns: 1fr;
+ padding: 9px 14px;
+ }
+ .seam-compact-column-heading > :not(:first-child) {
+ display: none;
+ }
+ .seam-compact-columns > summary {
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 8px 12px;
+ }
+ .seam-compact-column-cells {
+ grid-column: 1 / -1;
+ grid-row: 2;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 5px;
+ padding-left: 60px;
+ text-align: left;
+ }
+ .seam-compact-columns .seam-compact-disclosure {
+ grid-column: 2;
+ grid-row: 1;
+ }
+ .seam-compact-feature-icon-only {
+ width: auto;
+ height: auto;
+ padding: 3px 5px;
+ }
+ .seam-compact-icon-label {
+ position: static;
+ width: auto;
+ height: auto;
+ overflow: visible;
+ clip-path: none;
+ white-space: normal;
+ }
+ .seam-compact-stacked > summary {
+ grid-template-columns: 48px minmax(0, 1fr) auto;
+ }
+ .seam-compact-stacked .seam-compact-identity,
+ .seam-compact-stacked .seam-compact-identity-text {
+ display: contents;
+ }
+ .seam-compact-stacked .seam-compact-image {
+ grid-column: 1;
+ grid-row: 1 / 3;
+ align-self: start;
+ }
+ .seam-compact-stacked .seam-compact-device-name {
+ grid-column: 2;
+ grid-row: 1;
+ }
+ .seam-compact-stacked .seam-compact-disclosure {
+ grid-column: 3;
+ grid-row: 1;
+ }
+ .seam-compact-stacked .seam-compact-device-status {
+ grid-column: 2 / -1;
+ }
+ .seam-compact-stacked .seam-compact-features {
+ grid-column: 2 / -1;
+ grid-row: 2;
+ gap: 6px 10px;
+ }
+ .seam-compact-stacked .seam-compact-feature {
+ font-size: 11px;
+ }
+}
+@media (hover: hover) and (pointer: fine) {
+ .seam-compact-row-summary:hover {
+ background: var(--seam-ecosystem-surface-2);
+ }
+ .seam-proto-page a:hover {
+ text-decoration: underline;
+ }
+}
+/* Emil's prototype picker: intentionally separate from the product styling. */
+.proto-picker {
+ position: fixed;
+ bottom: 24px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 2147483647;
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ padding: 4px;
+ border-radius: 999px;
+ background: rgba(10, 10, 10, 0.82);
+ -webkit-backdrop-filter: blur(12px) saturate(1.4);
+ backdrop-filter: blur(12px) saturate(1.4);
+ box-shadow:
+ 0 0 0 1px rgba(255, 255, 255, 0.08) inset,
+ 0 8px 24px rgba(0, 0, 0, 0.24),
+ 0 2px 6px rgba(0, 0, 0, 0.12);
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+ font-size: 13px;
+ line-height: 1;
+ -webkit-font-smoothing: antialiased;
+ user-select: none;
+ -webkit-user-select: none;
+}
+.proto-picker-highlight {
+ position: absolute;
+ top: 4px;
+ left: 0;
+ height: 28px;
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.12);
+ will-change: transform;
+}
+.proto-picker[data-ready] .proto-picker-highlight {
+ transition:
+ transform 250ms cubic-bezier(0.23, 1, 0.32, 1),
+ width 250ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+@media (prefers-reduced-motion: reduce) {
+ .proto-picker[data-ready] .proto-picker-highlight {
+ transition: none;
+ }
+}
+.proto-picker-item {
+ position: relative;
+ display: flex;
+ align-items: center;
+ height: 28px;
+ padding: 0 12px;
+ border: 0;
+ border-radius: 999px;
+ background: transparent;
+ color: rgba(255, 255, 255, 0.55);
+ font: inherit;
+ cursor: pointer;
+ transition: color 150ms ease-out;
+}
+.proto-picker-item:hover {
+ color: rgba(255, 255, 255, 0.85);
+}
+.proto-picker-item:active {
+ transform: scale(0.97);
+}
+.proto-picker-item:focus-visible {
+ outline: 2px solid rgba(255, 255, 255, 0.4);
+ outline-offset: 2px;
+}
+.proto-picker-item[data-active] {
+ color: #fff;
+}
+.proto-picker-divider {
+ width: 1px;
+ height: 16px;
+ margin: 0 4px;
+ background: rgba(255, 255, 255, 0.12);
+}
+.proto-picker-replay {
+ padding: 0 10px;
+ font-size: 14px;
+}
+.proto-picker[data-position='top'] {
+ bottom: auto;
+ top: 24px;
+}
diff --git a/examples/supported-devices-prototypes/src/theme.scss b/examples/supported-devices-prototypes/src/theme.scss
new file mode 100644
index 000000000..85bec66a9
--- /dev/null
+++ b/examples/supported-devices-prototypes/src/theme.scss
@@ -0,0 +1,41 @@
+// Match the original DeviceDB semantic neutrals and status colors.
+.seam-proto-page {
+ --seam-ecosystem-fg-1: #090a0b;
+ --seam-ecosystem-fg-2: #5c6475;
+ --seam-ecosystem-fg-3: #848b9a;
+ --seam-ecosystem-surface-1: #fff;
+ --seam-ecosystem-surface-2: #f5f6f8;
+ --seam-ecosystem-line-1: #e5e7eb;
+ --seam-ecosystem-line-2: #ccd0d7;
+ --seam-ecosystem-accent: #4c45d9;
+ --seam-compact-green: #137413;
+ --seam-compact-green-bg: #edf8ed;
+ --seam-compact-orange: #945313;
+ --seam-compact-orange-bg: #fcf2e9;
+ --seam-compact-tile: #fff;
+ --seam-compact-tile-shadow:
+ 0 0 0 0.5px rgb(9 10 11 / 8%), 0 0 2px rgb(9 10 11 / 12%),
+ 0 2px 4px rgb(9 10 11 / 4%);
+
+ color: var(--seam-ecosystem-fg-1);
+ font-size: 15px;
+ line-height: 1.45;
+
+ &.seam-ecosystem-theme-dark {
+ --seam-ecosystem-fg-1: #eeeff2;
+ --seam-ecosystem-fg-2: #acb2be;
+ --seam-ecosystem-fg-3: #848b9a;
+ --seam-ecosystem-surface-1: #090a0b;
+ --seam-ecosystem-surface-2: #191b1f;
+ --seam-ecosystem-line-1: #282a30;
+ --seam-ecosystem-line-2: #31353f;
+ --seam-ecosystem-accent: #b5b1f1;
+ --seam-compact-green: #89d189;
+ --seam-compact-green-bg: rgb(25 143 25 / 16%);
+ --seam-compact-orange: #eaad70;
+ --seam-compact-orange-bg: rgb(154 89 25 / 16%);
+ --seam-compact-tile: #191b1f;
+ --seam-compact-tile-shadow:
+ 0 0 0 0.5px rgb(255 255 255 / 12%), 0 2px 4px rgb(0 0 0 / 12%);
+ }
+}
diff --git a/vercel.json b/vercel.json
index 537459a2c..c3b6375ee 100644
--- a/vercel.json
+++ b/vercel.json
@@ -184,6 +184,19 @@
"value": "noindex, follow"
}
]
+ },
+ {
+ "source": "/examples/supported-devices-prototypes(/.*)?",
+ "headers": [
+ {
+ "key": "Content-Security-Policy",
+ "value": "default-src 'self'; script-src 'self'; connect-src 'self'; img-src 'self' data: https://seam-ecosystem-media.fly.storage.tigris.dev https://strapi-media-bucket.fly.storage.tigris.dev; style-src 'self' 'unsafe-inline'; frame-ancestors 'self'"
+ },
+ {
+ "key": "X-Robots-Tag",
+ "value": "noindex, nofollow"
+ }
+ ]
}
]
}