Skip to main content
All Examples

UI Showcase

Interactive showcase of every Foldkit UI component with styled examples, routing, and component state management.

UI Components
Routing
/
import clsx from 'clsx'
import {
  Effect,
  Equivalence,
  Match as M,
  Schema as S,
  Stream,
  pipe,
} from 'effect'
import { Calendar, Command, Route, Runtime, Subscription, Ui } from 'foldkit'
import { Html, html } from 'foldkit/html'
import { m } from 'foldkit/message'
import { load, pushUrl } from 'foldkit/navigation'
import { literal, r } from 'foldkit/route'
import { evo } from 'foldkit/struct'
import { Url, toString as urlToString } from 'foldkit/url'

import * as Icon from './icon'
import { uiInit } from './init'
import {
  GotDragAndDropDemoMessage,
  GotMobileMenuDialogMessage,
  GotSliderRatingDemoMessage,
  GotSliderVolumeDemoMessage,
  UiMessage,
} from './message'
import { UiModel } from './model'
import { uiUpdate } from './update'
import * as View from './view'

// ROUTE

const HomeRoute = r('Home')
const ButtonRoute = r('Button')
const CalendarRoute = r('Calendar')
const CheckboxRoute = r('Checkbox')
const ComboboxRoute = r('Combobox')
const DatePickerRoute = r('DatePicker')
const DialogRoute = r('Dialog')
const DisclosureRoute = r('Disclosure')
const DragAndDropRoute = r('DragAndDrop')
const FieldsetRoute = r('Fieldset')
const FileDropRoute = r('FileDrop')
const InputRoute = r('Input')
const ListboxRoute = r('Listbox')
const MenuRoute = r('Menu')
const PopoverRoute = r('Popover')
const RadioGroupRoute = r('RadioGroup')
const SelectRoute = r('Select')
const SliderRoute = r('Slider')
const SwitchRoute = r('Switch')
const TabsRoute = r('Tabs')
const TextareaRoute = r('Textarea')
const ToastRoute = r('Toast')
const TooltipRoute = r('Tooltip')
const AnimationRoute = r('Animation')
const NotFoundRoute = r('NotFound', { path: S.String })

const AppRoute = S.Union(
  HomeRoute,
  ButtonRoute,
  CalendarRoute,
  CheckboxRoute,
  ComboboxRoute,
  DatePickerRoute,
  DialogRoute,
  DisclosureRoute,
  DragAndDropRoute,
  FieldsetRoute,
  FileDropRoute,
  InputRoute,
  ListboxRoute,
  MenuRoute,
  PopoverRoute,
  RadioGroupRoute,
  SelectRoute,
  SliderRoute,
  SwitchRoute,
  TabsRoute,
  TextareaRoute,
  ToastRoute,
  TooltipRoute,
  AnimationRoute,
  NotFoundRoute,
)

type AppRoute = typeof AppRoute.Type

const homeRouter = pipe(Route.root, Route.mapTo(HomeRoute))
const buttonRouter = pipe(literal('button'), Route.mapTo(ButtonRoute))
const calendarRouter = pipe(literal('calendar'), Route.mapTo(CalendarRoute))
const checkboxRouter = pipe(literal('checkbox'), Route.mapTo(CheckboxRoute))
const comboboxRouter = pipe(literal('combobox'), Route.mapTo(ComboboxRoute))
const datePickerRouter = pipe(
  literal('date-picker'),
  Route.mapTo(DatePickerRoute),
)
const dialogRouter = pipe(literal('dialog'), Route.mapTo(DialogRoute))
const disclosureRouter = pipe(
  literal('disclosure'),
  Route.mapTo(DisclosureRoute),
)
const dragAndDropRouter = pipe(
  literal('drag-and-drop'),
  Route.mapTo(DragAndDropRoute),
)
const fieldsetRouter = pipe(literal('fieldset'), Route.mapTo(FieldsetRoute))
const fileDropRouter = pipe(literal('file-drop'), Route.mapTo(FileDropRoute))
const inputRouter = pipe(literal('input'), Route.mapTo(InputRoute))
const listboxRouter = pipe(literal('listbox'), Route.mapTo(ListboxRoute))
const menuRouter = pipe(literal('menu'), Route.mapTo(MenuRoute))
const popoverRouter = pipe(literal('popover'), Route.mapTo(PopoverRoute))
const radioGroupRouter = pipe(
  literal('radio-group'),
  Route.mapTo(RadioGroupRoute),
)
const selectRouter = pipe(literal('select'), Route.mapTo(SelectRoute))
const sliderRouter = pipe(literal('slider'), Route.mapTo(SliderRoute))
const switchRouter = pipe(literal('switch'), Route.mapTo(SwitchRoute))
const tabsRouter = pipe(literal('tabs'), Route.mapTo(TabsRoute))
const textareaRouter = pipe(literal('textarea'), Route.mapTo(TextareaRoute))
const toastRouter = pipe(literal('toast'), Route.mapTo(ToastRoute))
const tooltipRouter = pipe(literal('tooltip'), Route.mapTo(TooltipRoute))
const animationRouter = pipe(literal('animation'), Route.mapTo(AnimationRoute))

const routeParser = Route.oneOf(
  buttonRouter,
  calendarRouter,
  checkboxRouter,
  comboboxRouter,
  datePickerRouter,
  dialogRouter,
  disclosureRouter,
  dragAndDropRouter,
  fieldsetRouter,
  fileDropRouter,
  inputRouter,
  listboxRouter,
  menuRouter,
  popoverRouter,
  radioGroupRouter,
  selectRouter,
  sliderRouter,
  switchRouter,
  tabsRouter,
  textareaRouter,
  toastRouter,
  tooltipRouter,
  animationRouter,
  homeRouter,
)

const urlToAppRoute = Route.parseUrlWithFallback(routeParser, NotFoundRoute)

// MODEL

const Model = S.Struct({
  route: AppRoute,
  uiModel: UiModel,
})

type Model = typeof Model.Type

// MESSAGE

const CompletedNavigateInternal = m('CompletedNavigateInternal')
const CompletedLoadExternal = m('CompletedLoadExternal')
const ClickedLink = m('ClickedLink', {
  request: Runtime.UrlRequest,
})
const ChangedUrl = m('ChangedUrl', { url: Url })
const GotUiMessage = m('GotUiMessage', {
  message: UiMessage,
})

export const Message = S.Union(
  CompletedNavigateInternal,
  CompletedLoadExternal,
  ClickedLink,
  ChangedUrl,
  GotUiMessage,
)
export type Message = typeof Message.Type

// COMMAND

const NavigateInternal = Command.define(
  'NavigateInternal',
  CompletedNavigateInternal,
)
const LoadExternal = Command.define('LoadExternal', CompletedLoadExternal)

// INIT

const Flags = S.Struct({
  today: Calendar.CalendarDate,
})

type Flags = typeof Flags.Type

const flags: Effect.Effect<Flags> = Effect.gen(function* () {
  const today = yield* Calendar.today.local
  return { today }
})

const init: Runtime.RoutingProgramInit<Model, Message, Flags> = (
  flags: Flags,
  url: Url,
) => {
  const [initialUiModel, uiCommands] = uiInit(flags.today)

  return [
    {
      route: urlToAppRoute(url),
      uiModel: initialUiModel,
    },
    uiCommands.map(
      Command.mapEffect(Effect.map(message => GotUiMessage({ message }))),
    ),
  ]
}

// UPDATE

const toUiMessage = (message: typeof UiMessage.Type): Message =>
  GotUiMessage({ message })

const toMobileMenuDialogMessage = (message: Ui.Dialog.Message): Message =>
  GotUiMessage({ message: GotMobileMenuDialogMessage({ message }) })

const update = (
  model: Model,
  message: Message,
): readonly [Model, ReadonlyArray<Command.Command<Message>>] =>
  M.value(message).pipe(
    M.withReturnType<
      readonly [Model, ReadonlyArray<Command.Command<Message>>]
    >(),
    M.tagsExhaustive({
      CompletedNavigateInternal: () => [model, []],
      CompletedLoadExternal: () => [model, []],

      ClickedLink: ({ request }) =>
        M.value(request).pipe(
          M.tagsExhaustive({
            Internal: ({
              url,
            }): [
              Model,
              ReadonlyArray<Command.Command<typeof CompletedNavigateInternal>>,
            ] => [
              model,
              [
                NavigateInternal(
                  pushUrl(urlToString(url)).pipe(
                    Effect.as(CompletedNavigateInternal()),
                  ),
                ),
              ],
            ],
            External: ({
              href,
            }): [
              Model,
              ReadonlyArray<Command.Command<typeof CompletedLoadExternal>>,
            ] => [
              model,
              [
                LoadExternal(
                  load(href).pipe(Effect.as(CompletedLoadExternal())),
                ),
              ],
            ],
          }),
        ),

      ChangedUrl: ({ url }) => {
        const [closedDialog, closeDialogCommands] = Ui.Dialog.update(
          model.uiModel.mobileMenuDialog,
          Ui.Dialog.Closed(),
        )

        return [
          evo(model, {
            route: () => urlToAppRoute(url),
            uiModel: uiModel =>
              evo(uiModel, {
                mobileMenuDialog: () => closedDialog,
              }),
          }),
          closeDialogCommands.map(
            Command.mapEffect(
              Effect.map(message => toMobileMenuDialogMessage(message)),
            ),
          ),
        ]
      },

      GotUiMessage: ({ message }) => {
        const [nextUiModel, uiCommands] = uiUpdate(model.uiModel, message)

        return [
          evo(model, { uiModel: () => nextUiModel }),
          uiCommands.map(
            Command.mapEffect(Effect.map(message => GotUiMessage({ message }))),
          ),
        ]
      },
    }),
  )

// VIEW

const {
  a,
  button,
  div,
  h1,
  header,
  keyed,
  li,
  main,
  nav,
  p,
  span,
  ul,
  AriaExpanded,
  AriaLabel,
  Autofocus,
  Class,
  Href,
  OnClick,
  Tabindex,
} = html<Message>()

type NavItem = Readonly<{
  label: string
  routeTag: string
  href: string
}>

const NAV_ITEMS: ReadonlyArray<NavItem> = [
  { label: 'Animation', routeTag: 'Animation', href: animationRouter() },
  { label: 'Button', routeTag: 'Button', href: buttonRouter() },
  { label: 'Calendar', routeTag: 'Calendar', href: calendarRouter() },
  { label: 'Checkbox', routeTag: 'Checkbox', href: checkboxRouter() },
  { label: 'Combobox', routeTag: 'Combobox', href: comboboxRouter() },
  { label: 'Date Picker', routeTag: 'DatePicker', href: datePickerRouter() },
  { label: 'Dialog', routeTag: 'Dialog', href: dialogRouter() },
  { label: 'Disclosure', routeTag: 'Disclosure', href: disclosureRouter() },
  {
    label: 'Drag and Drop',
    routeTag: 'DragAndDrop',
    href: dragAndDropRouter(),
  },
  { label: 'Fieldset', routeTag: 'Fieldset', href: fieldsetRouter() },
  { label: 'File Drop', routeTag: 'FileDrop', href: fileDropRouter() },
  { label: 'Input', routeTag: 'Input', href: inputRouter() },
  { label: 'Listbox', routeTag: 'Listbox', href: listboxRouter() },
  { label: 'Menu', routeTag: 'Menu', href: menuRouter() },
  { label: 'Popover', routeTag: 'Popover', href: popoverRouter() },
  { label: 'Radio Group', routeTag: 'RadioGroup', href: radioGroupRouter() },
  { label: 'Select', routeTag: 'Select', href: selectRouter() },
  { label: 'Slider', routeTag: 'Slider', href: sliderRouter() },
  { label: 'Switch', routeTag: 'Switch', href: switchRouter() },
  { label: 'Tabs', routeTag: 'Tabs', href: tabsRouter() },
  { label: 'Textarea', routeTag: 'Textarea', href: textareaRouter() },
  { label: 'Toast', routeTag: 'Toast', href: toastRouter() },
  { label: 'Tooltip', routeTag: 'Tooltip', href: tooltipRouter() },
]

const navLinkClassName = (isActive: boolean): string =>
  clsx(
    'block px-3 py-1.5 rounded-md text-sm transition-colors',
    isActive
      ? 'bg-accent-100 text-accent-700'
      : 'text-gray-700 hover:bg-gray-200',
  )

const mobileNavLinkClassName = (isActive: boolean): string =>
  clsx(
    'block px-4 py-2.5 rounded-md text-base transition-colors',
    isActive
      ? 'bg-accent-100 text-accent-700'
      : 'text-gray-700 hover:bg-gray-200',
  )

const sidebarView = (currentRoute: AppRoute): Html =>
  nav(
    [
      Class(
        'hidden md:flex w-56 shrink-0 border-r border-gray-200 bg-gray-50 p-4 flex-col',
      ),
    ],
    [
      div(
        [Class('mb-6')],
        [
          a(
            [Href(homeRouter()), Class('block')],
            [h1([Class('text-lg font-bold text-gray-900')], ['Foldkit UI'])],
          ),
          span([Class('text-xs text-gray-500')], ['Component Showcase']),
        ],
      ),
      ul(
        [Class('flex flex-col gap-0.5')],
        NAV_ITEMS.map(navItem =>
          li(
            [],
            [
              a(
                [
                  Href(navItem.href),
                  Class(
                    navLinkClassName(currentRoute._tag === navItem.routeTag),
                  ),
                ],
                [navItem.label],
              ),
            ],
          ),
        ),
      ),
    ],
  )

const mobileMenuContent = (currentRoute: AppRoute): Html =>
  div(
    [Class('flex flex-col h-full')],
    [
      div(
        [
          Class(
            'flex items-center justify-between border-b border-gray-200 px-4 py-3',
          ),
        ],
        [
          a(
            [Href(homeRouter()), Class('block')],
            [
              div(
                [Class('flex flex-col')],
                [
                  span(
                    [Class('text-base font-bold text-gray-900')],
                    ['Foldkit UI'],
                  ),
                  span(
                    [Class('text-xs text-gray-500')],
                    ['Component Showcase'],
                  ),
                ],
              ),
            ],
          ),
          button(
            [
              Class(
                'p-2 rounded-md hover:bg-gray-200 transition text-gray-700 cursor-pointer',
              ),
              AriaLabel('Close menu'),
              OnClick(toMobileMenuDialogMessage(Ui.Dialog.Closed())),
            ],
            [Icon.xMark('w-6 h-6')],
          ),
        ],
      ),
      nav(
        [
          Class('flex-1 overflow-y-auto min-h-0 p-4'),
          Tabindex(-1),
          Autofocus(true),
        ],
        [
          ul(
            [Class('flex flex-col gap-0.5')],
            NAV_ITEMS.map(navItem =>
              li(
                [],
                [
                  a(
                    [
                      Href(navItem.href),
                      Class(
                        mobileNavLinkClassName(
                          currentRoute._tag === navItem.routeTag,
                        ),
                      ),
                    ],
                    [navItem.label],
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    ],
  )

const mobileHeaderView = (model: Model): Html =>
  header(
    [
      Class(
        'md:hidden sticky top-0 z-40 flex items-center justify-between border-b border-gray-200 bg-gray-50 px-4 py-3',
      ),
    ],
    [
      a(
        [Href(homeRouter()), Class('block')],
        [
          div(
            [Class('flex flex-col')],
            [
              span(
                [Class('text-base font-bold text-gray-900')],
                ['Foldkit UI'],
              ),
              span([Class('text-xs text-gray-500')], ['Component Showcase']),
            ],
          ),
        ],
      ),
      button(
        [
          Class(
            'p-2 rounded-md hover:bg-gray-200 transition text-gray-700 cursor-pointer',
          ),
          AriaExpanded(model.uiModel.mobileMenuDialog.isOpen),
          AriaLabel('Toggle menu'),
          OnClick(toMobileMenuDialogMessage(Ui.Dialog.Opened())),
        ],
        [Icon.menu('w-6 h-6')],
      ),
    ],
  )

const mobileMenuView = (model: Model): Html =>
  Ui.Dialog.view({
    model: model.uiModel.mobileMenuDialog,
    toParentMessage: toMobileMenuDialogMessage,
    panelContent: mobileMenuContent(model.route),
    panelAttributes: [Class('fixed inset-0 z-[60] bg-white flex flex-col')],
    backdropAttributes: [Class('fixed inset-0 z-[59]')],
    attributes: [Class('md:hidden')],
  })

const homeView = (): Html =>
  div(
    [Class('max-w-2xl')],
    [
      h1(
        [Class('text-2xl md:text-3xl font-bold text-gray-900 mb-4')],
        ['Foldkit UI Showcase'],
      ),
      p(
        [Class('text-gray-600 mb-4')],
        [
          'This is a showcase of every Foldkit UI component. Select a component from the menu to see it in action.',
        ],
      ),
      p(
        [Class('text-gray-600')],
        [
          'Each component is headless — you provide the markup and styling via a callback, and Foldkit handles accessibility, keyboard navigation, and state management.',
        ],
      ),
    ],
  )

const notFoundView = (path: string): Html =>
  div(
    [Class('max-w-2xl')],
    [
      h1(
        [Class('text-2xl md:text-3xl font-bold text-red-600 mb-4')],
        ['404 — Page Not Found'],
      ),
      p([Class('text-gray-600 mb-4')], [`The path "${path}" was not found.`]),
      a(
        [Href(homeRouter()), Class('text-accent-600 hover:underline')],
        ['Go Home'],
      ),
    ],
  )

const contentView = (model: Model): Html =>
  M.value(model.route).pipe(
    M.tagsExhaustive({
      Home: homeView,
      Button: () => View.button(model.uiModel, toUiMessage),
      Calendar: () => View.calendar(model.uiModel, toUiMessage),
      Checkbox: () => View.checkbox(model.uiModel, toUiMessage),
      Combobox: () => View.combobox(model.uiModel, toUiMessage),
      DatePicker: () => View.datePicker(model.uiModel, toUiMessage),
      Dialog: () => View.dialog(model.uiModel, toUiMessage),
      Disclosure: () => View.disclosure(model.uiModel, toUiMessage),
      DragAndDrop: () => View.dragAndDrop(model.uiModel, toUiMessage),
      Fieldset: () => View.fieldset(model.uiModel, toUiMessage),
      FileDrop: () => View.fileDrop(model.uiModel, toUiMessage),
      Input: () => View.input(model.uiModel, toUiMessage),
      Listbox: () => View.listbox(model.uiModel, toUiMessage),
      Menu: () => View.menu(model.uiModel, toUiMessage),
      Popover: () => View.popover(model.uiModel, toUiMessage),
      RadioGroup: () => View.radioGroup(model.uiModel, toUiMessage),
      Select: () => View.select(model.uiModel, toUiMessage),
      Slider: () => View.slider(model.uiModel, toUiMessage),
      Switch: () => View.switch_(model.uiModel, toUiMessage),
      Tabs: () => View.tabs(model.uiModel, toUiMessage),
      Textarea: () => View.textarea(model.uiModel, toUiMessage),
      Toast: () => View.toast(model.uiModel, toUiMessage),
      Tooltip: () => View.tooltip(model.uiModel, toUiMessage),
      Animation: () => View.animation(model.uiModel, toUiMessage),
      NotFound: ({ path }) => notFoundView(path),
    }),
  )

const view = (model: Model): Html =>
  div(
    [Class('flex flex-col md:flex-row min-h-screen bg-white')],
    [
      mobileHeaderView(model),
      mobileMenuView(model),
      sidebarView(model.route),
      main(
        [Class('flex-1 p-4 md:p-8 overflow-auto')],
        [keyed('div')(model.route._tag, [], [contentView(model)])],
      ),
    ],
  )

// SUBSCRIPTION

const sliderFields = Ui.Slider.SubscriptionDeps.fields
const dragAndDropFields = Ui.DragAndDrop.SubscriptionDeps.fields

const SubscriptionDeps = S.Struct({
  sliderRatingPointer: sliderFields['documentPointer'],
  sliderRatingEscape: sliderFields['documentEscape'],
  sliderVolumePointer: sliderFields['documentPointer'],
  sliderVolumeEscape: sliderFields['documentEscape'],
  dragPointer: dragAndDropFields['documentPointer'],
  dragEscape: dragAndDropFields['documentEscape'],
  dragKeyboard: dragAndDropFields['documentKeyboard'],
  autoScroll: dragAndDropFields['autoScroll'],
})

const sliderSubscriptions = Ui.Slider.subscriptions
const dragAndDropSubscriptions = Ui.DragAndDrop.subscriptions

const mapRatingStream = (stream: Stream.Stream<Ui.Slider.Message>) =>
  stream.pipe(
    Stream.map(message =>
      GotUiMessage({ message: GotSliderRatingDemoMessage({ message }) }),
    ),
  )

const mapVolumeStream = (stream: Stream.Stream<Ui.Slider.Message>) =>
  stream.pipe(
    Stream.map(message =>
      GotUiMessage({ message: GotSliderVolumeDemoMessage({ message }) }),
    ),
  )

const mapDragStream = (stream: Stream.Stream<Ui.DragAndDrop.Message>) =>
  stream.pipe(
    Stream.map(message =>
      GotUiMessage({ message: GotDragAndDropDemoMessage({ message }) }),
    ),
  )

const subscriptions = Subscription.makeSubscriptions(SubscriptionDeps)<
  Model,
  Message
>({
  sliderRatingPointer: {
    modelToDependencies: model =>
      sliderSubscriptions.documentPointer.modelToDependencies(
        model.uiModel.sliderRatingDemo,
      ),
    dependenciesToStream: (dependencies, readDependencies) =>
      mapRatingStream(
        sliderSubscriptions.documentPointer.dependenciesToStream(
          dependencies,
          readDependencies,
        ),
      ),
  },
  sliderRatingEscape: {
    modelToDependencies: model =>
      sliderSubscriptions.documentEscape.modelToDependencies(
        model.uiModel.sliderRatingDemo,
      ),
    dependenciesToStream: (dependencies, readDependencies) =>
      mapRatingStream(
        sliderSubscriptions.documentEscape.dependenciesToStream(
          dependencies,
          readDependencies,
        ),
      ),
  },
  sliderVolumePointer: {
    modelToDependencies: model =>
      sliderSubscriptions.documentPointer.modelToDependencies(
        model.uiModel.sliderVolumeDemo,
      ),
    dependenciesToStream: (dependencies, readDependencies) =>
      mapVolumeStream(
        sliderSubscriptions.documentPointer.dependenciesToStream(
          dependencies,
          readDependencies,
        ),
      ),
  },
  sliderVolumeEscape: {
    modelToDependencies: model =>
      sliderSubscriptions.documentEscape.modelToDependencies(
        model.uiModel.sliderVolumeDemo,
      ),
    dependenciesToStream: (dependencies, readDependencies) =>
      mapVolumeStream(
        sliderSubscriptions.documentEscape.dependenciesToStream(
          dependencies,
          readDependencies,
        ),
      ),
  },
  dragPointer: {
    modelToDependencies: model =>
      dragAndDropSubscriptions.documentPointer.modelToDependencies(
        model.uiModel.dragAndDropDemo,
      ),
    dependenciesToStream: (dependencies, readDependencies) =>
      mapDragStream(
        dragAndDropSubscriptions.documentPointer.dependenciesToStream(
          dependencies,
          readDependencies,
        ),
      ),
  },
  dragEscape: {
    modelToDependencies: model =>
      dragAndDropSubscriptions.documentEscape.modelToDependencies(
        model.uiModel.dragAndDropDemo,
      ),
    dependenciesToStream: (dependencies, readDependencies) =>
      mapDragStream(
        dragAndDropSubscriptions.documentEscape.dependenciesToStream(
          dependencies,
          readDependencies,
        ),
      ),
  },
  dragKeyboard: {
    modelToDependencies: model =>
      dragAndDropSubscriptions.documentKeyboard.modelToDependencies(
        model.uiModel.dragAndDropDemo,
      ),
    dependenciesToStream: (dependencies, readDependencies) =>
      mapDragStream(
        dragAndDropSubscriptions.documentKeyboard.dependenciesToStream(
          dependencies,
          readDependencies,
        ),
      ),
  },
  autoScroll: {
    modelToDependencies: model =>
      dragAndDropSubscriptions.autoScroll.modelToDependencies(
        model.uiModel.dragAndDropDemo,
      ),
    equivalence: Equivalence.struct({ isDragging: Equivalence.boolean }),
    dependenciesToStream: (dependencies, readDependencies) =>
      mapDragStream(
        dragAndDropSubscriptions.autoScroll.dependenciesToStream(
          dependencies,
          readDependencies,
        ),
      ),
  },
})

// RUN

const program = Runtime.makeProgram({
  Model,
  Flags,
  flags,
  init,
  update,
  view,
  subscriptions,
  title: model =>
    M.value(model.route).pipe(
      M.tag('Home', () => 'UI Showcase'),
      M.orElse(({ _tag }) => `${_tag} — UI Showcase`),
    ),
  container: document.getElementById('root')!,
  routing: {
    onUrlRequest: request => ClickedLink({ request }),
    onUrlChange: url => ChangedUrl({ url }),
  },
})

Runtime.run(program)