npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@3halves-labs/score-trivia-widget

v0.5.1

Published

Embeddable widget for the Ruck & Recall trivia session game (/api/trivia/*) — web component + React, endless-run play with server-side scoring

Readme

@3halves-labs/score-trivia-widget

An embeddable widget for the Ruck & Recall trivia session game (/api/trivia/*). It drives the stateful, endless-run loop — open a session → serve a question → collect an answer → submit (server-scored, answer key withheld) → feedback → next / finish → summary. Karma is awarded asynchronously to the fan's loyalty balance.

On a wrong answer the feedback reveals the correct answer and the explanation the server returns (correctAnswer + explanation on the submit response) — so the widget teaches, not just scores.

Distinct from @3halves-labs/score-interactions, which renders the one-shot score.game "interactions" path (/api/interactions/:id). This one plays the R&R session game.

Ships three ways to embed, mirroring the interactions widget:

1. Web component (script-tag drop-in)

<script type="module" src="https://…/score-trivia-widget.esm.js"></script>
<score-trivia track="learning_the_game"
              endpoint="https://api.urc.anything-loyalty.3halves-labs.com"></score-trivia>
import { defineTriviaElement } from '@3halves-labs/score-trivia-widget';
defineTriviaElement(); // registers <score-trivia>

The trivia API needs a fan bearer token, so set a .client with a token provider (a bare endpoint attribute is unauthenticated):

import { createHttpClient } from '@3halves-labs/score-trivia-widget';
const elm = document.querySelector('score-trivia');
elm.client = createHttpClient({ endpoint, getToken: () => fanToken });
elm.addEventListener('complete', (e) => console.log(e.detail)); // { answered, correct, score, state }

2. Vanilla renderer

import { renderTriviaSession, createHttpClient } from '@3halves-labs/score-trivia-widget';

const ctl = renderTriviaSession(container, {
  client: createHttpClient({ endpoint, getToken: () => fanToken }),
  track: 'learning_the_game',
  questionTimeMs: 20_000,          // per-question countdown (a question's own timeLimit wins)
  autoStart: true,                 // false → show a Start button instead
  capabilities: undefined,         // what to tell the server this client can render
  theme: { vars: { primary: '#e4002b' } },
  onAnswer:   (r) => {},           // { correct, score }
  onComplete: (r) => {},           // { answered, correct, score, state }
});
// ctl.finish() ends the session early; ctl.setTheme(t) restyles in place;
// ctl.destroy() tears down.

3. React

import { TriviaWidget } from '@3halves-labs/score-trivia-widget/react';
import { createHttpClient } from '@3halves-labs/score-trivia-widget';

<TriviaWidget client={createHttpClient({ endpoint, getToken })} track="learning_the_game" useDefaultTheme />

Question types

Fifteen types are advertised on session create. Twelve are text (RENDERABLE_QUESTION_TYPES), each with a control in answerControls.ts; the other three lines below are the image pair, IMAGE_QUESTION_TYPES.

multiple_choice  true_false  fill_blank  odd_one_out  multi_select
two_truths_one_lie  higher_lower  numeric_estimate  cloze_passage
ordering  matching  categorise
image_identify        a headshot and four names   (grades like multiple_choice)
image_true_false      a headshot and one claim    (grades like true_false)

The image types come with a catch worth knowing: the server drops them unless the client advertises both the type and media: ['image']. defaultCapabilities() sends the pair, so they work out of the box. The picture arrives as a signed, time-boxed imageUrl on the API host — never the upstream asset URL, because the filename usually contains the player's name — and createHttpClient resolves it to an absolute URL before the renderer draws the <img>.

Upgrading from 0.3: this is a behaviour change under a caret range. 0.3 advertised the twelve text types; 0.4 advertises the image types too, by default, so an existing embedder starts being served headshot questions with no code change. Keep the old behaviour with capabilities: { types: [...RENDERABLE_QUESTION_TYPES] } (or the no-images attribute). Decline them deliberately if you have an accessibility obligation: as below, an image question cannot be answered by a screen-reader user, and that limitation now arrives by default.

To decline pictures, narrow the capabilities:

import { RENDERABLE_QUESTION_TYPES } from '@3halves-labs/score-trivia-widget';
renderTriviaSession(el, { client, capabilities: { types: [...RENDERABLE_QUESTION_TYPES] } });
<score-trivia no-images endpoint="…"></score-trivia>

Two caveats. A signed URL expires (an hour by default); a load failure degrades to a short note and the question stays answerable, but the fix is to re-fetch the question. And an image question is not answerable by a screen-reader useralt is a fixed generic string on purpose, since anything descriptive would give the answer away, and the spec's accessible equivalent is a server-side gap. A host with a hard a11y requirement should decline the image types.

Styling

Four layers, smallest first. All of them key off the part names — every element carries part="…", data-st="…" and a matching .st-… class, and those names are stable across releases.

1 · CSS variables. The default theme colours everything from --st-*, and custom properties pierce shadow DOM, so a page stylesheet reaches inside <score-trivia>:

score-trivia { --st-primary: #e4002b; --st-radius: 16px; }
score-trivia::part(option) { text-transform: uppercase; }

2 · A theme object, for the same thing from JS plus two things CSS alone cannot do here:

const ctl = renderTriviaSession(el, {
  client,
  theme: {
    vars: { primary: '#e4002b', radius: '16px' },   // → --st-primary, --st-radius
    css: '.st-stem { letter-spacing: -.02em }',     // injected after the default sheet
    classNames: { option: 'rounded-xl hover:bg-slate-100', stem: 'text-2xl font-bold' },
  },
});

3 · classNames is the Tailwind hook — a part → class-string map, applied as elements are created, including the markup a control rebuilds mid-question.

Your classes win without !important. A utility ties with the built-in class on specificity and takes the tie on document order, because the shipped sheet is injected first in the head — anything the page links or injects afterwards comes later and wins. The same holds for theme.css (injected last, into the same root) and for your own .st-* rules.

The shipped rules use ordinary class specificity rather than :where(), which 0.5.0 tried. Zero specificity meant a host's button { … } reset — or Tailwind's preflight, which every Tailwind app has — outranked the widget's own button styling, so in the light DOM the buttons rendered as bare transparent boxes. A class beats an element selector; injection order handles the rest.

Add replaceClasses: true to drop the built-in st-* classes on the parts you name and start from bare markup — the deterministic option if you would rather not depend on order at all. State classes (st-selected, the feedback verdict) still land, so a utility theme does not lose the selected state.

Tailwind and the shadow DOM: a page's compiled Tailwind sheet does not reach inside <score-trivia>. Use renderTriviaSession or the React wrapper (both render in the light DOM), or pass compiled utilities via theme.css.

4 · Restyle a live widget — no re-mount, so an in-flight question survives:

ctl.setTheme({ vars: { primary: '#0ea5e9' } });        // vanilla renderer
document.querySelector('score-trivia').setTheme({ … }); // web component
<TriviaWidget theme={theme} … />                        // React: prop changes apply in place

applyTheme(element, theme) applies vars + css to an already-rendered widget on its own and returns a disposer. Set no-default-theme (web component) or omit useDefaultTheme (React) to skip the shipped sheet entirely.

The full part list is the TriviaPart union in theme.ts.

Dev / offline

createMockClient(questions, grade?) serves a fixed question list in an endless loop and grades locally — for building UI without a backend:

import { renderTriviaSession, createMockClient } from '@3halves-labs/score-trivia-widget';
renderTriviaSession(el, { client: createMockClient(myQuestions, (q, a) => a === answerKey[q.id]) });

Scripts

npm run build      # tsup → CJS + ESM + d.ts (index + /react)
npm run typecheck  # tsc --noEmit
npm test           # vitest (jsdom)

The session flow is powered by the same /api/trivia/* routes wrapped by @3halves-labs/score-loyalty-sdk (≥1.6.0); this package is the UI on top.