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

@ticketlayer/elements-core

v0.12.0

Published

The headless cores the Ticketlayer Elements render: state, actions, derived values, status, strings, tokens and events, with no runtime in them

Readme

@ticketlayer/elements-core

The headless cores the Ticketlayer Elements render.

One core per element. A core is plain TypeScript: props, a state store, actions, derived values, a status, the copy it renders, the @ticketlayer/theme tokens its harnesses use, the tl* events it emits, the client methods it calls and the runtimes it is meant for. No DOM, no React, no network, and no client of its own: a core is handed one.

A harness renders a core and forwards its events, and decides nothing. @ticketlayer/elements is the web harness, built with Stencil; a React Native harness will render the same cores in an app. That split is the platform plan's section 2.7 and AGENTS.md at the root of this repository.

The shape

import { defineElement } from '@ticketlayer/elements-core';

export const exampleCore = defineElement({
  tag: 'tl-example',
  docs: 'What a host gets from putting this on a page.',
  category: 'purchase',
  runtimes: ['web', 'native'],
  tokens: ['primary', 'spacing-3'],
  sdkMethods: ['events.getTicketTypes'],
  props: { eventId: { type: 'string', attr: 'event-id', required: true, docs: 'Event ID' } },
  events: { tlPicked: { detail: '{ id: string; }', docs: 'Emitted when one is picked' } },
  strings: { empty: 'Nothing on sale' },
  statuses: ['loading', 'error', 'ready'],

  state: () => ({ loading: true, error: null, items: [] }),
  status: ({ state }) => (state.loading ? 'loading' : state.error ? 'error' : 'ready'),
  derive: ({ state, strings }) => ({ caption: state.items.length ? '' : strings.empty }),
  mount: (ctx) => {
    void (async () => {
      const client = await ctx.whenClient();
      if (!ctx.active()) return;
      await ctx.actions.load();
      const off = client.on?.('cart:updated', () => ctx.actions.load());
      if (off) ctx.onTeardown(off);
    })();
  },
  actions: (ctx) => ({
    async load() {
      const items = await ctx.client().getEventsManager().getTicketTypes(ctx.props.eventId, '');
      ctx.setState({ items, loading: false });
    },
  }),
});

A harness instantiates it, renders view, and forwards:

const element = createElement(exampleCore, {
  client: () => getSDK(),
  clientReady: () => whenSDK(),
  emit: (event, detail) => this.forward(event, detail),
});
element.subscribe((view) => (this.view = view));

view is { status, props, state, strings, derived }, recomputed after every change. The first three lines of a harness's render are a switch on status.

Mount and teardown

mount is where a core says what happens when a harness puts the element on screen, and hands back what undoes it (TKT-197). Loading and subscribing belong here and not in a harness: a harness that decided when to load, or who owns a subscription, would have to decide it again for every runtime, and the two would drift. The web harness calls mount() from connectedCallback and its teardown from disconnectedCallback; a native harness calls the same two from whatever its runtime calls them.

The rules, held by test/mount.test.mjs rather than by convention:

| | | |---|---| | instance.mount() | Runs the core's mount and returns that mount's teardown. A core with no mount still mounts, and still returns a teardown that does nothing. | | The teardown | Runs everything registered with ctx.onTeardown, in reverse order, plus whatever mount returned. Idempotent: calling it a second time does nothing. A teardown from an earlier mount does nothing at all. | | Mounting while mounted | Tears the live mount down first. At most one mount is live, so a remount cannot leave two subscriptions behind. | | destroy() | Terminal. Runs the live mount's teardown, clears the timers and the listeners; mounting afterwards does nothing. A harness that only ever destroys still leaks nothing. | | ctx.active() | False once this mount has been torn down or the instance destroyed. A mount that awaits anything checks it before touching the state. | | ctx.onTeardown(fn) after teardown | Runs fn immediately, so a subscription made after an await cannot outlive the mount that asked for it. | | ctx.whenClient() | Resolves with the host's client once clientReady says it is up, and rejects when the host never produces one. On an action too, not only a mount: an action a buyer triggers can be the first thing that needs the client (tl-promo-code redeems a code without reading anything first), and client() would throw on a page that is still starting up. |

On the web, destroy() is deliberately not called from disconnectedCallback: Stencil calls that for a move as well as a removal, and destroying there would leave a re-inserted element frozen. The teardown is what pairs with the mount.

What a core is handed besides the client

ctx.storage is the runtime's key/value store: strings in, strings out, synchronous, and allowed to fail silently. It exists because a core is occasionally allowed to remember something between visits - tl-cart-badge keeps the last item count so the badge does not flash empty on the next page - and where that is written down is per runtime. The web harness passes a localStorage wrapper; a harness that passes nothing gets NO_STORAGE, whose reads come back null and whose writes go nowhere, so a core never has to ask whether it has somewhere to write.

The rule is the same as for the client: the core says what it remembers and when, the harness says where.

ctx.query is the second seam, and the same bargain. Two elements are the far end of a link somebody follows: tl-login exchanges the ?code= a magic-link email lands on, and tl-order-confirmation finds its order in ?orderId= and reads ?redirect_status= to learn whether a payment provider sent the buyer back on a success. Where those parameters live is per runtime - the web has window.location.search, a native harness has its deep-link router - so the core says which parameter it reads and when, and the harness says where they come from. It reads one name at a time, synchronously, null when it is not there, and a core never navigates: where the address points is the host's. The one write a core may ask for is consume(name), for a parameter that is a secret and single use: it reads the parameter and has the harness take it out of where it came from (TKT-371). tl-login consumes its ?code=, so a spent sign-in code does not stay in the browser history or leave the page in a Referer; on the web webQuery does it with history.replaceState, keeping the path, every other parameter as written, the hash and the history state object, and writes nothing when the parameter is not there, so a host that re-renders on a URL change cannot loop. tl-order-confirmation only reads: an order id is not a credential (an order is read with the order-access token Live holds, or the buyer's own sign-in), and leaving it lets a refresh find the order again. consume is optional for a harness, so one written against the read-only seam still compiles; createElement gives it a consume that reads through get and removes nothing, and NO_QUERY answers null to both. The web harness passes webQuery; a harness that passes nothing gets NO_QUERY, and both elements fall through to what they do on a page with no link in it.

Analytics

ctx.track(name, props?, ids?) records a first-party analytics event (TKT-591): the core says what happened, and the client's analytics, reached through the connector, is where it goes; a client with none records nothing, and a throw there never reaches the element. Every event carries the element's own id (tl-cart#2, counted per tag in this runtime) and the eventId, occurrenceId and venueId props the element was given; the cart id is added by the client. instance.seen() is the harness saying the element has been on screen, which only a runtime can know (an IntersectionObserver on the web), and the core records element.view once per instance however often it is told.

What the cores record: cart.add beside tl-ticket-selector's tlAddToCart; seat.select when tl-seat-explorer holds a seat; element.open when tl-buy-modal has read its event, tl-cart-drawer opens and tl-seat-view opens on seats; concierge.open when Concierge opens; checkout.step with step checkout when tl-cart or tl-buy-modal hands over to checkout, complete when the buy modal's checkout is paid, and delivery or payment (with the method) when a checkout section's method is chosen. A core that records declares analytics.track in its sdkMethods. No customer, order or payment id is ever sent.

Props a type cannot say in one word

PropSpec.type is the primitive a prop carries across every runtime. When the harness's type is narrower - a literal union, or a prop that may be undefined - the core writes it out as tsType:

size: { type: 'string', tsType: '"lg" | "md" | "sm"', attr: 'size', default: 'md', docs: 'Size variant' },
icon: { type: 'string', tsType: 'string | undefined', attr: 'icon', docs: 'Custom icon' },

Written out for the same reason EventSpec.detail is: the catalogue is JSON and a type is not. The catalogue emitter compares tsType ?? type against what Stencil built, so a core and its harness cannot disagree about what a prop accepts.

Tokens

tokens names @ticketlayer/theme tokens by their canonical name: primary, not --tl-primary, not the color-primary alias, and never a value. The type is generated from packages/theme/tokens.json, so a token renamed there stops every core that named it compiling, and packages/elements/scripts/check-core-tokens.mjs fails a web harness whose stylesheet reaches for a token its core did not declare, or declares one the stylesheet never uses.

The manifest and the catalogue

manifestOf(core) projects the declarative half of a core to JSON. packages/elements/scripts/build-catalogue.mjs emits elements.catalogue.json from those manifests, so what the docs site, the CLI and a native host read is a projection of the object that runs rather than a second copy kept by hand. Every element has a core; the emitter fails a Stencil component with none, a core that claims the web runtime and no Stencil component answers to its tag, or when the props and events a core declares are not the ones its harness built.

Build and test

pnpm --filter @ticketlayer/elements-core build   # tsc twice: dist/esm and dist/cjs
pnpm --filter @ticketlayer/elements-core test    # builds, then node --test test/*.test.mjs

Both module formats are published. Stencil's rollup build and a native bundler take the ESM; the Jest run behind stencil test --spec and the catalogue emitter take the CommonJS, because Jest will not load ESM from node_modules.

Concierge

src/concierge/ is the headless Concierge the two Concierge cores are built from, exported from the package root so a native harness gets the same rules:

| Module | What | |---|---| | api.ts | createConciergeApi(manager): config, search, contact, verify start and complete, a turn, confirmProposal(proposalId) and closeConversation(conversationId), as typed calls over the connector's transport. Confirm sends an empty body: the confirmation token stays in Live's session and never reaches a core. normaliseConciergeConfig reads Live's config with every switch defaulting off; conciergeConfigOf shares one read per connector across every element on the page. A contact 501 is "not ready yet", not a failure. | | sse.ts | readServerSentEvents and readConciergeTurn: the text/event-stream rules over text chunks split anywhere, typed into the contract's token, tool, card, citation, handover, done and error, plus Live's conversation (the id the widget closes); anything else is skipped. | | sanitise.ts | sanitiseModelText(text, allowedLinkHosts): segments of text and of links whose href was rebuilt from a host on the list. No markup, no images, no javascript: or data:, no user info, no look-alike or non-ASCII host, no port but 443. | | cards.ts | conciergeCardOf(payload): a proposal card from a card event's payload (kind proposal), field by field, as plain text, with integer minor-unit amounts and the confirm card only on a confirm verdict; a confirmationToken in the payload is never read. conciergeCitationOf(payload): a source from a citation event. | | machine.ts | The modes (idle, searching, results, contact, verifying, chatting, handover, error) and the moves between them; error is left only by reset, and a person is reachable from every other mode. | | context.ts | The context bridge: event, occurrence, order and cart ids from a host, kept only when they look like ids. Page context, never authority. |

Prices and availability from the offers engine

src/offers.ts holds the shapes the Live event and listing views carry since TKT-324 (a ticket type's price with its fee lines and fee-inclusive total, its availability, remaining.bucket, limits and unlock; an event's prices range and availability) and the few pure rules the cores need to render them: readers that return null for a value that is absent or malformed; offerSelectionTotals, a selection's total made only of the figures the view printed (a per-ticket fee on every ticket, a per-order fee once); and createCountdownClock, the countdown to a sale opening on ctx.after, which tl-ticket-selector and tl-event-card start when the data they count to arrives and stop from their mount's teardown. Nothing in it computes a fee, a price or a state.

What is here today

All forty elements are split (TKT-11). The five seat map elements, the three best available ones and the two Concierge elements were born split, and so were the thirteen in the tables after this one:

| Core | What it proves | |---|---| | tl-ticket-selector | The reference element: the first split, and the one the shape was proved against (TKT-68, TKT-197). | | tl-cart | The first core to live on a subscription. It binds cart:updated in its own mount and hands the unsubscribe to ctx.onTeardown, and its lifecycle spec is the first end-to-end proof that a remount does not double subscribe (TKT-69). | | tl-cart-badge | The same subscription, plus ctx.storage: the one thing a core is allowed to remember. | | tl-cart-drawer | Two subscriptions on the client's own bus, and a state a host can drive from outside without hearing its own writes back. | | tl-event-card | A core filled either by a prop or by a read it starts itself, and an adapter that is behaviour rather than plumbing. | | tl-promo-code | A core with no mount hook at all: a harness still calls mount() and still gets a teardown. whenClient() on an action is what it needs instead. | | tl-login | The first core to read ctx.query, and the first whose whole shape is a phase machine. A magic-link code is single use, so the query is read once per instance and a remount cannot spend it again. | | tl-my-orders | Two subscriptions on the same bus, and a core whose emptiness is a question about the session rather than about the data: a 401 from the listing is not an error, it is the signed-out view. | | tl-order-confirmation | Three routes to one subject - a prop, the query, and a checkout:completed that arrives after the element is already on screen - and the last element of the eighteen to live on a subscription. | | tl-event-list | The smallest core there is: one read, one re-read when the question changes, and no events at all bar the click it passes on. | | tl-order-tickets | The one core that waits. Its mount owns a widening poll schedule and its teardown is what stops it, so no harness has to remember to. | | tl-buy-tickets-wrapper | The smallest core of the sixteen: no state at all, one action, and three events that exist only because the modal reports back to whoever opened it. Split once its events were renamed to tlComplete, tlError and tlClose (TKT-203). | | tl-checkout-wrapper | The same core pointed at the checkout, and the only one with no subject: the cart is the client's. | | tl-buy-tickets-button | The one whose look depends on its state. It derives the CLASS its sale state renders under and leaves the stylesheet to say which token family that class paints with, because a core cannot map a status onto one - the same bargain tl-event-card's status badge makes. | | tl-occurrence-selector | The biggest, at 883 lines with no spec of its own, and the one split back to front: the characterisation spec was written and committed against the unsplit component FIRST, then passed unchanged against the harness. That is what makes it a split rather than a rewrite, bugs included - it kept opening its calendar on the alphabetically first date, because toDateString() strings were what the component sorted, until a redesign (five views, chosen from how the dates fall) put that right on purpose (TKT-202). It also shows where the line falls: the core decides that auto turns horizontal at 768px, and the harness is what owns the ResizeObserver that measures one. | | tl-buy-modal | The whole buy flow in one frame, made of the Elements a host could place itself: tl-occurrence-selector for the date, tl-ticket-selector in sync-cart mode for the tickets (its steppers write to the cart as they are pressed) and tl-cart over tl-promo-code for the basket, each reading the modal's compact type scale. The core owns only the frame: which step is showing (date, tickets, payment, the confirmation), the two ways the date step can be skipped, what the call to action says about the cart, the hold, and the header's two bands, read from the channel theme's design.modalHeader and overridable per element. Payment is the hosted checkout in its bare layout: the harness mounts its frame through the connector's mountCheckout where the page offers one, and the core hands over with tlCheckout where it does not. | | tl-seat-explorer | The seat map (TKT-463), and the first core that drives a renderer it does not own: the core decides what the map shows (the blocks it needs, the buyer's seats, the price pins, the section it is asked into, which one card is pinned or hovering, whether the map is big enough for a card at all) and the harness hands that to @ticketlayer/seatmap-render and its taps back. Its keyboard walk, its list view and its announcements are the core's (TKT-510), so a native harness inherits them. It talks to its siblings over the bus only. Its 3D mode (TKT-475) is another drawing of the same state: the core offers the toggle only when the publish has a model the connector can read, reads the meshes and the views only when it opens, and goes back to the flat map when a harness says it cannot draw it (model3dFailed). | | tl-seat-card | One seat, and the one core that draws a picture without an image: viewSketchOf turns the seat's TLSV eye into SVG paths, so the card shows the view from the seat in any runtime that can draw a path. Compact (the box office's, TKT-535), the same facts in two lines beside a thumbnail. | | tl-seat-view | The view from one seat or a block (TKT-473), in a box over whatever the page puts it on, and the first core handed images: the connector reads the rendered stills and panorama tiles with the seat map token and gives the core bytes, never a URL, so the token never reaches an element. It decides which picture (the seat's still, the nearest rendered seat's with how far along, or the drawing), the facts in words (seatAngleOf, clearanceOf over the seat in front, the view band only when the venue has signed views off) and which panorama tiles a viewer needs (panoTilesInView, panoLevelFor); the harness only draws them. Anyone opens it over the bus (ui:seat-view-requested), so the card, the explorer and best available need not know where it is. | | tl-map-key | The minimap and the price bands. It hears where the map is (ui:section-focus, ui:seatmap-view) and asks for a section (ui:section-focus-requested) without knowing whether a map is on the page at all. In staff mode it reads the staff state itself and adds the Status tab, the box office map's legend with its counts (TKT-535). | | tl-price-picker | A held seat's ticket type: the optimistic change, put back when the cart refuses, with the seat kept either way. | | tl-best-available | Best available (TKT-471): asks the server, the only place the engine runs because the rank is never public, and lays out what comes back in its order, each option priced at face value from the ticket types its seats are sold on. It hears the party and the preferences on the bus, rings an option's seats on the explorer (ui:seats-highlight) and pins the card on what it takes (ui:seat-pin-requested), so it works wherever on the page it is. In staff mode, one option at a time and "Next best" (TKT-472). | | tl-quantity-picker | How many tickets, as buttons or a keypad, the limit from the seat map pointer. The smallest of the three, and the proof that a choice an element used to own (the buy modal's) travels on the bus rather than through a parent. | | tl-seat-preferences | What a party prefers beyond how many, listing only what differs from the default. A price ceiling is shown at face value and sent as that ticket's fee-inclusive total, because that is what the engine compares. | | tl-ticket | The one with a real design blocker rather than just a decision: rendering the QR needs qrcode, which emits SVG, and downloading the PDF needs an object URL and a synthetic anchor click - neither has a runtime-neutral half. The owner resolved it (TKT-204, "Option C") rather than a new core adding a blob seam: the core holds the ticket's identity, the barcode PAYLOAD (never rendered SVG), the labels and the used/void state a QR greys out under, and an action that only announces the buyer asked for the file; the harness keeps the SDK call, the object URL and anchor click, and the QR rendering. It is the last of the eighteen to split. | | tl-concierge | The first core built on a headless library of its own (src/concierge/): a typed client for Live's /v1/concierge/* over a transport the connector hands it (getConciergeManager(), so the core still never fetches), the turn's server-sent-event reader, a state machine, the context bridge, and the sanitiser every piece of model text passes through - text and links to allowed hosts only, never markup or an image - with confirm cards read from the engine's card event and nothing else. Hidden unless the channel's config turns it on for the placement it is in. | | tl-concierge-slot | The smallest visible core: it renders nothing until the shared config read says its host element's placement is on, which is what let four existing elements host it without changing what they render when Concierge is off. |

Three more were born split, for the checkout's sections (TKT-655, design option 1A, "one page, sections that resolve themselves"):

| Core | What it proves | |---|---| | tl-delivery-picker, tl-payment-picker | Two cores built from one behaviour, src/method-picker.ts: each is its own defineElement call with its own tag, copy and event, and defineMethodPicker is the definition both hand it. Each renders one list of the cart's options, writes its choice through the cart, folds to one line when it has only one answer and hears the other section's choices through the cart:quoted the connector publishes. | | tl-checkout-summary | The quote as rows, never a sum taken here (src/checkout.ts), and the one core that shows it is waiting on someone else's write: between cart:quoting and cart:quoted its total says it is updating rather than show a stale figure. |

Five more were born split for account credit and gift cards (P9, TKT-545). Their shared reading of the API, and the one plain sentence each refusal is shown as, are src/stored-value.ts:

| Core | What it proves | |---|---| | tl-checkout-credit | A checkout section that holds something without spending it. Gift cards and credit are applied to the client's tenders (held in memory, never in storage), each shown with what it covers in the order it will be spent, and spent on the order only when the order is paid (tenders.settle(orderId) in @ticketlayer/live, then the card for what is still due). A gift card's code lives in the core's state only while it is typed and checked; no view, event or label carries it. Hidden (unavailable) when the cart's options leave stored_value out. | | tl-gift-card-shop | A purchase element that is not a ticket: a product, an amount it sells, email or post (post only with a posted order, said beside the option), recipient and message, into the cart; the basket's figure is the quote's totals.giftCards, never a sum taken here. | | tl-gift-card-balance | A rate-limited read met in plain words: the button rests for the wait and comes back by itself. | | tl-my-credit | The signed-in buyer's credit balances, the signed-out view when there is no buyer, and each balance's activity read only when opened and paged by the API's cursor, kept per balance in the core's state. | | tl-refund-request | A form whose answer is a request, not an outcome: refund the way it was paid, or as account credit only where the order says credit is on offer (refundToCreditOffered); a host prop can hide the choice but never offer it. Staff decide. |

Five more were born split for holders, add-ons and wristbands (P8, TKT-544). Their shared reading of the API, the booking pass as the API marks it, the naming rules, wristband states and media in words, a guest's link, and the one plain sentence each refusal is shown as, are src/holders.ts:

| Core | What it proves | |---|---| | tl-checkout-holders | A checkout section whose writes replace the whole plan: every name and placement goes on each save, the answer is what is shown, and a refused placement is never shown as made. "One each" and "For Sam" read from how the API placed the add-ons; each unit offers only the API's eligibleHolders, and "Choose later" is sent as holder: null, so "one each" can be undone. Hidden when the cart has nobody to name. | | tl-booking-holders | People first: each action next to the person it changes, the pass policy's refusals on that person's card. Moves offer only the API's eligibleSlotIds; wristband activation only in posted_pending_activation; "Send to guest" mints a guest's own link from a page URL with no query of the booker's. | | tl-booking-pass | A pass the API marks (bookingPasses, bookingPass), covering each holder with a unit whose passId is it (bookingPassesOf). Nothing inferred. | | tl-holder-pass | A page for someone who is not the booker: the guest's own token, read from the link and used only on their own calls, opens their holder and their pass and nothing else; a link that no longer works is said as one. | | tl-wristband-activate | Runtime I/O the core cannot do (reading a chip) kept in the harness, as tl-ticket's download is: the harness says whether it can read and hands over the UID, and the core says "active" only on the API's word. |

There used to be one more, tl-seat-map: the hosted box office's map in a frame, with no core because an iframe and a postMessage protocol have no runtime-neutral half (TKT-208). It was retired once tl-seat-explorer and its siblings replaced it (TKT-470), and the hand-kept manifest it was described in went with it. packages/elements/scripts/check-runtime-claims.mjs now fails any element with no core.

Where a seat card goes

src/card-placement.ts is the rule every harness that puts a card over a map follows (TKT-526), as plain geometry in screen pixels: placeSeatCard puts the card above the seat, else below, else to its right, else to its left, slid along that side to stay on the map, always clear of the seat's hit circle by a gap, and answers null (show no card) when no side has room; cardCoversSeat is the check; seatCardReveal says where the seat should be on screen for a pinned card to have room, which the web harness turns into one camera move together with any section focus under way. The explorer core decides which card shows and whether it is compact; where it goes is this rule, measured by the harness on every frame the map moves.

Implementing a staff client

The seat elements serve the box office too (TKT-472): tl-seat-explorer and tl-best-available take a staff attribute, and with it look for a few optional capabilities on the client they are handed. The elements never know where a client talks to; a buyer's connector (@ticketlayer/live) offers none of these, so on a buyer's page staff changes nothing that matters. The Box Office client in backstage-app implements them against the Backstage API's staff routes, and packages/elements/stories/workbench/staff-data.ts is a mock of the same shape for Storybook. The types are exported from this package (src/client.ts).

A staff client is an ordinary ElementClient (the events, cart and seating managers the buyer elements already use, pointed at the staff routes) with these additions. Seats are seat indexes in the venue plan version throughout.

| Where | Method | Shape | Backstage API | |---|---|---|---| | seating | getStaffState(occurrenceId) | Promise<ElementStaffSeatState>: { statusVersion, seatCount, reserved: { mine: number[], others: number[] }, sold: number[], comp: number[], killed: number[], holds: Array<{ id, name, seats: number[] }>, notOnPlan?: number[], allocations?: ElementStaffAllocation[], killsReleased?: number[] }. A seat listed nowhere is available. mine is this sale's cart. An allocation is { id, name, seats: number[], holderType?, salesChannelId?, releaseAt? } | GET /v1/occurrences/{occurrenceId}/seating/state?cartId=<the staff cart>, its state, passed on as it is | | seating | getStaffAccess(occurrenceId) | Promise<ElementStaffAccess>: { sellHeld: boolean, acceptSingleSeatGap: boolean }. Absent, the elements assume neither | sellHeld from the person's permission to release a hold; acceptSingleSeatGap true when the channel's singleSeatGap rule is confirm (the box office default) | | seating | bestAvailable(occurrenceId, request) | The request is ElementBestAvailableRequest; resolve with the API's answer as it is ({ planVersion, statusVersion, options }). In staff mode the element sends no options, so the channel's default (five at a box office) applies. Set no sessionId: the API seeds the spread from the staff member | POST /v1/occurrences/{occurrenceId}/best-available, on the channel X-Ticketlayer-Channel names | | seating | getPointer, getSeatmap, getChunk, getView, getStatus, watchStatus | As for a buyer. The seat map token for them is staff's | POST /v1/occurrences/{occ}/seatmap/tokens (every scope, stf: true), then the seat map Worker as a buyer reads it | | cart | reserveSeats(occurrenceId, seats, ticketTypeId?, options?) | All or none; resolve with the cart. options is ElementSeatReserveOptions: { acceptSingleSeatGap?: boolean, releaseHolds?: boolean } | POST /v1/cart/{cartId}/seats with acceptSingleSeatGap when asked | | cart | reserveSeat(occurrenceId, seat, ticketTypeId?, options?) | One seat, the same options | the same route, one seat | | cart | setSeatType, releaseSeat, get, on('cart:updated') | As for a buyer | PATCH and DELETE /v1/cart/{cartId}/seats/{occ}/{seat} |

What the client must do, and the elements rely on:

  • Refusals keep the buyer client's shape. Reject with an error whose code is SEAT_UNAVAILABLE (with unavailable and alternatives), SINGLE_SEAT_GAP (with strands and suggestion), PLAN_VERSION_CHANGED (with planVersion) or SEAT_LIMIT, the details on the error or on its details (seatErrorOf reads both). The Backstage API's envelopes carry them on error.
  • releaseHolds means some of the seats are in a named hold and the person has confirmed selling them: take those seats out of their holds (POST /v1/occurrences/{occ}/seating/holds/{holdId}/release with { seats }), then reserve them, and if the reservation is refused put nothing back in a hold the person did not ask to change. Without it a held seat is SEAT_UNAVAILABLE, as the API says.
  • acceptSingleSeatGap is passed only after the element has asked the person and they said yes. Send it on the reservation; the API honours it only on a confirm channel.
  • The state moves with the availability. The explorer reads getStaffState on mount, again whenever the availability's statusVersion passes the state's, and after each of its own writes, so the two must count the same versions (they do in the API: one counter per occurrence). watchStatus is the buyer's poller and is enough. The seat card and the map key's Status tab read it on the same signals, so a page with all three asks up to three times for one version: answer them from one request per statusVersion (and per cart write) rather than three.
  • Allocations and released kills are optional. allocations is the API's open allocations (TKT-460): each a named block given to an agent or a promoter, sellable only on the channel it names, its seats the idle ones (a seat reserved or sold through it is in reserved or sold). The explorer draws them in seat-allocated, small, as seats the box office cannot sell; the seat card and the map key's Status tab name each one, "Allocated: Agent: See Tickets". killsReleased is the production kills released for sale on this occurrence: the event plan's overlay still marks them killed, so the elements draw and count them as available on its word (and the API leaves them out of killed). A client from before them sends neither and nothing is drawn as allocated or released; the availability must still show allocated seats as gone and released kills as free, as the API's does for the box office channel.
  • No countdown. A staff cart has a hold like any other; nothing in the staff elements shows it, and the explorer does not announce a hold that ran out.
  • The rank stays on the server. Nothing a staff client resolves may carry it, any more than a buyer's.

What a core still cannot say

Kept here because it is the useful half of a split, and because each one is a decision somebody will have to make rather than a gap to fill in quietly:

  • Copy with something emphasised in the middle of it. strings is a flat map of finished sentences, so tl-login's "We've sent a sign-in link to [email protected]. Open it on this device to continue." is carried as sentLeadBefore and sentLeadAfter with the address between them. That works and it does not translate: a language that puts the address first has nowhere to say so. What is missing is a placeholder in a string and a harness that fills it.
  • Which token family a status paints with. tokens names the theme tokens a harness renders with, and nothing in a core may hold a value. It is a flat list, so there is no shape for saying that soldOut paints from error and waitlist from info: a core can name the families its harnesses use, and not which one belongs to which state. That is not a gap the split elements wait on, because there is an answer already - the core derives the CLASS NAME the status renders under and the stylesheet says which family that class reaches for. tl-event-card's status badge has done it since it was split, and tl-buy-tickets-button, whose four states used to paint from literal hex in the component and now read the primary, error, info and warning families (TKT-203), was split on the same bargain. What is missing is only the ability to state the mapping in the manifest, so a native harness with no stylesheet could read it rather than write it out again.
  • A file the buyer takes away. tl-ticket's harness downloads a PDF by making an object URL and clicking an anchor. The core can say "the buyer asked for this ticket" - requestDownload() announces tlDownloadRequested
    • and that is all it can say: it has no seam for handing a runtime a blob to save, the way ctx.storage and ctx.query are seams for the other two runtime facts. TKT-204 decided this stays the harness's rather than inventing one; a native harness will need its own answer for turning a blob into a saved file, and it will not come from this core.
  • A timer you can cancel. ctx.after(ms, fn) schedules and hands nothing back, and the instance only clears its timers on destroy(), which a web harness never calls. tl-order-tickets polls, so it cancels by moving a generation counter on and letting the stale timer fire into a no-op. That is correct and it is not obvious, and every core that ever polls will have to write it again until after returns its own cancel. The countdowns wrap it once, in createCountdownClock (src/offers.ts).
  • Anything whose subject is another document. An iframe and a postMessage protocol have no runtime-neutral half to lift out. The one element that was only that, tl-seat-map (TKT-208), was retired rather than split (TKT-470); a new element whose subject is another document would need this decided again, and there is no hand-kept manifest to put it in.