Skip to main content

UI components

@smartcompanion/ui holds the web components visitors see. They are built with Stencil, so they work in any framework or none.

npm install @smartcompanion/ui

Every component is live in Storybook, with its properties and events.

Using them​

Importing the package registers every custom element:

import '@smartcompanion/ui';

Pages take the service facade as their facade property — through componentProps on a route:

<ion-route url="/selection" component="sc-page-selection" componentProps={{ facade: serviceFacade }} />

The package's entry points are the root, @smartcompanion/ui/loader (defineCustomElements, for apps that register the elements themselves) and @smartcompanion/ui/dist/collection/*. A Stencil app builds the components from that collection together with its own, rather than loading the prebuilt bundle.

Pages​

Each page is a full screen with its own header. Properties every list page shares: headerBackgroundColor ('primary' or 'secondary') and tourId — show only that tour's stations; 'default' means the default tour.

PageWhat it showsNotable properties
sc-page-loadingLogo and progress while the content loads, then moves on by itselfimage, imageDark, homeRoute (/stations/default), spinnerColor, progressBarColor, loadingTextColor
sc-page-languageThe language choice—
sc-page-pinThe PIN keypadvalidHours (6)
sc-page-errorThe load error, with a retry button—
sc-page-stationsStation list and player in one — the default app's main screenstationId, tourId, enableSwitchAudioOutput
sc-page-stationOne station with its playerstationId, tourId, enableBackButton, defaultBackButtonHref, enableSwitchAudioOutput
sc-page-multi-audio-stationOne station with several tracks to choose fromstationId, tourId, defaultBackButtonHref, enableSwitchAudioOutput
sc-page-selectionThe number keypadmaxLength (2), tourId
sc-page-station-listA plain list of stationstourId
sc-page-station-image-listStations as a list of imagestourId
sc-page-tabbed-station-listsc-page-station-image-list and sc-page-station-list as two tabstourId
sc-page-tour-listThe tours, with cover images and descriptions—
sc-page-mapStations on a mapmapBounds, mapStyleUrl or tileUrlTemplate, mapAttribution, tourId, enableBackButton, defaultBackButtonHref

Station lists are sorted by number. The list pages and sc-page-stations sort a tour's stations the same way; sc-page-station and sc-page-map keep the tour's order.

sc-page-stations and sc-page-station make two assumptions about the content:

  • Each station has exactly one audio track. The player's playlist has one item per audio asset, and the pages pass a station's position in the list as the playlist index. A station with no audio, or with several, shifts every station after it onto the wrong track. Stations with several tracks belong on sc-page-multi-audio-station.
  • sc-page-stations needs two images per station. It shows the active station's first image large, and each station's second image on its card in the list; a station with a single image fails to render. sc-page-station shows the first image only. The image slideshow is on sc-page-multi-audio-station.

Facade slices​

Pages do not import @smartcompanion/services. Each declares the part of the facade it uses as a type — PageLoadingFacade, StationListFacade, PageStationFacade and so on, exported from the package — and ServiceFacade satisfies all of them structurally. A test, or an app with its own services, can pass anything that has the same shape.

Building blocks​

The pages are made of five smaller components, which an app can use on its own:

ComponentWhat it isPropertiesEvents
sc-player-controlsPlay/pause, previous, next and a seek barplaying, position, duration, disabledplayPause, prev, next, startPositionChange, positionChange, endPositionChange
sc-station-iconA station number with its listening-progress ringcollected, collectedPercent, size, lowerLimitPercent, upperLimitPercent—
sc-numpadA numeric keypadfullnumber, delete, confirm
sc-image-slideshowA swipeable image galleryimages—
sc-marqueeText that scrolls when it does not fitactive—

Texts​

Pages look up their interface texts through the facade's __(), which reads the texts of the loaded content. Provide each of these keys in every language the app offers:

KeyPage
no-internet, try-againsc-page-error
enter-pin, pin-errorsc-page-pin
menu-selectionsc-page-selection
menu-overview, start-toursc-page-tour-list
station-listsc-page-station-list, sc-page-station-image-list
viewsc-page-station-image-list
menu-mapsc-page-map

The list is exported as the TranslationKey type. Keys an app uses in its own components — the default app's menu, for instance — are not restricted by it. A missing key shows as the key itself; see UI texts.

Accessibility​

Every component and page is checked with axe-core in the library's tests, and the issues each has today are recorded — see Development. The known ones, in short:

  • Images have no alternative text — station pictures, the slideshow, the loading page's logo.
  • Icon-only buttons have no accessible name — the player's previous, play/pause and next, the numpad's delete and confirm, the menu button, and the multi-track page's info button. Their icons are not hidden from screen readers either.
  • The tabbed station list's tabs have no accessible names, and the image list's list structure is incomplete.
  • The loading spinner has no accessible name.

sc-page-language, sc-page-station-list, sc-page-map, sc-marquee and sc-station-icon have no known violations. Contrast is checked against Ionic's default palette only — an app with its own colours checks its own.

Theming​

The components read the app's palette from CSS custom properties: the --sc-* properties described in Colors, and Ionic's --ion-color-* that those map onto. Define them in the app's global stylesheet — the audioguide app's app.scss is a complete example, for light and dark mode.

Maps​

sc-page-map renders with MapLibre GL. It needs mapBounds — top-left and bottom-right latitude/longitude — and exactly one of mapStyleUrl, for vector tiles, or tileUrlTemplate, a raster {z}/{y}/{x} template.

MapLibre draws in a web worker loaded from a file of its own, and that file cannot be bundled into the page's code. sc-page-map therefore ships the worker as a component asset, which a consuming Stencil app copies to build/assets/maplibre-gl-worker.mjs. Two consequences for apps:

  • Do not filter the component's assets out of the build — without the worker every map logs "Worker failed to load".
  • Precache the worker for offline use. An app with a service worker adds build/assets/maplibre-gl-worker.mjs to its precache patterns, or the map fails offline.

sc-page-map points MapLibre at the shipped worker only if no worker URL is set yet, so an app that serves the worker itself and calls setWorkerUrl() first keeps its own.

maplibre-gl is pinned to an exact version in the library, so the map code and the shipped worker always match.