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

clarity-tax-report-ui

v1.7.0

Published

Shared components, tokens and runtime for Clarity Tax HTML reports. Renders identically under the platform's Nunjucks engine and a report repo's local Jinja2 harness.

Readme

clarity-tax-report-ui

Shared components, design tokens and runtime for Clarity Tax HTML reports.

A report stays hand-written HTML. What this package supplies is the structure: macros for the markup, one token set for every colour and dimension, and the grid, drawer and formatting behaviour that every report was otherwise reimplementing.

New here? Open docs/architecture.html — the flow end to end, what each file holds, and what you can and cannot override.

npm install clarity-tax-report-ui
npx ct-report init template.html     # once
npx ct-report sync template.html     # after every upgrade

Adoption is the report repo's decision. Install it and your template can call ct.*; do nothing and your report renders exactly as it does today. Nothing in the platform changes either way — there is no backend code in this design.


How it reaches the page

ct-report maintains one region of your template.html, between <!-- ct:begin --> and <!-- ct:end -->. Inside it: the compiled stylesheet, the runtime, and the macro definitions. Outside it: your report, untouched.

<head>
  <!-- ct:begin 1.0.0 — generated by ct-report, do not edit inside these markers -->
    <style>…tokens + components…</style>
    <script>…runtime…</script>
    {% macro data_table(s) %}…{% endmacro %}
    {% set ct = { "data_table": data_table, … } %}
  <!-- ct:end -->
</head>
<body>
  {{ ct.report_head(report_title, generated_on) }}   ← yours
</body>

Why one file and not src/ → dist/. Report repos have no CI. A two-file build fails invisibly: edit the source, forget to rebuild, and the platform keeps serving the previous output with no warning and wrong figures. Here the author edits the same file the platform renders, so an unbuilt edit is impossible. Delete or corrupt the block and the render throws with a line number. The worst remaining failure is a stylesheet a release or two old.


The dual-engine constraint

The same template.html is rendered by two engines:

| where | engine | how | |---|---|---| | platform | Nunjucks 3.2.4 | ReportGeneratorEngine, autoescape on, null loader | | local | Jinja2 3.x | the report repo's run_report.py |

Every macro here lives in the intersection of the two languages, which is narrower than either. test/parity.mjs renders each one under both and fails the build on any difference — so a construct that works locally cannot reach a release and break on the platform.

If you write template code of your own, these are the traps. Each was measured, not assumed:

Empty collections. [] and {} are truthy in Nunjucks (JavaScript) and falsy in Jinja2 (Python). {% if rows %} takes opposite branches when the collection is empty — silently, on the one input nobody tests. Use {% if rows|length %}. An empty string is falsy in both, so {% if title %} is fine.

Loose equality. Nunjucks == is JavaScript's. They agree for numbers and null and disagree for the empty string: "" == 0 is true in Nunjucks, false in Jinja2. So {% if v or v == 0 %} — the guard that lets a legitimate zero through — is safe only where v is a number or null. Nunjucks has ===; Jinja2 does not, so there is no portable strict comparison.

Null rendering. {{ v }} where v is null renders empty in Nunjucks and None in Jinja2, and |default('') does not help — default only fires on undefined. Guard every nullable value explicitly.

|tojson is not portable. Nunjucks emits raw JSON; Jinja2 escapes it to &#34;. Use ct.data_script(key, section.rows_json), which applies |safe to the string the engine already serialised.

Banned outright: .items(), {% for k,v in dict %}, s[:-3] slicing, |slice, |round, |map, |selectattr, namespace, {% for %}{% else %}, is defined, is none, and {% set %} carrying a value out of a loop. Use |dictsort for dict iteration.

Numbers are formatted by the runtime, not the template — there is no portable way to group thousands, because Jinja2 has string slicing and Nunjucks does not. ct.num emits the raw value plus data-ct-num, and the runtime rewrites it in place, so the figure stays readable if the runtime never runs.


Commands

| command | what it does | |---|---| | ct-report init [file] | insert the managed block (default template.html) | | ct-report sync [file] | refresh the block from the installed version | | ct-report check [file] | exit non-zero if the block is missing, stale or hand-edited | | ct-report doctor [file] | show versions and validate the ct.* calls in the template |

Add them to the report repo so nobody has to remember the paths:

"scripts": {
  "sync":   "ct-report sync templates/my_report.html",
  "check":  "ct-report check templates/my_report.html",
  "doctor": "ct-report doctor templates/my_report.html"
}

Components

Run npm run gallery and open gallery/index.html for every component in every variant, rendered by the shipped macros.

Primitives — surface stack inline grid text. Everything else composes from these, which is why a KPI card and a table container share one border rule instead of two that happen to agree.

Numbers — num money pct. Grouped thousands, parenthesised negatives in red, tabular numerals. The conventions match the xlsx theme, so a report and its spreadsheet export agree.

Document — doc_open doc_close report_head section_open section_close footnote divider page_break.

Controls — toolbar_open/close toolbar_label toolbar_spacer search button count segmented basis_toggle tabs tab_panel accordion_open.

Data — data_table for any engine section; matrix_open for the frozen-column grid; buildup_table for a detail ladder.

Meta — kpi_open/kpi/kpi_close key_values filter_chips badge callout empty_state.

Transport — data_script config_script.


The matrix, and the spine contract

The platform engine hands templates flat query results and does not pivot, so the grid is built client-side. Rather than every report writing its own pivot, ctReport.matrix() consumes one agreed row shape — one row per (logical line × entity):

row_key  parent_key  depth  row_type  agg_rule  label  is_expandable
entity_code  entity_name  local  reporting   [section_number  sort_order]

row_type is one of spacer | group | leaf | subtotal | section_total | grand_total | rate. A report that can produce this shape gets freezing, the tree, entity filtering, column resize and cell selection without writing any of it.

agg_rule drives the frozen Total column. sum and none are built in; anything else is looked up in opts.aggregators, so a report keeps ownership of rules only it understands:

var matrix = ctReport.matrix(spine, {
  basis: 'local',
  aggregators: {
    // An effective tax rate is not the sum of its columns: it is one
    // section's total over another's, recomputed on the visible entity set.
    recompute_ratio: function (line, codes, basis, m) {
      var num = m.sectionTotal(21, basis);
      var den = m.sectionTotal(1, basis);
      return num !== null && den ? num / den : null;
    }
  },
  onCellSelect: function (info, td) { /* open a drawer */ }
});
ctReport.wireToolbar(matrix);

Invariants it encodes

Four structural rules that cost provision_report a debugging round each, now written down once:

  1. border-collapse: separate — with collapse, sticky cells lose their borders entirely.
  2. The z-index ladder: body-scroll 0 < body-frozen 1 < head-scroll 2 < head-frozen 3 < resize 4. Put head-frozen below head-scroll and the frozen headings paint underneath the scrolling ones, so scrolling right leaves the Total figures pinned under an entity's heading.
  3. Every row type sets an opaque background. Sticky cells paint their own; a transparent one lets scrolling figures show through the frozen column at any z-index.
  4. The body reserves the same .5px right border the head has, as transparent. box-sizing is border-box, so a border the head has and the body lacks makes the two content boxes differ, and identical padding then lands the heading and its figures on different right edges.

Runtime API

window.ctReport, available once the block is in the page.

| | | |---|---| | version | the version that produced this block | | format(v, dp, pct) | one figure, as a string | | formatAll(root) | rewrite every [data-ct-num] under root | | numSpan(v, opts) | a formatted <span> for runtime-built cells | | data(key) | read a ct.data_script payload | | config() | read the ct.config_script payload | | matrix(spine, opts) | mount the grid; returns the instance | | wireToolbar(m, ids) | bind basis, filter, expand, collapse and count | | drawer(id) | drawer controller: open close setHead renderBuildup |

Number formatting, segmented controls, tabs and accordions bind themselves on load. You only call in for the matrix and the drawer.


Versioning

major.minor.patch, and adopting repos pin ^1, so every repo on the major renders identically without anyone coordinating.

| bump | trigger | effect on ^1 | |---|---|---| | major | a macro removed or renamed, a required parameter added, a token removed | not taken; migrate on your own schedule | | minor | new macro, new optional parameter, new token, new variant | taken on next sync; existing markup unchanged | | patch | CSS internals, class renames, spacing fixes | taken on next sync |

Class names are internal and undocumented. Templates call macros and never write a .ct-* class, which is what keeps most visual work to a patch — and what stops a report from quietly drifting away from the others. If you need a variation, it becomes a macro parameter, reviewed once and then available everywhere.

The caveat. The caret resolves at sync time, not render time. With no CI in report repos, a patch reaches a repo only when someone runs npm update && ct-report sync. "Everyone on ^1 looks identical" is really everyone who has synced since the release.


Developing this package

npm run build          # tokens + css + macros + runtime -> dist/
npm run check:literals # fail on a raw hex or px outside the token layer
npm run test:parity    # render every macro under Nunjucks AND Jinja2, diff
npm run gallery        # gallery/index.html
npm test               # all of the above

npm test runs on prepublishOnly, so none of these can be skipped on the way to a release.

Publishing

The package is public and unscoped, so no npm org is involved. npm does, however, require either 2FA or a granular token to publish, even on an account whose profile reports two-factor auth: disabled — a plain npm publish returns:

403 Forbidden - Two-factor authentication or granular access token
with bypass 2fa enabled is required to publish packages.

A classic token is not enough. One will authenticate — npm whoami returns the username — and then fail the publish with the same 403. Classic tokens are the ones npm token list shows; granular tokens do not appear there, which is a quick way to tell which you have.

Either path works:

# 1. interactive, with 2FA enabled on the account
npm publish --access public --otp=123456

# 2. with a GRANULAR access token
#    npmjs.com -> Access Tokens -> Generate New Token -> Granular
#      - Permissions:  Packages and scopes -> Read and write
#      - Packages:     All packages  (see the note below)
#      - Check:        Bypass two-factor authentication
#    This is also what CI needs.
export NPM_TOKEN=<granular token>
npm publish --access public

.npmrc in this repo reads ${NPM_TOKEN} from the environment rather than storing a token, so the secret never lands on disk or in git.

First publish needs "All packages" scope. A granular token restricted to named packages cannot create one that does not exist yet, so a token scoped to clarity-tax-report-ui will fail the initial publish with a 404 or 403. Use "All packages" for the first release, then narrow the token afterwards if you want to.

prepublishOnly runs the full gate first, so a build that fails the token guard, the aggregation tests or dual-engine parity cannot be released.

After the first successful publish, point adopting repos at the registry instead of the working copy:

# in the report repo
npm pkg set dependencies.clarity-tax-report-ui=^1.0.0
npm install

Contributing

Three rules for contributors:

  1. Tokens only. Component CSS references var(--ct-*) and nothing else. check:literals enforces it; add the value to src/tokens/tokens.json, or allow it explicitly with a reason if it is structural rather than a design decision.
  2. Compose from the primitives. A new component is built from surface, stack, inline, text and grid, not authored standalone.
  3. Add a parity fixture. Every new macro gets one in test/parity.mjs. The loose-equality bug above passed 14/14 fixtures because none of them had a blank param — it was caught downstream in a report. A macro without a fixture covering its awkward inputs is not covered.