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

@tilewise/template

v0.1.2

Published

What a record shows, as JSON. A table column's cell, a list item, a tile and a card's body are all templates, drawn by the same code from the same JSON; each host keeps a few fixed parts of its own (a card's icon and title) as plain values, and the templa

Readme

@tilewise/template

What a record shows, as JSON. A table column's cell, a list item, a tile and a card's body are all templates, drawn by the same code from the same JSON; each host keeps a few fixed parts of its own (a card's icon and title) as plain values, and the template is the rest. It is the display half of what @tilewise/form is for input: a form is JSON over a registry of inputs and layouts, a template is JSON over a registry of layouts, renderers and formatters, each of which an app extends.

It holds no code, so a view stores it as it is; anything a template would compute, the app adds to the record first. That is why it imports no React and no DOM, only @tilewise/resource's types and @tilewise/registry, and sits under packages/shared/: a server checks and stores a template as a browser draws it. The components that draw one come from @tilewise/ui.

A template

A tree (TemplateNode): a layout node names a registered layout, gives it its options and holds nodes; a part is a leaf, one thing the record shows. Layouts nest as deep as a template needs, and a template's root is a node like any other, so a cell showing one field is one part. Any node may carry when, a filter on the record (@tilewise/resource's Filter), and shows, with everything in it, only while it holds.

{
  "layout": "stack",
  "options": { "gap": 1 },
  "nodes": [
    {
      "layout": "row",
      "options": { "justify": "between" },
      "nodes": [{ "field": "key" }, { "field": "status" }]
    },
    { "field": "title" },
    {
      "layout": "repeat",
      "options": { "field": "labels", "max": 3, "gap": 1 },
      "nodes": [{ "field": "labels" }]
    },
    { "field": "blockedBy", "when": { "field": "status.kind", "op": "equals", "value": "blocked" } }
  ]
}

Layouts

A layout node names its layout by a string. tilewise's four are @tilewise/ui's (BaseLayouts):

  • row (RowOptions): its nodes side by side. justify (start, center, end, between), align (start, center, end, stretch, baseline), gap and wrap. Default: packed at the start, centered across, no gap.
  • stack (StackOptions): its nodes one under another. align (start, center, end, stretch) and gap. Default: stretched across, no gap.
  • grid (GridOptions): its nodes in columns, row after row. columns is a count of equal ones (2), or a least width ({min: 160}), as many as fit; and gap, align.
  • repeat (RepeatOptions): its nodes once per entry of a list field (field: labels, assignees, a stack's services), up to max, then "+N", in a row, with row's options. In its nodes the field holds that one entry, so a part showing the field shows the entry; an object entry's keys are a level down, 'services.name'.

A gap is a step of 4 pixels, as @tilewise/ui's Row and Stack take it: 0, 1, 2, 3, 4, 6 or 8. An untyped template takes any layout. An app types its templates with its own layouts:

import type { TemplateNode } from '@tilewise/template';
import type { BaseLayouts } from '@tilewise/ui';

interface AppLayouts extends BaseLayouts {
  overlay: { anchor: 'top-end' | 'bottom-end' };
}

const avatar: TemplateNode<AppLayouts> = {
  layout: 'overlay',
  options: { anchor: 'bottom-end' },
  nodes: [{ field: 'assignee' }, { field: 'presence' }],
};

The layouts are a registry, one set for every host: @tilewise/ui's baseTemplateLayouts. Each layout is one file: its options' valibot schema, its component and its entry. row, stack and grid draw their nodes once; a layout with repeats draws them once per entry of a list field, up to max (entriesOf(record, field, max)). A layout gets its options, its items already drawn, and more, the entries past max (LayoutProps). An app defines its own with defineTemplateLayout and registers it with register, which gives a new registry:

const layouts = baseTemplateLayouts.register({
  overlay: defineTemplateLayout({
    options: v.strictObject({ anchor: v.picklist(['top-end', 'bottom-end']) }),
    Component: Overlay,
  }),
});

Parts

A part (Part) shows one thing: a field (field: a key, or an option's attribute, 'status.kind'), a constant (value: an icon's name, a separator) or a text template (text: '{project}-{number}', @tilewise/resource's Template). A registered renderer draws it (renderer), a registered format turns its value into text (format), each by its name or as {name, options}, as a field's format names one. Its options are the ones tilewise's renderers share: tone (a tilewise tone, by its name), mono, weight (400, 500, 600), clamp (the most lines), prefix, and empty, what it says while its value is empty; without one, an empty part shows nothing and takes no room.

The id as a blue link, the title as plain text, the description clamped to two lines:

[
  {
    "field": "id",
    "renderer": { "name": "link", "options": { "to": "/items/{id}" } },
    "options": { "tone": "blue", "mono": true }
  },
  { "field": "title" },
  { "field": "description", "options": { "clamp": 2 } }
]

A part that names no renderer or format takes its field's, so a field shows the same everywhere unless a template says otherwise: its kind's default renderer, and its format, else its kind's. resolvePart(part, field, defaults) fills them in, given the renderer registry's default for each kind and the kinds (baseKinds); a constant or a text template is drawn as text. fillText(text, show) fills a text template with each path's value as show gives it, trimmed, its runs of spaces folded, {{ and }} a brace; textPaths(text) names the paths it holds.

Renderers

A registry of renderers, by the name a part gives (renderer), one set for every host: @tilewise/ui's baseRenderers. Each renderer is one file: the kinds it shows, its options' valibot schema, its least width (minWidth), its component and its entry. A renderer gets RendererProps: its values, each ready as text in its format (Shown), its own options, the part's, the field and the record. An app defines its own with defineRenderer and registers it with baseRenderers.register.

| Renderer | Shows | Kinds | Options | | ---------- | ------------------------------------------------------- | -------------- | --------------- | | text | its text, in its format | any | | | link | its text, to its route, or a url's to itself | any | to | | pill | each value in a pill, an option in its color | option, text | variant | | date | its text, with the exact day on hover | date, datetime | | | number | its text, its digits of one width | number | | | avatar | a picture: a url's, a ref's row's, else initials | url, ref | size, shape | | user | a ref's row by its picture and its title | ref | size | | icon | a glyph of @tilewise/ui's registry, by the value's name | text | size | | progress | a bar, the value out of max (100) | number | max | | meter | a share of max (1), toned by how full it is | number | max | | trend | a change, an arrow its way, toned by whether it's good | number | good | | check | a check for true | boolean | | | code | JSON, or text, as code | json, text | |

Each of tilewise's kinds has a default renderer (@tilewise/ui's baseDefaultRenderers), the one a part showing a field of it gets when it names none: text as text, a url as link, a number as number, a date or datetime as date, a boolean as check, an option as pill, a ref as user and json as code. An app's own kind adds its default beside them, and partDefaults(renderers, kinds) gives resolvePart both:

import { baseKinds } from '@tilewise/resource';
import { partDefaults, resolvePart } from '@tilewise/template';
import { baseDefaultRenderers } from '@tilewise/ui';

const defaults = partDefaults(
  { ...baseDefaultRenderers, color: 'swatch' },
  baseKinds.register({ color }),
);
resolvePart({ field: 'status' }, status, defaults); // { field: 'status', renderer: 'pill' }

entryOf(record, name) finds an entry of a plain record, as the kinds' defaults, by its own name only, so a kind named 'constructor' finds nothing; named(ref) reads a renderer or a format named alone or as {name, options}; canShow and fits say whether a renderer shows a kind and fits a width.

Links

A link goes to its route, to in its options, filled from the record, so a title or a key links to its row; without one, a url links to itself. A route is text with field paths in braces, starting as an address does:

{ "field": "key", "renderer": { "name": "link", "options": { "to": "/p/{project}/t/{number}" } } }

linkTarget(to, record) fills it: each placeholder the value its path holds (an option its value, a ref its id), URL-encoded, so a space or a slash keeps to its place. A placeholder whose value is empty, or a list, leaves the part linking nowhere, as does what fills no address. isAddress is the one rule for every link: absolute with its scheme, or a path from the root; never //…, a relative path, or a scheme that runs code (javascript:, data:, vbscript:). isRoute checks a to as link's schema does, and textPaths(to) names the fields it needs. How the app's router follows the address is the app's: @tilewise/ui's createLinkRenderer(Anchor) draws its links with the app's router link in TextLink's look.

Formats

A registry of formats, by the name a field's format or a part's format gives, one set for every host and for a server as for a browser. A format turns a value into the text a renderer shows. Each entry (Formatter) is its function and its spec, @tilewise/resource's FormatSpec: its name, the kinds it takes and its options' valibot schema. The function is called with the value, its options (the schema's output, {} when it takes none) and the app's context, the locale and time zone it sets once and the moment relative counts from (FormatContext), and answers with pieces (FormatPiece), as Intl's formatToParts does:

import { baseFormats, formatText, formatValue } from '@tilewise/template';

const pieces = formatValue(baseFormats, { name: 'bytes', options: { per: 's' } }, 13_002_342);
// [{ text: '12.4' }, { text: ' ' }, { text: 'MiB/s', unit: true }]
formatText(pieces); // '12.4 MiB/s', for a filter chip, a tooltip, an export, an MCP answer

A unit is a piece with unit: true, so a card draws it smaller than its number and a cell joins them. tilewise registers eleven (baseFormats):

| Format | Writes | Kinds | Options | | ---------- | ------------------------------------------- | --------------------------------------- | ------------------------------- | | number | 1,234.5, compact 12.4K | number | digits, notation: 'compact' | | percent | a fraction as a percent: 0.427 is 43% | number | digits | | bytes | 12.4 MiB, as a rate 12.4 MiB/s | number | units, digits, per | | duration | 2h 5m, 840ms | number | unit, days | | money | €12.50 | number | currency (required) | | date | Jul 13, 2026, the UTC day | date | | | datetime | Jul 13, 2026, 09:05 | datetime | timeZone | | relative | 5 minutes ago, in 2 hours, 3 days ago | date, datetime | smallestUnit | | plain | the text as it is | text, markdown, url, email, phone, time | | | markdown | markdown as the text it reads as | markdown, text | | | rich | a rich text's words, a block to a line | richtext | |

digits is the most digits after the point, and a whole amount drops its zeros; percent takes a fraction; duration reads its number in unit (ms, s, min or h, minutes unless given). A format's timeZone wins over the context's. markdown and rich are the default formats of the markdown and richtext kinds: they write the words of the text, so a cell shows neither marks nor JSON. A value in no format, in one nothing registered, or with options its schema refuses is its own text (formatValue), so a stored mistake shows and takes nothing down.

baseFormats is a registry (@tilewise/registry), as the renderers are: an app defines its own with defineFormat, which checks it and types its function by its options' schema, registers it beside tilewise's with register, and passes them to the resource's registry (values()), which checks each field's format and its options. checkFormats(formats, kinds) checks the formats against the kinds: each kind's default format (KindSpec.format) registered and taking that kind:

import * as v from 'valibot';
import { createRegistry } from '@tilewise/resource';
import { baseFormats, checkFormats, defineFormat } from '@tilewise/template';

// All its options optional: the schema takes `undefined` too, so `format: 'rate'` alone is valid.
const options = v.optional(v.strictObject({ per: v.optional(v.picklist(['page', 'visit'])) }), {});

const rate = defineFormat({
  name: 'rate',
  kinds: ['number'],
  options,
  format: (value, { per = 'page' }) => [
    { text: String(value) },
    { text: ` per ${per}`, unit: true },
  ],
});

const formats = baseFormats.register({ rate });
checkFormats(formats);
const registry = createRegistry({ formats });

The pure functions the formats write with live here too, so a server formats as a browser does: formatBytes, formatDuration, formatDateTime, formatExact and relativeLabel. @tilewise/ui exports them by the same names.

Hosts

A host draws a record with a template: a table column's cell, a list item, a tile, a card's body. Each kind of host says which of its parts are fixed, set as plain values, and where its template goes among them (HostSpec): its fixed parts' valibot schema, and the parts it draws in order, with 'template' where the template goes. A host as a view stores it (Host) is its fixed parts and its template. defineHost checks a kind's order names the template once and no part twice, and types its parts by its schema's keys. A column, a list item, a tile and a card define their own.

import * as v from 'valibot';
import { defineHost, type Host } from '@tilewise/template';

const card = defineHost({
  fixed: v.strictObject({ icon: v.optional(v.string()), title: v.string() }),
  order: ['icon', 'title', 'template'],
});

const hosts: Host<v.InferOutput<typeof card.fixed>> = {
  fixed: { icon: 'server', title: 'Hosts' },
  template: { field: 'name' },
};

Docs

The Templates guide is in docs/, two pages so far, the overview and the formats; the docs site (pnpm docs:dev in tilewise) shows them at /templates/overview and /templates/formats.

Layer

It depends on @tilewise/resource, for its types: a filter, a field's path and metadata, a format, the kinds; and on valibot, for its registries' schemas. A test holds the main entry to importing those two alone, and its tests run in Node. @tilewise/ui is built on it.