From 4912a9d214f6216d039e485d5f5a0eb96b528464 Mon Sep 17 00:00:00 2001 From: Sy Bohy Date: Tue, 27 Oct 2026 15:38:18 -0700 Subject: [PATCH 1/2] chore: prototype five capability requirement layouts (DEVDB-467) --- .../supported-devices-prototypes/index.html | 13 + .../supported-devices-prototypes/src/App.tsx | 296 +++++++++ .../src/Expand.tsx | 39 ++ .../src/Groups.tsx | 44 ++ .../src/Inline.tsx | 24 + .../src/Notes.tsx | 57 ++ .../src/Picker.tsx | 96 +++ .../src/Table.tsx | 69 ++ .../supported-devices-prototypes/src/main.tsx | 20 + .../supported-devices-prototypes/src/model.ts | 68 ++ .../src/shared.tsx | 103 +++ .../src/style.css | 605 ++++++++++++++++++ .../src/theme.scss | 13 + vercel.json | 13 + 14 files changed, 1460 insertions(+) create mode 100644 examples/supported-devices-prototypes/index.html create mode 100644 examples/supported-devices-prototypes/src/App.tsx create mode 100644 examples/supported-devices-prototypes/src/Expand.tsx create mode 100644 examples/supported-devices-prototypes/src/Groups.tsx create mode 100644 examples/supported-devices-prototypes/src/Inline.tsx create mode 100644 examples/supported-devices-prototypes/src/Notes.tsx create mode 100644 examples/supported-devices-prototypes/src/Picker.tsx create mode 100644 examples/supported-devices-prototypes/src/Table.tsx create mode 100644 examples/supported-devices-prototypes/src/main.tsx create mode 100644 examples/supported-devices-prototypes/src/model.ts create mode 100644 examples/supported-devices-prototypes/src/shared.tsx create mode 100644 examples/supported-devices-prototypes/src/style.css create mode 100644 examples/supported-devices-prototypes/src/theme.scss diff --git a/examples/supported-devices-prototypes/index.html b/examples/supported-devices-prototypes/index.html new file mode 100644 index 000000000..66e519da7 --- /dev/null +++ b/examples/supported-devices-prototypes/index.html @@ -0,0 +1,13 @@ + + + + + + + Supported devices · 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..308cc2cc4 --- /dev/null +++ b/examples/supported-devices-prototypes/src/App.tsx @@ -0,0 +1,296 @@ +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 { Expand } from './Expand.js' +import { Groups } from './Groups.js' +import { Inline } from './Inline.js' +import { reviewDevice } from './model.js' +import { Notes } from './Notes.js' +import { Picker } from './Picker.js' +import { Table } from './Table.js' + +const variants = [ + { + name: 'Inline', + title: 'Requirements beside each capability', + description: + 'The most direct reading path. Everything is visible, with repetition when capabilities share requirements.', + component: Inline, + }, + { + name: 'Notes', + title: 'One shared list of requirements', + description: + 'A shorter device row. Numbered references connect each capability to what you’ll need below.', + component: Notes, + }, + { + name: 'Groups', + title: 'Plan around the things you need', + description: + 'Subscriptions and accessories appear once, each with the capabilities it enables. Clearer for planning a setup.', + component: Groups, + }, + { + name: 'Table', + title: 'Compare capabilities across models', + description: + 'Scan down a capability column to compare devices. Wider than the other options; scroll the table on a phone.', + component: Table, + }, + { + name: 'Expand', + title: 'Open details for one capability', + description: + 'Short requirement labels stay visible. Open a capability to read the full wording without expanding unrelated information.', + component: Expand, + }, +] +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 ( +
+ +
+
+ + Option {active + 1} of 5 · {variant.name} + +

{variant.title}

+

{variant.description}

+

+ Switch below or use keys 1–5 / ← →. R resets open details. +

+
+
+
+ Brand guides / {brand.label} + + View live guide ↗ + +
+

Supported devices

+

+ Capabilities available through Seam. Requirements below describe any + additional setup. +

+
+ + +
+
+ {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/Expand.tsx b/examples/supported-devices-prototypes/src/Expand.tsx new file mode 100644 index 000000000..5d50f67be --- /dev/null +++ b/examples/supported-devices-prototypes/src/Expand.tsx @@ -0,0 +1,39 @@ +import { requirementSummary, type VariantProps } from './model.js' +import { DeviceCard, RequirementList } from './shared.js' + +export function Expand({ devices }: VariantProps): JSX.Element { + return ( + <> + {devices.map((device) => ( + +

Supported capabilities

+
+ {device.capabilities.map((capability) => + capability.messages.length > 0 ? ( +
+ + + {capability.label} + + {requirementSummary(capability)} + + + + +
+ ) : ( +
+ {capability.label} + Supported +
+ ) + )} +
+
+ ))} + + ) +} diff --git a/examples/supported-devices-prototypes/src/Groups.tsx b/examples/supported-devices-prototypes/src/Groups.tsx new file mode 100644 index 000000000..f4ebf4540 --- /dev/null +++ b/examples/supported-devices-prototypes/src/Groups.tsx @@ -0,0 +1,44 @@ +import { RequirementIcon } from 'lib/seam/components/SupportedDevices/RequirementIcon.js' + +import { requirementLabel, type VariantProps } from './model.js' +import { DeviceCard } from './shared.js' + +export function Groups({ devices }: VariantProps): JSX.Element { + return ( + <> + {devices.map((device) => ( + +

Supported capabilities

+ + {device.requirements.length > 0 && ( +
+

What you’ll need

+ {device.requirements.map((requirement) => ( +
+ +
+ {requirementLabel(requirement.message)} +

{requirement.message}

+ + For {requirement.capabilities.join(', ')} + +
+
+ ))} +
+ )} +
+ ))} + + ) +} diff --git a/examples/supported-devices-prototypes/src/Inline.tsx b/examples/supported-devices-prototypes/src/Inline.tsx new file mode 100644 index 000000000..b248d1404 --- /dev/null +++ b/examples/supported-devices-prototypes/src/Inline.tsx @@ -0,0 +1,24 @@ +import type { VariantProps } from './model.js' +import { DeviceCard, RequirementList } from './shared.js' + +export function Inline({ devices }: VariantProps): JSX.Element { + return ( + <> + {devices.map((device) => ( + +

Supported capabilities

+ +
+ ))} + + ) +} diff --git a/examples/supported-devices-prototypes/src/Notes.tsx b/examples/supported-devices-prototypes/src/Notes.tsx new file mode 100644 index 000000000..fa9349cc2 --- /dev/null +++ b/examples/supported-devices-prototypes/src/Notes.tsx @@ -0,0 +1,57 @@ +import { useId } from 'react' + +import type { ReviewDevice, VariantProps } from './model.js' +import { DeviceCard } from './shared.js' + +export function Notes({ devices }: VariantProps): JSX.Element { + return ( + <> + {devices.map((device) => ( + + ))} + + ) +} + +function NoteDevice({ device }: { device: ReviewDevice }): JSX.Element { + const id = useId() + return ( + +

Supported capabilities

+
    + {device.capabilities.map((capability) => ( +
  • + {capability.label} + {capability.messages.map((message) => { + const index = device.requirements.findIndex( + (requirement) => requirement.message === message + ) + return ( + + {index + 1} + + ) + })} +
  • + ))} +
+ {device.requirements.length > 0 && ( +
+

What you’ll need

+
    + {device.requirements.map((requirement, index) => ( +
  1. + {requirement.message} +
  2. + ))} +
+
+ )} +
+ ) +} 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/Table.tsx b/examples/supported-devices-prototypes/src/Table.tsx new file mode 100644 index 000000000..1e63b4f92 --- /dev/null +++ b/examples/supported-devices-prototypes/src/Table.tsx @@ -0,0 +1,69 @@ +import type { VariantProps } from './model.js' +import { DeviceHeading, ModelDetails, RequirementList } from './shared.js' + +export function Table({ devices }: VariantProps): JSX.Element { + const capabilities = [ + ...new Map( + devices.flatMap((device) => + device.capabilities.map( + (capability) => [capability.name, capability.label] as const + ) + ) + ), + ] + return ( +
+ + + + + + {capabilities.map(([name, label]) => ( + + ))} + + + + {devices.map((device) => ( + + + {capabilities.map(([name]) => { + const capability = device.capabilities.find( + (item) => item.name === name + ) + return ( + + ) + })} + + ))} + +
Capabilities and requirements by model
Device + {label} +
+ + + + {capability == null ? ( + + Not listed as supported + + ) : ( + <> + + Supported + {capability.label.includes('(beta)') ? ' (beta)' : ''} + + {capability.messages.length > 0 && ( + + )} + + )} +
+
+ ) +} 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..0956baa3d --- /dev/null +++ b/examples/supported-devices-prototypes/src/model.ts @@ -0,0 +1,68 @@ +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, + })), + } +} + +// Short labels only for exact known copy; preserve unfamiliar constraints in full. +export function requirementLabel(message: string): string { + const labels: Record = { + 'A paying Nuki SmartHosting subscription is required.': + 'Nuki SmartHosting subscription', + 'A compatible bridge is required.': 'Compatible bridge', + 'An external keypad is required.': 'External keypad', + 'Additional setup is required.': 'Additional setup', + } + return labels[message] ?? message +} + +export function requirementSummary(capability: Capability): string { + return capability.messages.length === 0 + ? 'Supported' + : `Requires: ${capability.messages.map(requirementLabel).join(' + ')}` +} diff --git a/examples/supported-devices-prototypes/src/shared.tsx b/examples/supported-devices-prototypes/src/shared.tsx new file mode 100644 index 000000000..86f8ef6ec --- /dev/null +++ b/examples/supported-devices-prototypes/src/shared.tsx @@ -0,0 +1,103 @@ +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 { ReviewDevice } from './model.js' + +export function DeviceHeading({ + device, +}: { + device: ReviewDevice +}): JSX.Element { + const image = getDeviceModelImageUrl(device.source, null) + return ( +
+ + {image != null && ( + + )} + +
+

{device.name}

+ {device.source.capabilities_stale ? ( +

Compatibility is being refreshed.

+ ) : device.capabilities.length === 0 ? ( +

+ Compatibility not confirmed for this integration. +

+ ) : null} +
+
+ ) +} + +export function DeviceCard({ + device, + children, +}: PropsWithChildren<{ device: ReviewDevice }>): JSX.Element { + return ( +
+ +
+ {children} + +
+
+ ) +} + +export function ModelDetails({ + device, +}: { + device: ReviewDevice +}): JSX.Element { + const modelIds = [ + ...new Set( + device.source.variants + .map((variant) => variant.manufacturer_model_id) + .filter((id) => id != null && id.length > 0) + ), + ] + return ( +
+ Model details +
+ {modelIds.length > 0 && ( + <> +
Model numbers
+
{modelIds.join(', ')}
+ + )} +
Integration
+
+ {device.source.providers + .map((provider) => provider.display_name ?? provider.slug) + .join(', ')} +
+
+ {device.source.product_url != null && ( + + Manufacturer’s product page ↗ + + )} +
+ ) +} + +export function RequirementList({ + messages, +}: { + messages: string[] +}): JSX.Element { + return ( +
    + {messages.map((message) => ( +
  • + + {message} +
  • + ))} +
+ ) +} diff --git a/examples/supported-devices-prototypes/src/style.css b/examples/supported-devices-prototypes/src/style.css new file mode 100644 index 000000000..e224d5888 --- /dev/null +++ b/examples/supported-devices-prototypes/src/style.css @@ -0,0 +1,605 @@ +* { + box-sizing: border-box; +} +body { + margin: 0; +} +.seam-proto-page { + min-height: 100vh; + background: var(--seam-ecosystem-surface-2); + font-family: + Inter, + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + sans-serif; +} +.seam-proto-page button, +.seam-proto-page input, +.seam-proto-page select { + font: inherit; +} +.seam-proto-page button, +.seam-proto-page select, +.seam-proto-page summary { + cursor: pointer; +} +.seam-proto-page :focus-visible { + outline: 2px solid var(--seam-ecosystem-accent); + outline-offset: 3px; +} +.seam-proto-page h1, +.seam-proto-page h2, +.seam-proto-page h3, +.seam-proto-page h4, +.seam-proto-page p { + margin: 0; +} +.seam-proto-page h1 { + font-size: 26px; + line-height: 1.25; + letter-spacing: -0.6px; + font-weight: 600; +} +.seam-proto-page h2 { + font-size: 24px; + line-height: 1.3; + letter-spacing: -0.5px; + font-weight: 600; +} +.seam-proto-page h3 { + font-size: 16px; + line-height: 1.4; + font-weight: 600; +} +.seam-proto-page h4 { + font-size: 13px; + font-weight: 500; + color: var(--seam-ecosystem-fg-2); + margin-bottom: 10px; +} +.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-page a:hover { + text-decoration: underline; +} +.seam-proto-review-bar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 28px; + 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: end; + 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 input, +.seam-proto-page 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: 40px; + padding: 8px 12px; +} +.seam-proto-review-controls button[aria-pressed='true'] { + background: var(--seam-ecosystem-surface-2); +} +.seam-proto-workspace { + max-width: 890px; + margin: 0 auto; + padding: 42px 24px 112px; +} +.seam-proto-intro { + margin-bottom: 30px; +} +.seam-proto-eyebrow { + display: block; + font-size: 12px; + font-weight: 600; + letter-spacing: 0.05em; + text-transform: uppercase; + color: var(--seam-ecosystem-fg-2); + margin-bottom: 10px; +} +.seam-proto-intro p { + max-width: 690px; + font-size: 14px; + color: var(--seam-ecosystem-fg-2); + margin-top: 10px; + line-height: 1.6; +} +.seam-proto-intro .seam-proto-shortcuts { + font-size: 12px; + margin-top: 12px; +} +.seam-proto-stage { + background: var(--seam-ecosystem-surface-1); + border: 1px solid var(--seam-ecosystem-line-1); + border-radius: 12px; + padding: 28px; +} +.seam-proto-context { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 8px; + font-size: 12px; + color: var(--seam-ecosystem-fg-2); + margin-bottom: 22px; +} +.seam-proto-stage .seam-proto-stage-description { + font-size: 14px; + color: var(--seam-ecosystem-fg-2); + margin-top: 8px; + line-height: 1.5; +} +.seam-proto-filters { + display: flex; + align-items: end; + gap: 12px; + margin-top: 24px; +} +.seam-proto-filters label { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 12px; + color: var(--seam-ecosystem-fg-2); + min-width: 0; +} +.seam-proto-filters input, +.seam-proto-filters select { + width: 100%; + font-size: 14px; +} +.seam-proto-search { + flex: 1; +} +.seam-proto-catalog-heading { + display: flex; + justify-content: space-between; + padding: 22px 0 12px; + border-bottom: 1px solid var(--seam-ecosystem-line-1); + font-size: 14px; +} +.seam-proto-catalog-heading span { + font-size: 13px; + color: var(--seam-ecosystem-fg-2); +} +.seam-proto-device { + padding: 24px 0; + border-bottom: 1px solid var(--seam-ecosystem-line-1); +} +.seam-proto-device-heading { + display: flex; + align-items: center; + gap: 12px; +} +.seam-proto-image { + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + flex-shrink: 0; + border-radius: 9px; + background: var(--seam-ecosystem-surface-2); +} +.seam-proto-image img { + object-fit: contain; +} +.seam-proto-device-body { + margin: 14px 0 0 60px; + font-size: 14px; +} +.seam-proto-inline-list { + padding: 0 0 0 17px; + margin: 0; +} +.seam-proto-inline-list > li { + padding-left: 2px; + margin-bottom: 16px; +} +.seam-proto-requirement-list { + margin: 8px 0 0; + padding: 0; + list-style: none; +} +.seam-proto-requirement-list li { + display: flex; + align-items: start; + gap: 8px; + margin: 6px 0; + color: var(--seam-ecosystem-fg-2); + font-size: 13px; + line-height: 1.5; +} +.seam-proto-page .seam-supported-devices-requirement-icon { + flex-shrink: 0; + width: 18px; + height: 18px; + margin-top: 1px; + color: var(--seam-ecosystem-fg-2); +} +.seam-proto-model-details { + margin-top: 12px; + font-size: 13px; + color: var(--seam-ecosystem-fg-2); + font-weight: 400; +} +.seam-proto-model-details summary { + width: fit-content; + min-height: 36px; + align-content: center; +} +.seam-proto-model-details[open] { + padding-bottom: 8px; +} +.seam-proto-model-details dl { + margin: 8px 0; + line-height: 1.6; +} +.seam-proto-model-details dt { + color: var(--seam-ecosystem-fg-1); + font-weight: 500; +} +.seam-proto-model-details dd { + margin: 0 0 8px; + overflow-wrap: anywhere; +} +.seam-proto-note-capabilities, +.seam-proto-capability-bullets { + display: flex; + flex-wrap: wrap; + gap: 4px 30px; + margin: 0; + padding-left: 17px; +} +.seam-proto-note-capabilities li { + min-height: 32px; +} +.seam-proto-page .seam-proto-note-ref { + display: inline-flex; + justify-content: center; + align-items: center; + width: 26px; + height: 30px; + margin-left: 3px; + font-size: 11px; + font-weight: 600; + vertical-align: super; + border-radius: 5px; + background: var(--seam-ecosystem-surface-2); +} +.seam-proto-notes { + margin-top: 14px; + padding: 14px 16px; + border-radius: 8px; + background: var(--seam-ecosystem-surface-2); +} +.seam-proto-notes ol { + margin: 0; + padding-left: 19px; + font-size: 13px; + color: var(--seam-ecosystem-fg-2); +} +.seam-proto-notes li { + padding: 3px 4px; + scroll-margin-top: 24px; + border-radius: 4px; +} +.seam-proto-notes li:target { + background: var(--seam-ecosystem-accent-soft); + color: var(--seam-ecosystem-fg-1); + outline: 2px solid var(--seam-ecosystem-accent); +} +.seam-proto-groups { + margin-top: 18px; + padding: 16px; + border-radius: 8px; + background: var(--seam-ecosystem-surface-2); +} +.seam-proto-requirement-group { + display: flex; + gap: 10px; + padding: 12px 0; + border-top: 1px solid var(--seam-ecosystem-line-1); + font-size: 14px; +} +.seam-proto-requirement-group p { + font-size: 13px; + color: var(--seam-ecosystem-fg-2); + margin: 4px 0; +} +.seam-proto-requirement-group .seam-proto-muted { + display: block; + font-size: 12px; +} +.seam-proto-expand-list { + border-top: 1px solid var(--seam-ecosystem-line-1); +} +.seam-proto-capability-details { + border-bottom: 1px solid var(--seam-ecosystem-line-1); +} +.seam-proto-capability-details summary { + display: flex; + gap: 12px; + align-items: center; + justify-content: space-between; + padding: 12px 0; + min-height: 48px; + list-style: none; +} +.seam-proto-capability-details summary::-webkit-details-marker { + display: none; +} +.seam-proto-capability-details summary::after { + content: '+'; + font-size: 20px; + color: var(--seam-ecosystem-fg-2); + padding: 0 6px; +} +.seam-proto-capability-details[open] summary::after { + content: '−'; +} +.seam-proto-capability-details summary .seam-proto-muted { + display: block; + margin-top: 4px; + font-size: 13px; + line-height: 1.5; +} +.seam-proto-capability-details > .seam-proto-requirement-list { + margin: 0 0 14px; + padding: 12px; + background: var(--seam-ecosystem-surface-2); + border-radius: 7px; +} +.seam-proto-unconditional { + padding: 12px 0; + border-bottom: 1px solid var(--seam-ecosystem-line-1); + display: flex; + justify-content: space-between; + gap: 12px; +} +.seam-proto-table-scroll { + overflow-x: auto; + margin-top: 10px; +} +.seam-proto-table { + border-collapse: collapse; + width: 100%; + min-width: 700px; + text-align: left; + font-size: 13px; +} +.seam-proto-table caption { + text-align: left; + font-size: 12px; + color: var(--seam-ecosystem-fg-2); + padding: 8px 0 16px; +} +.seam-proto-table th, +.seam-proto-table td { + padding: 16px 12px; + vertical-align: top; + border-bottom: 1px solid var(--seam-ecosystem-line-1); +} +.seam-proto-table thead th { + background: var(--seam-ecosystem-surface-2); + font-weight: 500; +} +.seam-proto-table th:first-child { + width: 28%; + padding-left: 0; +} +.seam-proto-table .seam-proto-device-heading { + align-items: start; + gap: 8px; +} +.seam-proto-table h3 { + font-size: 14px; +} +.seam-proto-table .seam-proto-image { + height: 36px; + width: 36px; +} +.seam-proto-table .seam-proto-image img { + width: 32px; + height: 32px; +} +.seam-proto-table .seam-proto-requirement-list li { + font-size: 12px; + gap: 5px; + margin-bottom: 10px; +} +.seam-proto-table .seam-supported-devices-requirement-icon { + width: 14px; + height: 16px; +} +.seam-proto-supported { + font-size: 12px; + font-weight: 600; +} +.seam-proto-page .seam-proto-catalog-link { + display: inline-block; + font-size: 13px; + margin-top: 20px; +} +.seam-proto-review-note { + font-size: 12px; + color: var(--seam-ecosystem-fg-2); + text-align: center; + margin-top: 18px !important; +} +.seam-proto-state { + padding: 36px 0; + font-size: 14px; + color: var(--seam-ecosystem-fg-2); +} +.seam-proto-state button { + margin-top: 12px; +} +@media (max-width: 600px) { + .seam-proto-review-bar { + padding: 12px 16px; + flex-wrap: wrap; + } + .seam-proto-review-controls { + width: 100%; + justify-content: space-between; + } + .seam-proto-workspace { + padding: 28px 12px 100px; + } + .seam-proto-intro { + padding: 0 4px; + } + .seam-proto-page h1 { + font-size: 23px; + } + .seam-proto-stage { + padding: 20px 16px; + } + .seam-proto-filters { + flex-wrap: wrap; + } + .seam-proto-filters label { + width: 100%; + flex-basis: 100%; + } + .seam-proto-filters input, + .seam-proto-filters select, + .seam-proto-review-controls select { + font-size: 16px; + } + .seam-proto-device-body { + margin-left: 0; + margin-top: 18px; + } + .seam-proto-note-capabilities { + flex-direction: column; + } + .seam-proto-capability-bullets { + gap: 6px 26px; + } + .seam-proto-model-details summary { + min-height: 44px; + } +} +@media (hover: hover) and (pointer: fine) { + .seam-proto-capability-details summary:hover, + .seam-proto-model-details summary:hover { + color: var(--seam-ecosystem-accent); + } +} + +/* 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..de3fc71f9 --- /dev/null +++ b/examples/supported-devices-prototypes/src/theme.scss @@ -0,0 +1,13 @@ +@use '../../../src/styles/ecosystem'; + +.seam-proto-page { + @include ecosystem.light-tokens; + + color: var(--seam-ecosystem-fg-1); + font-size: 16px; + line-height: 1.4; + + &.seam-ecosystem-theme-dark { + @include ecosystem.dark-tokens; + } +} 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" + } + ] } ] } From 686eedb88d6c7eee3ea92215471091d37b301fe9 Mon Sep 17 00:00:00 2001 From: Sy Bohy Date: Tue, 27 Oct 2026 17:16:23 -0700 Subject: [PATCH 2/2] chore: explore compact DeviceDB rows with capability icons (DEVDB-468) --- .../supported-devices-prototypes/index.html | 2 +- .../supported-devices-prototypes/src/App.tsx | 58 +- .../src/Chips.tsx | 23 + .../src/Columns.tsx | 72 ++ .../src/Expand.tsx | 39 - .../src/Groups.tsx | 44 -- .../src/Inline.tsx | 24 - .../src/Notes.tsx | 57 -- .../src/Stacked.tsx | 23 + .../src/Table.tsx | 69 -- .../supported-devices-prototypes/src/model.ts | 18 - .../src/shared.tsx | 288 +++++-- .../src/style.css | 738 +++++++++++------- .../src/theme.scss | 40 +- 14 files changed, 849 insertions(+), 646 deletions(-) create mode 100644 examples/supported-devices-prototypes/src/Chips.tsx create mode 100644 examples/supported-devices-prototypes/src/Columns.tsx delete mode 100644 examples/supported-devices-prototypes/src/Expand.tsx delete mode 100644 examples/supported-devices-prototypes/src/Groups.tsx delete mode 100644 examples/supported-devices-prototypes/src/Inline.tsx delete mode 100644 examples/supported-devices-prototypes/src/Notes.tsx create mode 100644 examples/supported-devices-prototypes/src/Stacked.tsx delete mode 100644 examples/supported-devices-prototypes/src/Table.tsx diff --git a/examples/supported-devices-prototypes/index.html b/examples/supported-devices-prototypes/index.html index 66e519da7..d9d700df0 100644 --- a/examples/supported-devices-prototypes/index.html +++ b/examples/supported-devices-prototypes/index.html @@ -4,7 +4,7 @@ - Supported devices · Design review + 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 index 308cc2cc4..672d9eca8 100644 --- a/examples/supported-devices-prototypes/src/App.tsx +++ b/examples/supported-devices-prototypes/src/App.tsx @@ -9,49 +9,34 @@ import { import { useEcosystemDeviceModelSummaries } from 'lib/seam/ecosystem/use-ecosystem-device-model-summaries.js' import { useEcosystemProviderRequirements } from 'lib/seam/ecosystem/use-ecosystem-provider-requirements.js' -import { Expand } from './Expand.js' -import { Groups } from './Groups.js' -import { Inline } from './Inline.js' +import { Chips } from './Chips.js' +import { Columns } from './Columns.js' import { reviewDevice } from './model.js' -import { Notes } from './Notes.js' import { Picker } from './Picker.js' -import { Table } from './Table.js' +import { Legend } from './shared.js' +import { Stacked } from './Stacked.js' const variants = [ { - name: 'Inline', - title: 'Requirements beside each capability', + name: 'Chips', + title: 'Compact rows · capability chips', description: - 'The most direct reading path. Everything is visible, with repetition when capabilities share requirements.', - component: Inline, + 'The original image tiles and row styling, with labeled capability chips and one requirements disclosure.', + component: Chips, }, { - name: 'Notes', - title: 'One shared list of requirements', + name: 'Columns', + title: 'Compact rows · aligned feature icons', description: - 'A shorter device row. Numbered references connect each capability to what you’ll need below.', - component: Notes, + 'A small icon in each capability column makes the list easy to scan across models. Labels stay above the columns.', + component: Columns, }, { - name: 'Groups', - title: 'Plan around the things you need', + name: 'Stacked', + title: 'Compact rows · features below the name', description: - 'Subscriptions and accessories appear once, each with the capabilities it enables. Clearer for planning a setup.', - component: Groups, - }, - { - name: 'Table', - title: 'Compare capabilities across models', - description: - 'Scan down a capability column to compare devices. Wider than the other options; scroll the table on a phone.', - component: Table, - }, - { - name: 'Expand', - title: 'Open details for one capability', - description: - 'Short requirement labels stay visible. Open a capability to read the full wording without expanding unrelated information.', - component: Expand, + 'Two compact lines: device name above, supported capabilities below. Requirements open from the right.', + component: Stacked, }, ] const brands = [ @@ -170,12 +155,12 @@ export function App(): JSX.Element {
- Option {active + 1} of 5 · {variant.name} + Option {active + 1} of {variants.length} · {variant.name}

{variant.title}

{variant.description}

- Switch below or use keys 1–5 / ← →. R resets open details. + Switch below or use keys 1–3 / ← →. R resets open details.

@@ -191,8 +176,8 @@ export function App(): JSX.Element {

Supported devices

- Capabilities available through Seam. Requirements below describe any - additional setup. + Supported capabilities at a glance. Open a row for setup + requirements and model details.

+ {loading ? (

Loading capabilities and requirements… @@ -240,7 +226,7 @@ export function App(): JSX.Element { type='button' onClick={() => { void modelsQuery.refetch() - void requirementsQuery.refetch() + if (missing.length > 0) void requirementsQuery.refetch() }} > Try again 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/Expand.tsx b/examples/supported-devices-prototypes/src/Expand.tsx deleted file mode 100644 index 5d50f67be..000000000 --- a/examples/supported-devices-prototypes/src/Expand.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { requirementSummary, type VariantProps } from './model.js' -import { DeviceCard, RequirementList } from './shared.js' - -export function Expand({ devices }: VariantProps): JSX.Element { - return ( - <> - {devices.map((device) => ( - -

Supported capabilities

-
- {device.capabilities.map((capability) => - capability.messages.length > 0 ? ( -
- - - {capability.label} - - {requirementSummary(capability)} - - - - -
- ) : ( -
- {capability.label} - Supported -
- ) - )} -
-
- ))} - - ) -} diff --git a/examples/supported-devices-prototypes/src/Groups.tsx b/examples/supported-devices-prototypes/src/Groups.tsx deleted file mode 100644 index f4ebf4540..000000000 --- a/examples/supported-devices-prototypes/src/Groups.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { RequirementIcon } from 'lib/seam/components/SupportedDevices/RequirementIcon.js' - -import { requirementLabel, type VariantProps } from './model.js' -import { DeviceCard } from './shared.js' - -export function Groups({ devices }: VariantProps): JSX.Element { - return ( - <> - {devices.map((device) => ( - -

Supported capabilities

-
    - {device.capabilities.map((capability) => ( -
  • {capability.label}
  • - ))} -
- {device.requirements.length > 0 && ( -
-

What you’ll need

- {device.requirements.map((requirement) => ( -
- -
- {requirementLabel(requirement.message)} -

{requirement.message}

- - For {requirement.capabilities.join(', ')} - -
-
- ))} -
- )} -
- ))} - - ) -} diff --git a/examples/supported-devices-prototypes/src/Inline.tsx b/examples/supported-devices-prototypes/src/Inline.tsx deleted file mode 100644 index b248d1404..000000000 --- a/examples/supported-devices-prototypes/src/Inline.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import type { VariantProps } from './model.js' -import { DeviceCard, RequirementList } from './shared.js' - -export function Inline({ devices }: VariantProps): JSX.Element { - return ( - <> - {devices.map((device) => ( - -

Supported capabilities

-
    - {device.capabilities.map((capability) => ( -
  • - {capability.label} - {capability.messages.length > 0 && ( - - )} -
  • - ))} -
-
- ))} - - ) -} diff --git a/examples/supported-devices-prototypes/src/Notes.tsx b/examples/supported-devices-prototypes/src/Notes.tsx deleted file mode 100644 index fa9349cc2..000000000 --- a/examples/supported-devices-prototypes/src/Notes.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { useId } from 'react' - -import type { ReviewDevice, VariantProps } from './model.js' -import { DeviceCard } from './shared.js' - -export function Notes({ devices }: VariantProps): JSX.Element { - return ( - <> - {devices.map((device) => ( - - ))} - - ) -} - -function NoteDevice({ device }: { device: ReviewDevice }): JSX.Element { - const id = useId() - return ( - -

Supported capabilities

-
    - {device.capabilities.map((capability) => ( -
  • - {capability.label} - {capability.messages.map((message) => { - const index = device.requirements.findIndex( - (requirement) => requirement.message === message - ) - return ( - - {index + 1} - - ) - })} -
  • - ))} -
- {device.requirements.length > 0 && ( -
-

What you’ll need

-
    - {device.requirements.map((requirement, index) => ( -
  1. - {requirement.message} -
  2. - ))} -
-
- )} -
- ) -} 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/Table.tsx b/examples/supported-devices-prototypes/src/Table.tsx deleted file mode 100644 index 1e63b4f92..000000000 --- a/examples/supported-devices-prototypes/src/Table.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import type { VariantProps } from './model.js' -import { DeviceHeading, ModelDetails, RequirementList } from './shared.js' - -export function Table({ devices }: VariantProps): JSX.Element { - const capabilities = [ - ...new Map( - devices.flatMap((device) => - device.capabilities.map( - (capability) => [capability.name, capability.label] as const - ) - ) - ), - ] - return ( -
- - - - - - {capabilities.map(([name, label]) => ( - - ))} - - - - {devices.map((device) => ( - - - {capabilities.map(([name]) => { - const capability = device.capabilities.find( - (item) => item.name === name - ) - return ( - - ) - })} - - ))} - -
Capabilities and requirements by model
Device - {label} -
- - - - {capability == null ? ( - - Not listed as supported - - ) : ( - <> - - Supported - {capability.label.includes('(beta)') ? ' (beta)' : ''} - - {capability.messages.length > 0 && ( - - )} - - )} -
-
- ) -} diff --git a/examples/supported-devices-prototypes/src/model.ts b/examples/supported-devices-prototypes/src/model.ts index 0956baa3d..0c81508e3 100644 --- a/examples/supported-devices-prototypes/src/model.ts +++ b/examples/supported-devices-prototypes/src/model.ts @@ -48,21 +48,3 @@ export function reviewDevice(source: DeviceModelSummary): ReviewDevice { })), } } - -// Short labels only for exact known copy; preserve unfamiliar constraints in full. -export function requirementLabel(message: string): string { - const labels: Record = { - 'A paying Nuki SmartHosting subscription is required.': - 'Nuki SmartHosting subscription', - 'A compatible bridge is required.': 'Compatible bridge', - 'An external keypad is required.': 'External keypad', - 'Additional setup is required.': 'Additional setup', - } - return labels[message] ?? message -} - -export function requirementSummary(capability: Capability): string { - return capability.messages.length === 0 - ? 'Supported' - : `Requires: ${capability.messages.map(requirementLabel).join(' + ')}` -} diff --git a/examples/supported-devices-prototypes/src/shared.tsx b/examples/supported-devices-prototypes/src/shared.tsx index 86f8ef6ec..e1ff88ce7 100644 --- a/examples/supported-devices-prototypes/src/shared.tsx +++ b/examples/supported-devices-prototypes/src/shared.tsx @@ -3,101 +3,277 @@ 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 { ReviewDevice } from './model.js' +import type { Capability, ReviewDevice } from './model.js' -export function DeviceHeading({ - device, +export function CapabilityIcon({ name }: { name: string }): JSX.Element { + return ( + + {name.includes('access_codes') ? ( + <> + + + + ) : name.includes('unlock') ? ( + <> + + + + ) : name.includes('lock') ? ( + <> + + + + ) : ( + <> + + + + )} + + ) +} + +export function InfoMark(): JSX.Element { + return ( + + + + + ) +} + +export function Feature({ + capability, + iconOnly = false, }: { - device: ReviewDevice + 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 && ( - + + + {image == null ? ( + {device.name.slice(0, 1)} + ) : ( + )} -
-

{device.name}

+ + {showType && ( + + {device.source.device_type?.display_name ?? 'Device'} + + )} + {device.name} {device.source.capabilities_stale ? ( -

Compatibility is being refreshed.

+ + Compatibility is being refreshed. + ) : device.capabilities.length === 0 ? ( -

- Compatibility not confirmed for this integration. -

+ + Compatibility not confirmed. + ) : null} -
-
+ {children} + + ) } -export function DeviceCard({ +export function CompactRow({ device, + layout, children, -}: PropsWithChildren<{ device: ReviewDevice }>): JSX.Element { +}: PropsWithChildren<{ device: ReviewDevice; layout: string }>): JSX.Element { + const conditional = device.requirements.length > 0 return ( -
- -
+
+ {children} - -
-
+ + {conditional && } + {conditional ? 'Requirements' : 'Details'} + + + + + + + ) } -export function ModelDetails({ - device, -}: { - device: ReviewDevice -}): JSX.Element { +function ExpandedDetails({ device }: { device: ReviewDevice }): JSX.Element { const modelIds = [ ...new Set( device.source.variants .map((variant) => variant.manufacturer_model_id) - .filter((id) => id != null && id.length > 0) + .filter((id): id is string => id != null && id.length > 0) ), ] + const integration = device.source.providers + .map((provider) => provider.display_name ?? provider.slug) + .join(', ') return ( -
- Model details -
+
+
+ + {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
-
- {device.source.providers - .map((provider) => provider.display_name ?? provider.slug) - .join(', ')} -
+
+
Integration
+
{integration}
+
{device.source.product_url != null && ( - + Manufacturer’s product page ↗ )} -
+ ) } -export function RequirementList({ - messages, -}: { - messages: string[] -}): JSX.Element { +export function Legend(): JSX.Element { return ( -
    - {messages.map((message) => ( -
  • - - {message} -
  • - ))} -
+
+ + + + + Supported + + + + Requires setup · open requirements + +
) } diff --git a/examples/supported-devices-prototypes/src/style.css b/examples/supported-devices-prototypes/src/style.css index e224d5888..f96b1a0eb 100644 --- a/examples/supported-devices-prototypes/src/style.css +++ b/examples/supported-devices-prototypes/src/style.css @@ -8,55 +8,35 @@ body { min-height: 100vh; background: var(--seam-ecosystem-surface-2); font-family: - Inter, + Roboto, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } -.seam-proto-page button, -.seam-proto-page input, -.seam-proto-page select { +.seam-proto-page :is(button, input, select) { font: inherit; } -.seam-proto-page button, -.seam-proto-page select, -.seam-proto-page summary { +.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 h1, -.seam-proto-page h2, -.seam-proto-page h3, -.seam-proto-page h4, -.seam-proto-page p { +.seam-proto-page :is(h1, h2, h3, h4, p) { margin: 0; } .seam-proto-page h1 { - font-size: 26px; - line-height: 1.25; - letter-spacing: -0.6px; - font-weight: 600; + font-size: 21px; + line-height: 1.3; + letter-spacing: -0.4px; + font-weight: 550; } .seam-proto-page h2 { - font-size: 24px; - line-height: 1.3; + font-size: 22px; + font-weight: 550; letter-spacing: -0.5px; - font-weight: 600; -} -.seam-proto-page h3 { - font-size: 16px; - line-height: 1.4; - font-weight: 600; -} -.seam-proto-page h4 { - font-size: 13px; - font-weight: 500; - color: var(--seam-ecosystem-fg-2); - margin-bottom: 10px; } .seam-proto-page strong { font-weight: 550; @@ -69,14 +49,11 @@ body { color: var(--seam-ecosystem-accent); text-underline-offset: 3px; } -.seam-proto-page a:hover { - text-decoration: underline; -} .seam-proto-review-bar { display: flex; align-items: center; justify-content: space-between; - padding: 14px 28px; + padding: 10px 24px; gap: 16px; border-bottom: 1px solid var(--seam-ecosystem-line-1); background: var(--seam-ecosystem-surface-1); @@ -84,7 +61,7 @@ body { } .seam-proto-review-controls { display: flex; - align-items: end; + align-items: center; gap: 12px; } .seam-proto-review-controls label { @@ -94,87 +71,81 @@ body { font-size: 12px; color: var(--seam-ecosystem-fg-2); } -.seam-proto-page input, -.seam-proto-page select, +.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: 40px; - padding: 8px 12px; + 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: 890px; + max-width: 1010px; margin: 0 auto; - padding: 42px 24px 112px; + padding: 24px 24px 100px; } .seam-proto-intro { - margin-bottom: 30px; + margin-bottom: 20px; } .seam-proto-eyebrow { display: block; - font-size: 12px; - font-weight: 600; - letter-spacing: 0.05em; + font-size: 11px; + font-weight: 550; + letter-spacing: 0.04em; text-transform: uppercase; color: var(--seam-ecosystem-fg-2); - margin-bottom: 10px; + margin-bottom: 6px; } .seam-proto-intro p { - max-width: 690px; - font-size: 14px; + font-size: 13px; color: var(--seam-ecosystem-fg-2); - margin-top: 10px; - line-height: 1.6; + margin-top: 6px; } .seam-proto-intro .seam-proto-shortcuts { - font-size: 12px; - margin-top: 12px; + font-size: 11px; } .seam-proto-stage { background: var(--seam-ecosystem-surface-1); border: 1px solid var(--seam-ecosystem-line-1); - border-radius: 12px; - padding: 28px; + border-radius: 10px; + padding: 24px 20px; } .seam-proto-context { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 8px; - font-size: 12px; + font-size: 11px; color: var(--seam-ecosystem-fg-2); - margin-bottom: 22px; + margin-bottom: 12px; } -.seam-proto-stage .seam-proto-stage-description { - font-size: 14px; +.seam-proto-stage-description { + font-size: 13px; color: var(--seam-ecosystem-fg-2); - margin-top: 8px; - line-height: 1.5; + margin-top: 4px !important; } .seam-proto-filters { display: flex; align-items: end; gap: 12px; - margin-top: 24px; + margin-top: 16px; } .seam-proto-filters label { display: flex; flex-direction: column; - gap: 6px; - font-size: 12px; + gap: 4px; + font-size: 11px; color: var(--seam-ecosystem-fg-2); min-width: 0; } -.seam-proto-filters input, -.seam-proto-filters select { +.seam-proto-filters :is(input, select) { width: 100%; - font-size: 14px; + font-size: 13px; } .seam-proto-search { flex: 1; @@ -182,290 +153,352 @@ body { .seam-proto-catalog-heading { display: flex; justify-content: space-between; - padding: 22px 0 12px; - border-bottom: 1px solid var(--seam-ecosystem-line-1); - font-size: 14px; + padding: 18px 0 10px; + font-size: 16px; } -.seam-proto-catalog-heading span { - font-size: 13px; +.seam-proto-catalog-heading > span { + font-size: 12px; color: var(--seam-ecosystem-fg-2); } -.seam-proto-device { - padding: 24px 0; - border-bottom: 1px solid var(--seam-ecosystem-line-1); +.seam-proto-catalog-link { + display: inline-block; + font-size: 12px; + margin-top: 18px; } -.seam-proto-device-heading { - display: flex; - align-items: center; - gap: 12px; +.seam-proto-review-note { + font-size: 11px; + color: var(--seam-ecosystem-fg-2); + text-align: center; + margin-top: 16px !important; } -.seam-proto-image { +.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; - justify-content: center; - width: 48px; - height: 48px; - flex-shrink: 0; - border-radius: 9px; - background: var(--seam-ecosystem-surface-2); + gap: 5px; } -.seam-proto-image img { - object-fit: contain; +.seam-compact-legend > span:first-child svg { + color: var(--seam-compact-green); } -.seam-proto-device-body { - margin: 14px 0 0 60px; - font-size: 14px; +.seam-compact-legend > span:last-child svg { + color: var(--seam-compact-orange); } -.seam-proto-inline-list { - padding: 0 0 0 17px; - margin: 0; +.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-proto-inline-list > li { - padding-left: 2px; - margin-bottom: 16px; +.seam-compact-row { + border-bottom: 1px solid var(--seam-ecosystem-line-1); } -.seam-proto-requirement-list { - margin: 8px 0 0; - padding: 0; +.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-proto-requirement-list li { +.seam-compact-row-summary::-webkit-details-marker { + display: none; +} +.seam-compact-row-summary > * { + min-width: 0; +} +.seam-compact-identity { display: flex; - align-items: start; - gap: 8px; - margin: 6px 0; - color: var(--seam-ecosystem-fg-2); - font-size: 13px; - line-height: 1.5; + align-items: center; + gap: 16px; + min-width: 0; } -.seam-proto-page .seam-supported-devices-requirement-icon { +.seam-compact-image { + display: flex; + align-items: center; + justify-content: center; + width: 56px; + height: 56px; + padding: 6px; flex-shrink: 0; - width: 18px; - height: 18px; - margin-top: 1px; - color: var(--seam-ecosystem-fg-2); -} -.seam-proto-model-details { - margin-top: 12px; - font-size: 13px; - color: var(--seam-ecosystem-fg-2); - font-weight: 400; + border-radius: 8px; + background: var(--seam-compact-tile); + box-shadow: var(--seam-compact-tile-shadow); } -.seam-proto-model-details summary { - width: fit-content; - min-height: 36px; - align-content: center; +.seam-compact-image img { + width: 44px; + height: 44px; + object-fit: contain; } -.seam-proto-model-details[open] { - padding-bottom: 8px; +.seam-compact-identity-text { + display: flex; + flex-direction: column; + gap: 3px; + min-width: 0; } -.seam-proto-model-details dl { - margin: 8px 0; - line-height: 1.6; +.seam-compact-device-type { + font-size: 11px; + color: var(--seam-ecosystem-fg-2); } -.seam-proto-model-details dt { - color: var(--seam-ecosystem-fg-1); +.seam-compact-device-name { + font-size: 15px; + line-height: 1.4; font-weight: 500; -} -.seam-proto-model-details dd { - margin: 0 0 8px; overflow-wrap: anywhere; } -.seam-proto-note-capabilities, -.seam-proto-capability-bullets { +.seam-compact-device-status { + font-size: 11px; + color: var(--seam-ecosystem-fg-2); +} +.seam-compact-features { display: flex; flex-wrap: wrap; - gap: 4px 30px; - margin: 0; - padding-left: 17px; -} -.seam-proto-note-capabilities li { - min-height: 32px; + gap: 5px; + align-items: center; } -.seam-proto-page .seam-proto-note-ref { +.seam-compact-feature { display: inline-flex; - justify-content: center; align-items: center; - width: 26px; - height: 30px; - margin-left: 3px; - font-size: 11px; - font-weight: 600; - vertical-align: super; - border-radius: 5px; - background: var(--seam-ecosystem-surface-2); + 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-proto-notes { - margin-top: 14px; - padding: 14px 16px; - border-radius: 8px; - background: var(--seam-ecosystem-surface-2); +.seam-compact-feature svg { + width: 15px; + height: 15px; + flex-shrink: 0; } -.seam-proto-notes ol { - margin: 0; - padding-left: 19px; - font-size: 13px; +.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-proto-notes li { - padding: 3px 4px; - scroll-margin-top: 24px; - border-radius: 4px; +.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-proto-notes li:target { - background: var(--seam-ecosystem-accent-soft); - color: var(--seam-ecosystem-fg-1); - outline: 2px solid var(--seam-ecosystem-accent); +.seam-compact-chevron { + flex-shrink: 0; } -.seam-proto-groups { - margin-top: 18px; - padding: 16px; - border-radius: 8px; +.seam-compact-row[open] > summary .seam-compact-chevron { + transform: rotate(180deg); +} +.seam-compact-row[open] > summary { background: var(--seam-ecosystem-surface-2); } -.seam-proto-requirement-group { +.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; - padding: 12px 0; - border-top: 1px solid var(--seam-ecosystem-line-1); font-size: 14px; } -.seam-proto-requirement-group p { - font-size: 13px; - color: var(--seam-ecosystem-fg-2); - margin: 4px 0; -} -.seam-proto-requirement-group .seam-proto-muted { - display: block; +.seam-compact-detail-heading p { font-size: 12px; + color: var(--seam-ecosystem-fg-2); + margin-top: 2px; } -.seam-proto-expand-list { - border-top: 1px solid var(--seam-ecosystem-line-1); -} -.seam-proto-capability-details { - border-bottom: 1px solid var(--seam-ecosystem-line-1); -} -.seam-proto-capability-details summary { +.seam-compact-provider-mark { display: flex; - gap: 12px; align-items: center; - justify-content: space-between; - padding: 12px 0; - min-height: 48px; - list-style: none; -} -.seam-proto-capability-details summary::-webkit-details-marker { - display: none; -} -.seam-proto-capability-details summary::after { - content: '+'; - font-size: 20px; + 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); - padding: 0 6px; + font-size: 15px; + font-weight: 600; } -.seam-proto-capability-details[open] summary::after { - content: '−'; +.seam-compact-requirements { + list-style: none; + margin: 16px 0; + padding: 0; + display: grid; + gap: 12px; } -.seam-proto-capability-details summary .seam-proto-muted { - display: block; - margin-top: 4px; - font-size: 13px; +.seam-compact-requirements li { + display: flex; + align-items: start; + gap: 9px; + font-size: 14px; line-height: 1.5; } -.seam-proto-capability-details > .seam-proto-requirement-list { - margin: 0 0 14px; - padding: 12px; - background: var(--seam-ecosystem-surface-2); - border-radius: 7px; +.seam-compact-requirements .seam-supported-devices-requirement-icon { + width: 17px; + height: 19px; + color: var(--seam-compact-orange); + flex-shrink: 0; } -.seam-proto-unconditional { - padding: 12px 0; - border-bottom: 1px solid var(--seam-ecosystem-line-1); - display: flex; - justify-content: space-between; - gap: 12px; +.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-proto-table-scroll { - overflow-x: auto; - margin-top: 10px; +.seam-compact-model-info dt { + color: var(--seam-ecosystem-fg-2); + font-size: 11px; + margin-bottom: 4px; } -.seam-proto-table { - border-collapse: collapse; - width: 100%; - min-width: 700px; - text-align: left; - font-size: 13px; +.seam-compact-model-info dd { + margin: 0; + overflow-wrap: anywhere; } -.seam-proto-table caption { - text-align: left; +.seam-compact-product-link { + display: inline-block; font-size: 12px; - color: var(--seam-ecosystem-fg-2); - padding: 8px 0 16px; -} -.seam-proto-table th, -.seam-proto-table td { - padding: 16px 12px; - vertical-align: top; - border-bottom: 1px solid var(--seam-ecosystem-line-1); + margin: 8px 0; } -.seam-proto-table thead th { - background: var(--seam-ecosystem-surface-2); - font-weight: 500; +.seam-compact-columns > summary, +.seam-compact-column-heading { + grid-template-columns: minmax(210px, 1fr) auto 120px; } -.seam-proto-table th:first-child { - width: 28%; - padding-left: 0; +.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-proto-table .seam-proto-device-heading { - align-items: start; - gap: 8px; +.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-proto-table h3 { - font-size: 14px; +.seam-compact-column-labels > span { + padding: 0 6px; } -.seam-proto-table .seam-proto-image { - height: 36px; - width: 36px; +.seam-compact-column-cell { + display: inline-flex; + justify-content: center; } -.seam-proto-table .seam-proto-image img { +.seam-compact-feature-icon-only { width: 32px; - height: 32px; + height: 30px; + padding: 0; + justify-content: center; } -.seam-proto-table .seam-proto-requirement-list li { - font-size: 12px; - gap: 5px; - margin-bottom: 10px; +.seam-compact-feature-icon-only svg { + width: 18px; + height: 18px; } -.seam-proto-table .seam-supported-devices-requirement-icon { - width: 14px; - height: 16px; +.seam-compact-icon-label, +.seam-compact-sr-only { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip-path: inset(50%); + white-space: nowrap; } -.seam-proto-supported { - font-size: 12px; - font-weight: 600; +.seam-compact-stacked > summary { + grid-template-columns: minmax(0, 1fr) auto; } -.seam-proto-page .seam-proto-catalog-link { - display: inline-block; - font-size: 13px; - margin-top: 20px; +.seam-compact-stacked .seam-compact-identity-text { + gap: 7px; } -.seam-proto-review-note { - font-size: 12px; - color: var(--seam-ecosystem-fg-2); - text-align: center; - margin-top: 18px !important; +.seam-compact-stacked .seam-compact-features { + gap: 14px; } -.seam-proto-state { - padding: 36px 0; - font-size: 14px; - color: var(--seam-ecosystem-fg-2); +.seam-compact-stacked .seam-compact-feature { + background: none; + box-shadow: none; + padding: 0; + font-size: 12px; } -.seam-proto-state button { - margin-top: 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: 12px 16px; + padding: 10px 14px; flex-wrap: wrap; } .seam-proto-review-controls { @@ -473,50 +506,163 @@ body { justify-content: space-between; } .seam-proto-workspace { - padding: 28px 12px 100px; + padding: 20px 10px 100px; } .seam-proto-intro { padding: 0 4px; } .seam-proto-page h1 { - font-size: 23px; + font-size: 19px; } .seam-proto-stage { - padding: 20px 16px; + 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 input, - .seam-proto-filters select, + .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-proto-device-body { - margin-left: 0; - margin-top: 18px; + .seam-compact-feature svg { + width: 14px; + height: 14px; } - .seam-proto-note-capabilities { - flex-direction: column; + .seam-compact-expanded { + margin: 0 14px 16px 38px; + padding: 16px 12px 0; } - .seam-proto-capability-bullets { - gap: 6px 26px; + .seam-compact-expanded::before { + display: none; } - .seam-proto-model-details summary { - min-height: 44px; + .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-proto-capability-details summary:hover, - .seam-proto-model-details summary:hover { - color: var(--seam-ecosystem-accent); + .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; diff --git a/examples/supported-devices-prototypes/src/theme.scss b/examples/supported-devices-prototypes/src/theme.scss index de3fc71f9..85bec66a9 100644 --- a/examples/supported-devices-prototypes/src/theme.scss +++ b/examples/supported-devices-prototypes/src/theme.scss @@ -1,13 +1,41 @@ -@use '../../../src/styles/ecosystem'; - +// Match the original DeviceDB semantic neutrals and status colors. .seam-proto-page { - @include ecosystem.light-tokens; + --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: 16px; - line-height: 1.4; + font-size: 15px; + line-height: 1.45; &.seam-ecosystem-theme-dark { - @include ecosystem.dark-tokens; + --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%); } }