trieur.

Reference

new Deck(root, options)

OptionDefaultRole
items[]the pile to sort; the first element is the top card
zones[]zones: { id, label?, key?, color?, icon?, image?, disabled? }, or null for a free zone
renderCard(item, el)—draws the card (required in practice)
renderZone(zone, el)folder tiledraws a zone
meta(item)the itemwhat the model is allowed to look at
advisor—a Recommender, or anything with best()
minConfidence0.45minimum score for a zone to be suggested
layout'auto''auto', 'circle', 'radial', 'voronoi', 'grid', 'dock', or (n, box) => …
segmentstruecarve the stage into regions and aim at the region
tapZonestruea tap on a tile files the card — no drag at all
keys'asdfghjkl…'keys handed to zones, in order
threshold90drag distance past which the drop is armed, in px
multifalseallow a card to be filed into several zones (details)
multiPad'auto'the held pad: 'auto' (dynamic on touch), 'dynamic', 'left', 'right', false
holdDelay420ms a finger must rest on a card to open the stack; 0 disables it
touchPreviewtrueon touch, an inline deck waits for play before taking the gesture (why)
keyboard'auto''auto' also answers keys when it is the only deck on screen; 'focus'; false
deadZone0grows the dead zone around the pile, in px
zonePadding12safe margin between a tile and the edge of the stage
zonePull0.18how far floating tiles gather back in towards the pile
piles1experimental — deal several piles side by side, sharing the zones

| flickMs | 170 | how far ahead a throw is projected, in ms of travel | | flickDecay | — | the same projection as a deceleration rate per ms (0.99 = iOS fast) | | flickBias | 0.4 | how much wider the model’s suggestion catches a throw, in tiles | | flickMin | 0.6 | px/ms below which a release is an ordinary drop | | flickDebug | false | draw the throw vector and where it lands | | text | en | labels (fr provided, or your own) | | onSort(item, zone) | — | performs the filing; may be async, a rejection cancels | | onSortMany(item, zones) | — | files into several zones at once | | onUndo(item, zone) | — | undoes the last filing | | onUndoMany(item, zones) | — | undoes a multi-zone filing | | onSkip(item) | — | card pushed to the back of the pile | | onAssign(index, item) | — | drop on a free zone | | onEmpty() | — | the pile is empty |

Methods

deck.setItems(items)        // replaces the pile
deck.setZones(zones)        // replaces the zones (reassigns the keys)
deck.setOptions(patch)      // changes part of the configuration
deck.commit(zone, fling?)   // files the top card (what a key press does)
deck.commitMany(zones?)     // files into several zones (defaults to the current stack)
deck.skip()                 // pushes the card to the back of the pile
deck.undo()                 // undoes the last filing
deck.refresh()              // redraws the cards on screen, in place (renderCard again)
deck.suggest()              // recomputes the suggestion
deck.expand(on)             // fullscreen
deck.play(on)               // touch: take the gesture, or give the page its swipe back
deck.layout(force?)         // re-places the zones (skipped when nothing moved; force to insist)
deck.zoneAt(x, y)           // zone under a screen point
deck.highlight(zone, armed) // light a zone as if the gesture were pointing at it
deck.destroy()              // removes everything from the DOM, and the listeners

deck.current                // top card (of the active pile)
deck.active                 // which pile the keyboard talks to; assignable
deck.zones                  // placed zones (index, key, angle, pos, cell)
deck.prediction             // { id, score, why } or null
deck.picking                // the multi-zone stack, in pick order
deck.multi                  // whether multi-zone mode is on
deck.expanded               // fullscreen state
deck.live                   // touch: whether it has taken the gesture

<trieur-deck> and <trieur-zone>

Attributes of <trieur-deck>: layout, keys, threshold, min-confidence, segments, multi.

Attributes of <trieur-zone>: value (the id; absent means a free zone), key, label (falling back to the tag’s text), color, icon, image.

JS properties: .options, .items, .zones, .deck, .current, .prediction, .picking. Methods: .skip(), .undo(), .focus().

Added, changed or removed on the fly, a zone follows — without interrupting the session.

Models

interface Model {
  readonly kind: string;
  readonly examples: number;
  learn(features: string[], target: string, weight?: number): void;
  predict(features: string[], targets: string[]): Ranked[];
  toJSON(): ModelJSON;
}
ClassOptions
Bayesalpha (0.4), minExamples (3)
Linearlr (0.5), margin (1), minExamples (3), maxVocab (40,000)
Knnk (12), capacity (1500), probe (24), minExamples (1)
Ensemblenew Ensemble([…models])

modelFromJSON(json) rebuilds any of them; unknown JSON yields defaultModel().

Features

tokens(meta)                              // metadata → features
crosses([['domain', 'tag']], max = 4)     // adds the crosses
only('domain', 'tag')                     // keeps only those keys
pipe(tokens, crosses([…]), only(…))       // chains them
defaultFeatures                            // tokens + domain/author/host × tag crosses

Recommenders

createRecommender({ key, model?, features?, store?, minConfidence?, saveDelay?, server? })

No server → LocalRecommender. With server → HybridRecommender, which additionally exposes pending, warm() and serverStats().

Storage

memoryStore(), localStore(prefix), idbStore(db, store), autoStore().

interface Store {
  load<T>(key: string): Promise<T | null>;
  save(key: string, value: unknown): Promise<void>;
  remove(key: string): Promise<void>;
}

Bench

import { crossed, evaluate, synth } from '@trieur/learn/bench';
evaluate(name, model, extractor, cards); // → { top1, top3, silent, vocab, ms, asked }

Geometry

voronoi(points, w, h) returns the polygons, inPolygon(poly, x, y) tests membership, and layouts.auto | circle | radial | voronoi | grid | dock are the built-in layouts.

Two of them are parameterised, and the factories are exported:

import { radialLayout, dockLayout } from '@trieur/core';

radialLayout({ sweep: Math.PI, start: -Math.PI / 2 })  // a half menu, opening right
radialLayout({ sweep: Math.PI / 2, start: Math.PI })   // a quarter, top-left
radialLayout({ ringGap: 14 })                          // air between the rings
dockLayout({ split: true })                            // top *and* bottom edge
dockLayout({ rows: 2 })                                // two rows; by default, as many as fit

An arc rather than a full circle is what lets a radial menu live against an edge, or beside a thumb, without wedges pointing off the screen — the capacity of each ring scales with the arc it actually covers. A dock wraps on its own when the tiles no longer fit across the stage: six tiles on a phone become two rows of three rather than six tiles spilling off both sides.

A layout returns points, or { points, cells } when it wants to describe its own regions — that is how 'radial' draws wedges instead of letting the Voronoi decide.

clearCentre(points, box) pushes any seed inside the card out to its edge, and fitToStage(points, box) scales the set down until the tiles fit. resolveLayout() applies both to every layout, including yours. clearanceAt(angle, box) is the rule they use: the ray hitting the card’s box inflated by half a tile — a rectangle, not an ellipse, because a tile at 45° sat outside an ellipse and still overlapped the card.

The box a layout receives is { w, h, cardW, cardH, clearX, clearY, tile }: the stage, the card, the half-extents to keep clear (the card plus half a tile) and the measured size of a tile. clampToStage(points, box) pulls points back inside one axis at a time — that is what runs on a layout that draws its own regions, since scaling would slide the labels out of their own wedges.

Keyboard

KeyEffect
a zone letterfiles the card there
⇧ + lettersstacks zones; releasing ⇧ files them
⇧ tapped alonelatches multi-zone mode, or files a pending stack
↵files a pending stack, otherwise accepts the suggestion
spaceskip
⌫undo, and unlearn
Escdrops the stack, then leaves fullscreen

The full reference, gestures included, is Keyboard and gestures.

Classes the deck writes

.tr-layout-<name> for the layout in play, .tr-multi while a stack is open, .tr-full in fullscreen, .tr-piles with more than one pile, and .tr-sm / .tr-xs when the stage measures under 560 / 400px — the responsive scale keys off those, not off the viewport. The deck also writes --tr-tray (the depth of a zone tray along the bottom edge) and --tr-card-x / --tr-card-y (where a layout asked for the card to sit).

On a touch screen, double-tapping a card accepts the suggestion — the equivalent of ↵, which a thumb cannot press. The browser’s own double-tap zoom is turned off across the sorter, so the two never fight.