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

@xivdyetools/svg

v4.1.0

Published

Platform-agnostic SVG card generators for the XIV Dye Tools ecosystem (pure functions: data in → SVG string out)

Downloads

903

Readme

@xivdyetools/svg

Platform-agnostic SVG card generators for the XIV Dye Tools ecosystem — pure functions: data in, SVG string out.

npm version License: MIT

Overview

@xivdyetools/svg generates all visual output for XIV Dye Tools bots as SVG strings. Every generator is a pure function — it takes data and returns an SVG string. PNG rasterization is handled by consumers using their platform's renderer (@resvg/resvg-wasm for Cloudflare Workers, @resvg/resvg-js for Node.js).

Since 2.0 the cards are not free-form drawings. They are compositions of one shared vocabulary defined in frame.ts — read that file before changing any generator.

Installation

npm install @xivdyetools/svg

The frame system

The whole design rests on one measurement: the canvas width IS the display width. Four constants encode it, and changing any of them changes every card:

| Constant | Value | Why | |----------|-------|-----| | CARD_WIDTH | 400 | Discord's embed image box. Draw here, raster at 2× for sharpness. | | CARD_MAX_HEIGHT | 350 | A wall, not a guideline. Past 350 the client contracts the box horizontally, shrinking every type size in it. | | CARD_TYPE | { label: 11, value: 13, name: 16 } | The type floor. Nothing below 11 px. | | ROW_CAP | 5 | Every list graphic holds five rows at full size; the tail becomes a swatch strip plus a count in the embed. |

HARMONY_ROW_CAP is 4 — harmony's taller 39 px slot rows. Height grows with the result and stops at the ceiling; 350 is a maximum, never a target.

Generators

Several commands route rather than scale — /contrast, /compare and /accessibility each pick a different frame from their input (pair count, dye count, vision:).

| Function | Command | Description | |----------|---------|-------------| | generateHarmonyCard(options) | /harmony | Harmony palette — found dye vs. computed ideal | | generateDyeInfoCard(options) | /dye info | Single dye sheet with color values | | generateRandomDyesGrid(options) | /dye random | Table of randomly selected dyes | | generateComparisonCard(options) | /comparison | Side-by-side comparison (routes on dye count, 2–4) | | generateContrastCard(options) | /contrast | WCAG contrast ratios (routes on pair count) | | generateA11yCard(options) | /accessibility | Color-vision lenses (routes on vision:) | | generateMixerCard(options) | /mixer | Ratio-sweep blending card | | generateGradientCard(options) | /gradient | Gradient strip over distinct dyes | | generatePaletteGrid(options) | /extractor image | Extracted palette ramp | | generateNearestSheet(options) | /extractor color | Nearest-dye color sheet | | generateSwatchCard(options) | /swatch | .chara character-file sheet | | generateBudgetLedger(options) | /budget | Market price ledger, tier-grouped | | generatePresetSwatch(options) | /preset | Preset swatch (pre-frame; redesign deferred) |

Usage

import { generateDyeInfoCard, generateHarmonyCard } from '@xivdyetools/svg';

// Generate a dye info card (11B). 2.0.0 generators take caller-supplied,
// already-translated `labels` plus `lang` — the package draws no English of its own.
const svg = generateDyeInfoCard({
  dye: snowWhite,                       // full Dye object
  localizedName: 'Snow White',
  localizedCategory: 'White',
  stainID: 1,
  srcValue: 'Dye Vendor · 216 Gil',
  mktValue: 'Standard Spectrum · 52254',
  nearest: [/* NearestDyeInfo[] */],
  labels: dyeInfoLabels,                // DyeInfoLabels (translated)
  lang: 'en',
  theme: 'dark',                        // optional; light ships via /preferences
});
// → '<svg xmlns="http://www.w3.org/2000/svg" ...>...</svg>'

// Generate a harmony card (11A)
const harmonySvg = generateHarmonyCard({
  typeLabel: 'Triadic',
  baseHex: '#CC6C5E',
  baseName: 'Coral Pink',
  slots: [/* HarmonyCardSlot[] — found dye vs computed ideal per slot */],
  labels: harmonyLabels,                // HarmonyCardLabels
  tierWords: ['Exact', 'Close', 'Near', 'Far'],
  method: 'ciede2000',
  lang: 'en',
});

Frame primitives

The vocabulary every generator is built from:

import {
  CARD_WIDTH, CARD_MAX_HEIGHT, CARD_TYPE, ROW_CAP,
  cardTheme, cardShell, cardText, textWidth, fitText,
  commandChip, appIcon, markFooter,
  swatch, idealSwatch, measuredRow, dashedRule, hairline,
} from '@xivdyetools/svg';

const theme = cardTheme('dark');
const svg = cardShell(320, theme, /* content */);

measuredRow has no variants and no optional slots — lead value, source→dye pair, name, tier bar, measure. Every argument is required. A consumer that cannot fill all five is a signal the abstraction needs revisiting, not a reason to add a flag.

swatch is solid for a buyable dye and idealSwatch is outlined for "the hue the maths asked for" — an ideal gradient step, a blend, a harmony's target hue.

Low-level SVG primitives

import { createSvgDocument, rect, circle, line, text, group, THEME, FONTS } from '@xivdyetools/svg';

const svg = createSvgDocument(400, 300,
  rect(0, 0, 400, 300, { fill: THEME.background }),
  circle(200, 150, 50, { fill: '#FF6B6B' }),
  text(200, 150, 'Hello', { fill: '#FFFFFF', fontFamily: FONTS.primary }),
);

Color and text utilities

import {
  hexToRgb,
  getLuminance, getContrastTextColor,
  contrastRatio, escapeXml, estimateTextWidth,
} from '@xivdyetools/svg';

// Contrast-safe text color
const textColor = getContrastTextColor('#1a1a2e');   // → '#FFFFFF'

// WCAG contrast ratio
const ratio = contrastRatio('#FFFFFF', '#000000');   // → 21

Constants

| Export | Description | |--------|-------------| | THEME / CARD_DARK / CARD_LIGHT | Shared theme tokens; light ships via /preferences | | FONTS | Font stacks (see below) | | ACCENT | The product accent | | NUMFMT / num / grp | Number formatting helpers | | CATEGORY_DISPLAY | Dye category display-name mapping | | LEDGER_*_H | Budget ledger row/section heights |

Design Principles

  • Pure functions — no side effects, no file I/O, no network calls. Same input, byte-identical output.
  • No rendering — outputs SVG strings only; consumers handle PNG rasterization.
  • The PNG must be self-contained. These get saved and reposted into servers that never ran the command, so never burn an instruction into an image — actionable, context-dependent lines belong in the embed.
  • Never ellipsise to a character count. Use fitText / estimateTextWidth: CJK counts 2× and German compounds run ~3× English.
  • Blanks, never inventions. A missing price is an em dash; a ratio with no numerator is an empty cell.
  • XML escaping is automatic in cardText and text() — never concatenate user strings into raw <text> blocks.

Font stacks

Fragment Mono (mono), Onest (body), Space Grotesk (display), each falling back through Noto Sans JP → SC → KR. Order matters: JP must precede SC or Japanese renders in Chinese letterforms, and SC has zero Hangul glyphs so KR must come last.

Rendering boundary (consumer's responsibility)

  1. Load the font files (Onest, Space Grotesk, Fragment Mono, Noto Sans JP + SC + KR subsets).
  2. Feed the SVG and font buffers to resvg-wasm / @resvg/resvg-js.
  3. Return the PNG bytes to Discord, Revolt, etc.

A dye name introducing a glyph outside the current Noto subsets renders as .notdef tofu — re-subset via apps/discord-worker/scripts/subset-cjk-fonts.py.

Dependencies

| Package | Purpose | |---------|---------| | @xivdyetools/core | Color algorithms, classifyBandTier, abbreviateDyeName, dye database (read-only) | | @xivdyetools/core/blending | In-card color mixing | | @xivdyetools/types | Shared type definitions (Dye, HexColor, RGB, HSV) | | @xivdyetools/test-utils | Snapshot-test fixtures (devDependency) |

Consumers

apps/og-worker keeps its own local theme and SVG services; it is on the OG card directions, not this frame system.

Credits & Acknowledgements

Connect With Me

Flash Galatine | Midgardsormr (Aether)

🎮 FFXIV: Lodestone Character 💻 GitHub: @FlashGalatine 🐦 X/Twitter: @AsheJunius 📺 Twitch: flashgalatine 🌐 BlueSky: projectgalatine.com ❤️ Patreon: ProjectGalatine ☕ Ko-Fi: flashgalatine 💬 Discord: Join Server

License

MIT © 2025-2026 Flash Galatine — see LICENSE.

Legal Notice

FINAL FANTASY is a registered trademark of Square Enix Holdings Co., Ltd. FINAL FANTASY XIV © SQUARE ENIX CO., LTD.

XIV Dye Tools is an unofficial fan project and is not affiliated with, endorsed by, or sponsored by Square Enix Co., Ltd.