All files / src/ui/stores tiles.ts

86.84% Statements 33/38
91.66% Branches 11/12
100% Functions 3/3
86.84% Lines 33/38

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90                                            2x 2x 2x                   2x     2x 253x 253x 253x 253x 253x 253x 253x                       1384x 1384x               1384x     2x   2050x 2050x 2050x 2050x 2050x 2050x 2050x 2050x     2050x 2050x 2050x 2050x 2050x 2050x 2050x 2050x 2050x  
/**
 * The row of stores, as tiles: one square per store, the mark on it, the name
 * and one line under it.
 *
 * Two sizes of the same thing. **Large**, on the landing page, where the row is
 * the second of the two things a reader can do — load files, or connect to a
 * store — and each tile earns a sentence. **Small**, along the drawer's head,
 * where the row is how a reader switches store without closing it, and the
 * name is enough.
 *
 * The last two tiles are always the doors: a **track hub**, and a store file by
 * URL — so a lab's own catalogue and somebody else's hub sit in the same row as
 * the shipped ones and are reached the same way. The hub comes first of the two
 * because far more of the data in the world is behind one.
 *
 * **Both doors are dashed** (`.store-tile-add`), which is the one cue in the row
 * that a tile asks for an address rather than opening a catalogue that is
 * already there. Only the custom one was, and the hub door — wearing UCSC's own
 * mark, as every remembered hub does — was a solid tile a reader could
 * reasonably expect to browse.
 */
 
import { el } from '../dom'
import { storeMark } from './marks'
import { customStore, hubStoreFront, type StoreFront } from './registry'
 
export interface StoreTilesProps {
  fronts: readonly StoreFront[]
  size: 'large' | 'small'
  /** The store that is open, drawn as such. */
  current?: string | null
  onPick(id: string): void
}
 
const MARK_SIZE = { large: 52, small: 26 } as const
 
/** The row. Rebuilt by the caller when the list changes — it is at most a handful. */
export function storeTiles(props: StoreTilesProps): HTMLElement {
  const fronts = [...props.fronts, hubStoreFront(), customStore()]
  return el(
    'div',
    { class: `store-tiles store-tiles-${props.size}`, attrs: { role: 'list' } },
    ...fronts.map((front) => tile(front, props)),
  )
}
 
/**
 * The line under a tile's name: short enough for the two lines it is given.
 *
 * A shipped store writes its own (`registry.ts`), because the first fifty
 * characters of a sentence are a truncation rather than a shorter sentence. A
 * store the reader remembered has no such line — its blurb is the URL it came
 * from — and a URL is the one string here that can be arbitrarily long, so what
 * the tile shows is the **host**, which is the part of it that says whose shelf
 * this is. The whole URL is on the tile's `title` either way.
 */
function tagline(front: StoreFront): string {
  if (front.tagline !== undefined) return front.tagline
  try {
    return new URL(front.blurb).host
  } catch {
    // Not a URL: a blurb written somewhere this module does not know about.
    // Shown as it is, and the row grows a line rather than losing a word.
    return front.blurb
  }
}
 
/** The two tiles that take an address rather than opening a catalogue. */
const DOORS = new Set(['custom', 'hub'])
 
function tile(front: StoreFront, props: StoreTilesProps): HTMLElement {
  const on = props.current === front.id
  const node = el(
    'button',
    {
      class: `store-tile${DOORS.has(front.id) ? ' store-tile-add' : ''}${on ? ' is-on' : ''}`,
      type: 'button',
      title: front.blurb,
      // The store's id, so the tutorial can point at one tile (§30) without
      // reading a label that a later build is free to reword.
      attrs: { role: 'listitem', 'aria-pressed': String(on), 'data-store': front.id },
    },
    storeMark(front.mark, MARK_SIZE[props.size]),
    el('span', { class: 'store-tile-name', text: front.label }),
    props.size === 'large' ? el('span', { class: 'store-tile-blurb', text: tagline(front) }) : null,
  )
  node.addEventListener('click', () => props.onPick(front.id))
  return node
}