Compare commits

...

34 Commits

Author SHA1 Message Date
Ben
63bafc2feb chore: Remove title, description, and viewport meta tags from app.html. 2026-03-11 11:26:22 -07:00
Ben
d23e910aa7 feat: enhance value formatting by adding scientific notation for extremely small numbers. 2026-03-10 14:48:00 -07:00
Ben
ae95a66668 fix: deduplicate SEO tags in app.html 2026-03-09 13:44:39 -07:00
Ben
0d099e34cd feat: add title and meta description, and relocate SvelteKit head tags to the top of the head. 2026-03-09 13:40:23 -07:00
Codex Agent
b44e9e5702 Normalize conversion factor formatting 2026-03-09 19:50:26 +00:00
Codex Agent
c20f2ebc60 Ensure calculator factors are explicit 2026-03-09 19:22:48 +00:00
Codex Agent
56873816bb Adjust calc grid max columns 2026-03-09 19:17:59 +00:00
Codex Agent
07a299275b Gause to orsted 2026-03-09 18:58:58 +00:00
Codex Agent
17ed319fe7 Increase category tooltip opacity 2026-03-09 18:49:04 +00:00
Codex Agent
de799c3a7b Fix category tooltip layering and placement 2026-03-09 18:46:58 +00:00
Codex Agent
b1bf3f40d8 Keep tooltip above cursor 2026-03-09 18:43:25 +00:00
Codex Agent
e4987d6764 Track tooltip vertically 2026-03-09 18:41:48 +00:00
Codex Agent
6af3b23987 Make category tooltips track cursor 2026-03-09 18:40:22 +00:00
Codex Agent
66d02c7f14 Add conversion-rate tooltips to category calculator cards 2026-03-09 18:36:47 +00:00
Codex Agent
5651ecb6d4 Fix calculator loader URL for SSR 2026-03-09 07:58:28 +00:00
Codex Agent
3e26376584 Fix slug lookup and add regression test 2026-03-09 07:55:17 +00:00
Ben
c006971cbe feat: Prerender the homepage, update HTML cache control, and inline critical CSS for improved performance. 2026-03-08 20:01:47 -07:00
Ben
de4fa5ba85 feat: add color palette definitions and data for various themes. 2026-03-08 19:51:39 -07:00
Ben
379bb60722 feat: Update total calculator count to include all calculators by modifying the migration script. 2026-03-08 19:34:07 -07:00
Ben
3ae77e02a0 refactor: Optimize sidebar data loading, update font to ExtraBold, and adjust blur effects for category cards and header. 2026-03-08 19:20:53 -07:00
Ben
1093208324 refactor: Centralize calculator statistics and categories into a new module, lazy load search functionality, and remove unused font preloads. 2026-03-08 19:15:42 -07:00
Ben
0114e00618 style: Reduce site logo gap and split 'How Do You' text into individual spans. 2026-03-08 14:01:36 -07:00
Ben
2794835590 feat: Add Python consistency tests for calculator definitions and refactor QuickConversionTable to explicitly pass calculator configuration to its row builder. 2026-03-08 13:41:50 -07:00
Ben
193affca27 fix: Embed JSON-LD scripts using {@html} for correct rendering. 2026-03-08 13:16:17 -07:00
Codex Agent
4c989ef1b3 Fix sidebar unit navigation to include reverse conversion pairs 2026-03-08 20:06:46 +00:00
Codex Agent
a1758a9074 Remove src param compatibility from shared links 2026-03-08 19:53:18 +00:00
Codex Agent
afe72b9ee1 Use minimal share params and source-aware hydration 2026-03-08 19:49:08 +00:00
Codex Agent
cf1114e8d8 Fix share-link serialization and tooltip positioning 2026-03-08 19:44:30 +00:00
Codex Agent
02b9c2411f Fix calculator link copy icon and tooltips 2026-03-08 19:40:30 +00:00
Codex Agent
2dca3654a8 Fix duplicate hasInputs declaration in Calculator 2026-03-08 19:31:29 +00:00
Codex Agent
51a26ad120 Update sidebar unit grouping and apply layout/page updates 2026-03-08 19:29:26 +00:00
Codex Agent
6a0347fd22 Sidebar updates 2026-03-08 19:27:44 +00:00
Codex Agent
edb08e3e5c Simplify share button 2026-03-08 18:58:55 +00:00
Codex Agent
700953194a Add share link button 2026-03-08 18:55:31 +00:00
34 changed files with 54382 additions and 544 deletions

15730
hdyc-svelte/sitemap.xml Normal file

File diff suppressed because it is too large Load Diff

116
hdyc-svelte/sitemap.xsd Normal file
View File

@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
elementFormDefault="qualified">
<xsd:annotation>
<xsd:documentation>
XML Schema for Sitemap files.
Last Modifed 2008-03-26
</xsd:documentation>
</xsd:annotation>
<xsd:element name="urlset">
<xsd:annotation>
<xsd:documentation>
Container for a set of up to 50,000 document elements.
This is the root element of the XML file.
</xsd:documentation>
</xsd:annotation>
<xsd:complexType>
<xsd:sequence>
<xsd:any namespace="##other" minOccurs="0" maxOccurs="unbounded" processContents="strict"/>
<xsd:element name="url" type="tUrl" maxOccurs="unbounded"/>
</xsd:sequence>
</xsd:complexType>
</xsd:element>
<xsd:complexType name="tUrl">
<xsd:annotation>
<xsd:documentation>
Container for the data needed to describe a document to crawl.
</xsd:documentation>
</xsd:annotation>
<xsd:sequence>
<xsd:element name="loc" type="tLoc"/>
<xsd:element name="lastmod" type="tLastmod" minOccurs="0"/>
<xsd:element name="changefreq" type="tChangeFreq" minOccurs="0"/>
<xsd:element name="priority" type="tPriority" minOccurs="0"/>
<xsd:any namespace="##other" minOccurs="0" maxOccurs="unbounded" processContents="strict"/>
</xsd:sequence>
</xsd:complexType>
<xsd:simpleType name="tLoc">
<xsd:annotation>
<xsd:documentation>
REQUIRED: The location URI of a document.
The URI must conform to RFC 2396 (http://www.ietf.org/rfc/rfc2396.txt).
</xsd:documentation>
</xsd:annotation>
<xsd:restriction base="xsd:anyURI">
<xsd:minLength value="12"/>
<xsd:maxLength value="2048"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="tLastmod">
<xsd:annotation>
<xsd:documentation>
OPTIONAL: The date the document was last modified. The date must conform
to the W3C DATETIME format (http://www.w3.org/TR/NOTE-datetime).
Example: 2005-05-10
Lastmod may also contain a timestamp.
Example: 2005-05-10T17:33:30+08:00
</xsd:documentation>
</xsd:annotation>
<xsd:union>
<xsd:simpleType>
<xsd:restriction base="xsd:date"/>
</xsd:simpleType>
<xsd:simpleType>
<xsd:restriction base="xsd:dateTime"/>
</xsd:simpleType>
</xsd:union>
</xsd:simpleType>
<xsd:simpleType name="tChangeFreq">
<xsd:annotation>
<xsd:documentation>
OPTIONAL: Indicates how frequently the content at a particular URL is
likely to change. The value "always" should be used to describe
documents that change each time they are accessed. The value "never"
should be used to describe archived URLs. Please note that web
crawlers may not necessarily crawl pages marked "always" more often.
Consider this element as a friendly suggestion and not a command.
</xsd:documentation>
</xsd:annotation>
<xsd:restriction base="xsd:string">
<xsd:enumeration value="always"/>
<xsd:enumeration value="hourly"/>
<xsd:enumeration value="daily"/>
<xsd:enumeration value="weekly"/>
<xsd:enumeration value="monthly"/>
<xsd:enumeration value="yearly"/>
<xsd:enumeration value="never"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="tPriority">
<xsd:annotation>
<xsd:documentation>
OPTIONAL: The priority of a particular URL relative to other pages
on the same site. The value for this element is a number between
0.0 and 1.0 where 0.0 identifies the lowest priority page(s).
The default priority of a page is 0.5. Priority is used to select
between pages on your site. Setting a priority of 1.0 for all URLs
will not help you, as the relative priority of pages on your site
is what will be considered.
</xsd:documentation>
</xsd:annotation>
<xsd:restriction base="xsd:decimal">
<xsd:minInclusive value="0.0"/>
<xsd:maxInclusive value="1.0"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>

View File

@@ -10,7 +10,7 @@
font-family: 'Inter'; font-family: 'Inter';
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
font-display: swap; font-display: optional;
src: url('/fonts/inter/Inter-Medium.woff2') format('woff2'); src: url('/fonts/inter/Inter-Medium.woff2') format('woff2');
} }
@@ -18,7 +18,7 @@
font-family: 'Inter'; font-family: 'Inter';
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; font-display: optional;
src: url('/fonts/inter/Inter-SemiBold.woff2') format('woff2'); src: url('/fonts/inter/Inter-SemiBold.woff2') format('woff2');
} }
@@ -26,7 +26,7 @@
font-family: 'Inter'; font-family: 'Inter';
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
font-display: swap; font-display: optional;
src: url('/fonts/inter/Inter-Bold.woff2') format('woff2'); src: url('/fonts/inter/Inter-Bold.woff2') format('woff2');
} }
@@ -42,7 +42,7 @@
font-family: 'JetBrains Mono'; font-family: 'JetBrains Mono';
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
font-display: swap; font-display: optional;
src: url('/fonts/jetbrains-mono/JetBrainsMono-Regular.woff2') format('woff2'); src: url('/fonts/jetbrains-mono/JetBrainsMono-Regular.woff2') format('woff2');
} }
@@ -50,7 +50,7 @@
font-family: 'JetBrains Mono'; font-family: 'JetBrains Mono';
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
font-display: swap; font-display: optional;
src: url('/fonts/jetbrains-mono/JetBrainsMono-Medium.woff2') format('woff2'); src: url('/fonts/jetbrains-mono/JetBrainsMono-Medium.woff2') format('woff2');
} }
@@ -58,7 +58,7 @@
font-family: 'JetBrains Mono'; font-family: 'JetBrains Mono';
font-style: normal; font-style: normal;
font-weight: 600; font-weight: 600;
font-display: swap; font-display: optional;
src: url('/fonts/jetbrains-mono/JetBrainsMono-SemiBold.woff2') format('woff2'); src: url('/fonts/jetbrains-mono/JetBrainsMono-SemiBold.woff2') format('woff2');
} }
@@ -343,11 +343,16 @@ a:focus-visible {
justify-content: space-between; justify-content: space-between;
padding: 0 1.5rem; padding: 0 1.5rem;
background: var(--header-bg); background: var(--header-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
} }
@media (min-width: 1025px) {
.site-header {
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
}
}
.header-left { .header-left {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -357,7 +362,7 @@ a:focus-visible {
.site-logo { .site-logo {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem; gap: 0.3rem;
text-decoration: none; text-decoration: none;
color: var(--text); color: var(--text);
font-weight: 800; font-weight: 800;
@@ -664,11 +669,17 @@ a:focus-visible {
.calc-list { .calc-list {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
width: 100%;
max-width: calc(4 * 360px + 3 * 0.75rem);
margin: 0 auto;
gap: 0.75rem; gap: 0.75rem;
} }
.calc-list-item { .calc-list-item {
display: block; display: block;
position: relative;
--calc-tooltip-left: 50%;
--calc-tooltip-translate: -0.35rem;
padding: 1rem 1.25rem; padding: 1rem 1.25rem;
background: var(--card-bg); background: var(--card-bg);
border: 1px solid var(--border); border: 1px solid var(--border);
@@ -684,11 +695,40 @@ a:focus-visible {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
color: var(--accent); color: var(--accent);
z-index: 20;
} }
.calc-list-item:focus-visible { .calc-list-item:focus-visible {
outline: none; outline: none;
border-color: var(--accent); border-color: var(--accent);
box-shadow: 0 0 0 3px var(--accent-glow); box-shadow: 0 0 0 3px var(--accent-glow);
z-index: 20;
}
.calc-list-tooltip {
position: absolute;
bottom: var(--calc-tooltip-bottom, calc(100% + 0.4rem));
top: var(--calc-tooltip-top, auto);
left: var(--calc-tooltip-left, 50%);
background: color-mix(in srgb, var(--bg-elevated) 92%, black 8%);
color: var(--text);
border: 1px solid var(--border);
border-radius: 6px;
padding: 0.35rem 0.55rem;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.02em;
pointer-events: none;
opacity: 0;
visibility: hidden;
transform: translate(-50%, calc(var(--calc-tooltip-translate, -0.35rem) + 0.15rem));
transition: opacity 0.2s ease, transform 0.2s ease, visibility 0.2s ease;
white-space: nowrap;
z-index: 10;
}
.calc-list-item:hover .calc-list-tooltip,
.calc-list-item:focus-visible .calc-list-tooltip {
opacity: 1;
visibility: visible;
transform: translate(-50%, var(--calc-tooltip-translate, -0.35rem));
} }
/* ─── Related Converters ─────────────────────────────────── */ /* ─── Related Converters ─────────────────────────────────── */

View File

@@ -2,7 +2,9 @@
<html lang="en" data-theme="dark" data-palette="classic"> <html lang="en" data-theme="dark" data-palette="classic">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> %sveltekit.head%
<link <link
rel="preload" rel="preload"
href="/fonts/inter/Inter-Regular.woff2" href="/fonts/inter/Inter-Regular.woff2"
@@ -10,27 +12,6 @@
type="font/woff2" type="font/woff2"
crossorigin crossorigin
/> />
<link
rel="preload"
href="/fonts/inter/Inter-Medium.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link
rel="preload"
href="/fonts/inter/Inter-SemiBold.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link
rel="preload"
href="/fonts/inter/Inter-Bold.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link <link
rel="preload" rel="preload"
href="/fonts/inter/Inter-ExtraBold.woff2" href="/fonts/inter/Inter-ExtraBold.woff2"
@@ -38,27 +19,25 @@
type="font/woff2" type="font/woff2"
crossorigin crossorigin
/> />
<link <style>
rel="preload" /* Critical CSS inlined to eliminate render-blocking stylesheet for FCP/LCP */
href="/fonts/jetbrains-mono/JetBrainsMono-Regular.woff2" :root{--bg:#0c0f14;--bg-elevated:#12161e;--card-bg:rgba(18,22,30,.85);--border:rgba(255,255,255,.08);--text:#e8ecf4;--text-muted:#7b8498;--accent:#10b981;--accent-dark:#059669;--accent-glow:rgba(16,185,129,.15);--accent-gradient:linear-gradient(135deg,#10b981,#06b6d4);--header-bg:rgba(12,15,20,.85);--header-h:64px;--font-body:'Inter',-apple-system,BlinkMacSystemFont,sans-serif}
as="font" :root[data-theme='light']{--bg:#f8fafc;--bg-elevated:#fff;--card-bg:#fff;--border:rgba(15,23,42,.12);--text:#0f172a;--text-muted:#475569;--accent:#047857;--accent-dark:#065f46;--accent-glow:rgba(16,185,129,.15);--accent-gradient:linear-gradient(135deg,#10b981,#06b6d4);--header-bg:rgba(255,255,255,.95)}
type="font/woff2" *,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
crossorigin html{height:100%;-webkit-text-size-adjust:100%}
/> body{min-height:100vh;font-family:var(--font-body);background:var(--bg);color:var(--text);line-height:1.6}
<link .site-header{position:sticky;top:0;z-index:50;height:var(--header-h);display:flex;align-items:center;justify-content:space-between;padding:0 1.5rem;background:var(--header-bg);border-bottom:1px solid var(--border)}
rel="preload" .header-left{display:flex;align-items:center;gap:.75rem}
href="/fonts/jetbrains-mono/JetBrainsMono-Medium.woff2" .site-logo{display:flex;align-items:center;gap:.3rem;text-decoration:none;color:var(--text);font-weight:800;font-size:1.15rem;letter-spacing:-.02em;white-space:nowrap}
as="font" .site-logo .logo-accent{color:var(--accent)}.site-logo .logo-domain{color:var(--text-muted);font-weight:500}
type="font/woff2" .hero{text-align:center;padding:3rem 1rem 2rem;margin-bottom:1rem}
crossorigin .hero h1{font-size:2.5rem;font-weight:800;letter-spacing:-.03em;line-height:1.15;margin-bottom:.75rem;background:var(--accent-gradient);-webkit-background-clip:text;-webkit-text-fill-color:transparent;background-clip:text}
/> .hero p{color:var(--text-muted);font-size:1.1rem;max-width:500px;margin:0 auto 1.5rem}
<link .stats-row{display:flex;justify-content:center;gap:2.5rem;margin-bottom:2.5rem;padding:1rem 0}
rel="preload" .stat-num{font-size:1.8rem;font-weight:800;color:var(--accent)}
href="/fonts/jetbrains-mono/JetBrainsMono-SemiBold.woff2" .stat-label{font-size:.78rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em}
as="font" .main-content{flex:1;width:100%;max-width:900px;margin:0 auto;padding:2rem 1.25rem 3rem}
type="font/woff2" </style>
crossorigin
/>
<script> <script>
(function () { (function () {
const doc = document.documentElement; const doc = document.documentElement;
@@ -77,8 +56,9 @@
} }
})(); })();
</script> </script>
%sveltekit.head% <!-- SvelteKit head tags moved to top of <head> -->
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div> <div style="display: contents">%sveltekit.body%</div>
</body> </body>

View File

@@ -13,7 +13,7 @@ const MIME_TYPES: Record<string, string> = {
'.otf': 'font/otf' '.otf': 'font/otf'
}; };
const HTML_CACHE_CONTROL = 'public, max-age=0, must-revalidate'; const HTML_CACHE_CONTROL = 'public, max-age=0, s-maxage=3600, stale-while-revalidate=86400';
const IMMUTABLE_ASSET_CACHE_CONTROL = 'public, max-age=31536000, immutable'; const IMMUTABLE_ASSET_CACHE_CONTROL = 'public, max-age=31536000, immutable';
const ASSET_404_CACHE_CONTROL = 'no-store'; const ASSET_404_CACHE_CONTROL = 'no-store';
const LONG_CACHE_EXTENSIONS = new Set([ const LONG_CACHE_EXTENSIONS = new Set([

View File

@@ -2,7 +2,9 @@
import { solve } from '$lib/engine'; import { solve } from '$lib/engine';
import type { CalculatorDef } from '$lib/data/calculators'; import type { CalculatorDef } from '$lib/data/calculators';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { onMount } from 'svelte'; import { onDestroy, onMount } from 'svelte';
import { browser } from '$app/environment';
import { getConversionRateText } from '$lib/utils/conversionRate';
import QuickDefinitionCard from '$lib/components/QuickDefinitionCard.svelte'; import QuickDefinitionCard from '$lib/components/QuickDefinitionCard.svelte';
import QuickConversionExample from '$lib/components/QuickConversionExample.svelte'; import QuickConversionExample from '$lib/components/QuickConversionExample.svelte';
import QuickConversionTable from '$lib/components/QuickConversionTable.svelte'; import QuickConversionTable from '$lib/components/QuickConversionTable.svelte';
@@ -14,18 +16,34 @@
let val2 = ''; let val2 = '';
let val3 = ''; let val3 = '';
let activeField: 1 | 2 | 3 = 1; let activeField: 1 | 2 | 3 = 1;
let swapState: { originalField: 1 | 2; originalValue: string } | null = null; let swapState: { originalField: 1 | 2; originalValue: string | number | null } | null = null;
let copyStatus: 'idle' | 'copied' | 'failed' = 'idle';
let statusTimeout: ReturnType<typeof setTimeout> | null = null;
let tooltipFadeTimeout: ReturnType<typeof setTimeout> | null = null;
let tooltipHideTimeout: ReturnType<typeof setTimeout> | null = null;
let showCopyTooltip = false;
let isTooltipFading = false;
let showHoverTooltip = false;
let footerControlsEl: HTMLDivElement | null = null;
let tooltipX = 20;
let copyStatusMessage = '';
let initializedSlug: string | null = null;
let conversionRateText: string | null = null;
$: has3 = ['3col', '3col-mul'].includes(config.type) || !!config.labels.in3; $: has3 = ['3col', '3col-mul'].includes(config.type) || !!config.labels.in3;
$: isTextInput = ['base', 'text-bin', 'bin-text', 'dec-frac', 'dms-dd', 'dd-dms'].includes(config.type); $: isTextInput = ['base', 'text-bin', 'bin-text', 'dec-frac', 'dms-dd', 'dd-dms'].includes(config.type);
$: conversionRateText = getConversionRateText(config);
// Clear inputs on config (route) change // Clear inputs only when navigating to a different calculator slug.
$: if (config) { $: if (config?.slug) {
if (!paramsInitializing) clear(); if (initializedSlug === null) {
initializedSlug = config.slug;
} else if (initializedSlug !== config.slug) {
initializedSlug = config.slug;
clear();
}
} }
let paramsInitializing = true;
function handleInput(source: 1 | 2 | 3, options?: { preserveSwap?: boolean }) { function handleInput(source: 1 | 2 | 3, options?: { preserveSwap?: boolean }) {
if (!options?.preserveSwap) { if (!options?.preserveSwap) {
swapState = null; swapState = null;
@@ -64,12 +82,147 @@
swapState = null; swapState = null;
} }
function buildShareUrl() {
const params = new URLSearchParams();
const v1 = toQueryValue(val1);
const v2 = toQueryValue(val2);
const v3 = toQueryValue(val3);
const source: 1 | 2 | 3 = has3 ? activeField : (activeField === 2 ? 2 : 1);
if (!has3) {
const sourceValue = source === 1 ? v1 : v2;
if (sourceValue !== null) {
params.set(source === 1 ? 'v1' : 'v2', sourceValue);
}
} else if (source === 3) {
if (v2 !== null) params.set('v2', v2);
if (v3 !== null) params.set('v3', v3);
} else {
if (v1 !== null) params.set('v1', v1);
if (v2 !== null) params.set('v2', v2);
}
const shareUrl = new URL($page.url);
shareUrl.search = params.toString();
return shareUrl.toString();
}
function toQueryValue(value: unknown): string | null {
if (value === null || value === undefined) {
return null;
}
const stringValue = String(value);
return stringValue.trim() ? stringValue : null;
}
async function copyText(text: string) {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
return;
}
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.setAttribute('readonly', '');
textArea.style.position = 'absolute';
textArea.style.left = '-9999px';
document.body.appendChild(textArea);
textArea.select();
const copied = document.execCommand('copy');
document.body.removeChild(textArea);
if (!copied) {
throw new Error('execCommand copy failed');
}
}
function triggerCopyTooltip() {
if (tooltipFadeTimeout) clearTimeout(tooltipFadeTimeout);
if (tooltipHideTimeout) clearTimeout(tooltipHideTimeout);
showCopyTooltip = true;
isTooltipFading = false;
tooltipFadeTimeout = setTimeout(() => {
isTooltipFading = true;
}, 900);
tooltipHideTimeout = setTimeout(() => {
showCopyTooltip = false;
isTooltipFading = false;
}, 1300);
}
function updateTooltipPosition(event: MouseEvent) {
if (!footerControlsEl) return;
const rect = footerControlsEl.getBoundingClientRect();
const x = event.clientX - rect.left;
tooltipX = Math.max(12, Math.min(rect.width - 12, x));
}
function positionTooltipFromButton(button: HTMLButtonElement) {
if (!footerControlsEl) return;
const controlsRect = footerControlsEl.getBoundingClientRect();
const buttonRect = button.getBoundingClientRect();
const centerX = buttonRect.left - controlsRect.left + buttonRect.width / 2;
tooltipX = Math.max(12, Math.min(controlsRect.width - 12, centerX));
}
async function copyLink() {
if (!browser) return;
const url = buildShareUrl();
try {
await copyText(url);
copyStatus = 'copied';
triggerCopyTooltip();
} catch (error) {
console.error('Failed to copy link', error);
copyStatus = 'failed';
} finally {
if (statusTimeout) {
clearTimeout(statusTimeout);
}
statusTimeout = setTimeout(() => {
copyStatus = 'idle';
}, 2000);
}
}
$: copyStatusMessage =
copyStatus === 'copied'
? 'Link copied to clipboard'
: copyStatus === 'failed'
? 'Failed to copy link'
: '';
onMount(() => { onMount(() => {
const params = new URLSearchParams($page.url.search); const params = new URLSearchParams($page.url.search);
if (params.has('v1')) { val1 = params.get('v1')!; handleInput(1); } const hasV1 = params.has('v1');
else if (params.has('v2')) { val2 = params.get('v2')!; handleInput(2); } const hasV2 = params.has('v2');
else if (params.has('v3') && has3) { val3 = params.get('v3')!; handleInput(3); } const hasV3 = has3 && params.has('v3');
setTimeout(() => { paramsInitializing = false; }, 0);
if (has3 && hasV2 && hasV3) {
val2 = params.get('v2') ?? '';
val3 = params.get('v3') ?? '';
handleInput(3);
} else if (has3 && hasV1 && hasV2) {
val1 = params.get('v1') ?? '';
val2 = params.get('v2') ?? '';
handleInput(1);
} else if (hasV1) {
val1 = params.get('v1') ?? '';
handleInput(1);
} else if (hasV2) {
val2 = params.get('v2') ?? '';
handleInput(2);
} else if (hasV3) {
val3 = params.get('v3') ?? '';
handleInput(3);
}
});
onDestroy(() => {
if (statusTimeout) clearTimeout(statusTimeout);
if (tooltipFadeTimeout) clearTimeout(tooltipFadeTimeout);
if (tooltipHideTimeout) clearTimeout(tooltipHideTimeout);
}); });
</script> </script>
@@ -142,12 +295,54 @@
</div> </div>
<div class="calc-footer"> <div class="calc-footer">
<button type="button" class="clear-btn" on:click={clear} aria-label="Clear calculator inputs"> <div class="footer-controls" bind:this={footerControlsEl}>
Clear <button type="button" class="clear-btn" on:click={clear} aria-label="Clear calculator inputs">
</button> Clear
{#if config.factor && config.type === 'standard'} </button>
<button
type="button"
class="icon-btn"
on:click={(event) => {
positionTooltipFromButton(event.currentTarget as HTMLButtonElement);
copyLink();
}}
on:mouseenter={(event) => {
showHoverTooltip = true;
updateTooltipPosition(event);
}}
on:mousemove={updateTooltipPosition}
on:mouseleave={() => (showHoverTooltip = false)}
on:focus={(event) => {
showHoverTooltip = true;
positionTooltipFromButton(event.currentTarget as HTMLButtonElement);
}}
on:blur={() => (showHoverTooltip = false)}
aria-label="Copy calculator link"
>
<svg viewBox="0 0 24 24" role="presentation" aria-hidden="true">
<path
d="M13.5 6.5l1.5-1.5a4.243 4.243 0 0 1 6 6L19.5 12.5M10.5 17.5L9 19a4.243 4.243 0 1 1-6-6L4.5 11.5M8 16l8-8"
fill="none"
stroke="currentColor"
stroke-width="1.9"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</button>
{#if showHoverTooltip && !showCopyTooltip}
<span class="copy-tooltip hover" style={`left: ${tooltipX}px;`}>Copy link</span>
{/if}
{#if showCopyTooltip && copyStatus === 'copied'}
<span class="copy-tooltip" class:fading={isTooltipFading} style={`left: ${tooltipX}px;`}>Link copied!</span>
{/if}
<span class="sr-only" aria-live="polite">
{copyStatusMessage}
</span>
</div>
{#if conversionRateText}
<span class="formula-hint"> <span class="formula-hint">
1 {config.labels.in1} = {config.factor}{config.offset ? ` + ${config.offset}` : ''} {config.labels.in2} {conversionRateText}
</span> </span>
{/if} {/if}
</div> </div>
@@ -284,6 +479,13 @@
padding: 1rem 2rem 1.25rem; padding: 1rem 2rem 1.25rem;
border-top: 1px solid var(--border); border-top: 1px solid var(--border);
} }
.footer-controls {
display: flex;
align-items: center;
gap: 0.5rem;
position: relative;
}
.clear-btn { .clear-btn {
padding: 0.5rem 1.25rem; padding: 0.5rem 1.25rem;
border: 1px solid var(--border); border: 1px solid var(--border);
@@ -303,6 +505,69 @@
outline: none; outline: none;
box-shadow: 0 0 0 3px var(--accent-glow); box-shadow: 0 0 0 3px var(--accent-glow);
} }
.icon-btn {
width: 40px;
height: 40px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--section-bg);
color: var(--text);
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
cursor: pointer;
transition: background 0.2s, border-color 0.2s;
}
.icon-btn svg {
width: 1.1rem;
height: 1.1rem;
}
.icon-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.icon-btn:not(:disabled):hover {
border-color: var(--accent);
background: var(--surface-hover);
}
.copy-tooltip {
position: absolute;
bottom: calc(100% + 0.4rem);
background: color-mix(in srgb, var(--accent) 90%, black 10%);
color: #fff;
border-radius: 6px;
padding: 0.35rem 0.55rem;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.02em;
pointer-events: none;
opacity: 1;
transform: translate(-50%, 0);
transition: opacity 0.35s ease, transform 0.35s ease;
white-space: nowrap;
z-index: 2;
}
.copy-tooltip.hover {
background: var(--section-bg);
color: var(--text);
border: 1px solid var(--border);
}
.copy-tooltip.fading {
opacity: 0;
transform: translate(-50%, -0.2rem);
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.formula-hint { .formula-hint {
font-size: 0.78rem; font-size: 0.78rem;
color: var(--text-muted); color: var(--text-muted);

View File

@@ -24,8 +24,6 @@
text-decoration: none; text-decoration: none;
color: var(--text); color: var(--text);
transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s; transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
cursor: pointer; cursor: pointer;
} }
.category-card:hover { .category-card:hover {

View File

@@ -1,6 +1,7 @@
<script lang="ts"> <script lang="ts">
import { solve } from '$lib/engine'; import { solve } from '$lib/engine';
import type { CalculatorDef } from '$lib/data/calculators'; import type { CalculatorDef } from '$lib/data/calculators';
import { formatConversionValue } from '$lib/utils/formatConversionValue';
export let config: CalculatorDef; export let config: CalculatorDef;
@@ -17,40 +18,29 @@
? solve(config, 1, exampleInput.toString(), '', '') ? solve(config, 1, exampleInput.toString(), '', '')
: null; : null;
$: offset = config.offset ?? 0; $: offset = config.offset ?? 0;
$: formulaExpression = supportsExample $: hasOffset = Boolean(offset);
? `${exampleInput} × ${config.factor}${offset ? ` + ${offset}` : ''}` $: formattedFactorValue = supportsExample
? formatConversionValue(config.factor)
: '';
$: formattedOffsetValue = hasOffset
? formatConversionValue(offset)
: '';
$: formulaExpression = supportsExample
? `${exampleInput} × ${formattedFactorValue}${hasOffset ? ` + ${formattedOffsetValue}` : ''}`
: ''; : '';
const formatExampleValue = (value: number | null): string => {
if (value === null || Number.isNaN(value)) {
return '—';
}
if (!Number.isFinite(value)) {
return value.toString();
}
if (value === 0) {
return '0';
}
const rounded = parseFloat(value.toFixed(6));
if (rounded !== 0) {
return rounded.toString();
}
const precise = value.toFixed(12).replace(/\.?0+$/, '');
return precise || '0';
};
$: reverseExampleValue = $: reverseExampleValue =
supportsExample && config.factor !== 0 supportsExample && config.factor !== 0
? (1 - offset) / config.factor ? (1 - offset) / config.factor
: null; : null;
$: formattedReverseValue = formatExampleValue(reverseExampleValue); $: formattedReverseValue = formatConversionValue(reverseExampleValue);
</script> </script>
{#if supportsExample && result} {#if supportsExample && result}
<section class="example-card"> <section class="example-card">
<h3>How to convert {config.labels.in1} to {config.labels.in2}</h3> <h3>How to convert {config.labels.in1} to {config.labels.in2}</h3>
<p class="example-note"> <p class="example-note">
1 {config.labels.in1} = {config.factor}{config.offset ? ` + ${config.offset}` : ''} {config.labels.in2} 1 {config.labels.in1} = {formattedFactorValue}{hasOffset ? ` + ${formattedOffsetValue}` : ''} {config.labels.in2}
</p> </p>
<p class="example-note"> <p class="example-note">
1 {config.labels.in2} = {formattedReverseValue} {config.labels.in1} 1 {config.labels.in2} = {formattedReverseValue} {config.labels.in1}

View File

@@ -1,20 +1,18 @@
<script lang="ts"> <script lang="ts">
import { solve } from '$lib/engine'; import { solve } from '$lib/engine';
import { parseScientificNotation, type ScientificNotationParts } from '$lib/utils/formatScientific';
import type { CalculatorDef } from '$lib/data/calculators'; import type { CalculatorDef } from '$lib/data/calculators';
export let config: CalculatorDef; export let config: CalculatorDef;
const numericSamples = [0.1, 0.5, 1, 2, 5, 10, 20, 50, 100]; const numericSamples = [0.1, 0.5, 1, 2, 5, 10, 20, 50, 100];
type Row = { input: number; output: string; scientific?: ScientificNotationParts }; type Row = { input: number; output: string };
const buildRow = (value: number): Row => { const buildRow = (value: number, c: CalculatorDef): Row => {
const formatted = solve(config, 1, value.toString(), '', ''); const formatted = solve(c, 1, value.toString(), '', '');
return { return {
input: value, input: value,
output: formatted.val2 || '—', output: formatted.val2 || '—',
scientific: formatted.val2 ? parseScientificNotation(formatted.val2) ?? undefined : undefined,
}; };
}; };
@@ -24,8 +22,8 @@
let outputLabel = 'target units'; let outputLabel = 'target units';
$: supportsTable = ['standard', 'inverse'].includes(config.type); $: supportsTable = ['standard', 'inverse'].includes(config.type);
$: rows = supportsTable $: rows = (config && supportsTable)
? numericSamples.map(buildRow) ? numericSamples.map(v => buildRow(v, config))
: []; : [];
$: inputLabel = config.labels?.in1 ?? 'source units'; $: inputLabel = config.labels?.in1 ?? 'source units';
$: outputLabel = config.labels?.in2 ?? 'target units'; $: outputLabel = config.labels?.in2 ?? 'target units';
@@ -44,16 +42,7 @@
<div class="chart-row"> <div class="chart-row">
<p class="chart-statement"> <p class="chart-statement">
Converting {row.input} <span class="chart-unit">{inputLabel}</span> into <span class="chart-unit">{outputLabel}</span> equals Converting {row.input} <span class="chart-unit">{inputLabel}</span> into <span class="chart-unit">{outputLabel}</span> equals
<span class="chart-output-value"> <span class="chart-output-value">{row.output}</span>
{#if row.scientific}
<span class="scientific-base">{row.scientific.base}</span>
<span class="scientific-suffix">
×10<sup>{row.scientific.exponent}</sup>
</span>
{:else}
{row.output}
{/if}
</span>
<span class="chart-output-unit">{outputLabel}</span>. <span class="chart-output-unit">{outputLabel}</span>.
</p> </p>
</div> </div>
@@ -105,22 +94,6 @@
color: var(--text); color: var(--text);
} }
.scientific-base {
margin-right: 0.15rem;
}
.scientific-suffix {
display: inline-flex;
align-items: baseline;
gap: 0.15rem;
font-size: 0.85em;
}
.scientific-suffix sup {
font-size: 0.75em;
vertical-align: super;
}
.chart-unit, .chart-unit,
.chart-output-unit { .chart-output-unit {
font-variant: petite-caps; font-variant: petite-caps;

View File

@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { getDefinition } from '$lib/data/unitDefinitions'; import { getDefinition } from '$lib/data/unitDefinitions';
import type { CalculatorDef } from '$lib/data/calculators'; import type { CalculatorDef } from '$lib/data/calculatorLoader';
export let config: CalculatorDef; export let config: CalculatorDef;
@@ -11,8 +11,10 @@
$: label1 = config.labels.in1 || 'Unit 1'; $: label1 = config.labels.in1 || 'Unit 1';
$: label2 = config.labels.in2 || 'Unit 2'; $: label2 = config.labels.in2 || 'Unit 2';
$: def1 = getDefinition(label1, config.category); $: {
$: def2 = getDefinition(label2, config.category); getDefinition(label1, config.category).then(d => { def1 = d; });
getDefinition(label2, config.category).then(d => { def2 = d; });
}
</script> </script>
<section class="definition-card"> <section class="definition-card">

View File

@@ -1,15 +1,19 @@
<script lang="ts"> <script lang="ts">
import { searchCalculators } from '$lib/data/calculators';
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { loadCalculators, searchCalculators, type CalculatorDef } from '$lib/data/calculatorLoader';
export let idPrefix = 'search'; export let idPrefix = 'search';
let query = ''; let query = '';
let focused = false; let focused = false;
let selectedIndex = -1; let selectedIndex = -1;
let lastQuery = ''; let lastQuery = '';
let allCalcs: CalculatorDef[] = [];
$: results = query.length >= 2 ? searchCalculators(query).slice(0, 8) : []; $: if (query.length >= 1 && allCalcs.length === 0) {
loadCalculators().then(data => { allCalcs = data; });
}
$: results = (query.length >= 2 && allCalcs.length > 0) ? searchCalculators(allCalcs, query).slice(0, 8) : [];
$: listboxId = `${idPrefix}-listbox`; $: listboxId = `${idPrefix}-listbox`;
$: inputId = `${idPrefix}-input`; $: inputId = `${idPrefix}-input`;
$: isOpen = focused && results.length > 0; $: isOpen = focused && results.length > 0;

View File

@@ -2,7 +2,18 @@
import { browser } from '$app/environment'; import { browser } from '$app/environment';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { page } from '$app/stores'; import { page } from '$app/stores';
import { categories, getCalculatorsByCategory, type CalculatorDef } from '$lib/data/calculators'; import { categories } from '$lib/data/stats';
import { loadCalculators, type CalculatorDef } from '$lib/data/calculatorLoader';
let allCalculators: CalculatorDef[] = [];
let isLoaded = false;
async function loadData() {
if (isLoaded || !browser) return;
const data = await loadCalculators();
allCalculators = data;
isLoaded = true;
}
let expandedCategory = ''; let expandedCategory = '';
let expandedUnits: Record<string, string> = {}; let expandedUnits: Record<string, string> = {};
@@ -23,50 +34,85 @@
type UnitGroup = { type UnitGroup = {
label: string; label: string;
conversions: CalculatorDef[]; conversions: UnitConversionLink[];
}; };
type UnitBucket = { type UnitBucket = {
label: string; label: string;
conversions: CalculatorDef[]; conversions: UnitConversionLink[];
}; };
const sortConversionsForUnit = (conversions: CalculatorDef[], unitLabel: string) => { type UnitConversionLink = {
const normalizedUnit = unitLabel.toLowerCase(); name: string;
return conversions.slice().sort((a, b) => { slug: string;
const aIsSource = a.labels.in1?.toLowerCase() === normalizedUnit; sortKey: string;
const bIsSource = b.labels.in1?.toLowerCase() === normalizedUnit;
if (aIsSource !== bIsSource) {
return aIsSource ? -1 : 1;
}
return a.name.localeCompare(b.name);
});
}; };
const sortConversionsForUnit = (conversions: UnitConversionLink[]) =>
conversions.slice().sort((a, b) => a.name.localeCompare(b.name));
const toPairKey = (unitA: string, unitB: string) =>
[unitA.toLowerCase(), unitB.toLowerCase()].sort().join('::');
const toDirectionKey = (fromUnit: string, toUnit: string) =>
`${fromUnit.toLowerCase()}::${toUnit.toLowerCase()}`;
function addConversion(
buckets: Map<string, UnitBucket>,
fromUnit: string,
toUnit: string,
slug: string
) {
const bucketKey = fromUnit.toLowerCase();
const directionKey = toDirectionKey(fromUnit, toUnit);
const conversion: UnitConversionLink = {
name: `${fromUnit} to ${toUnit}`,
slug,
sortKey: directionKey,
};
const existing = buckets.get(bucketKey);
if (existing) {
if (!existing.conversions.some(link => link.sortKey === directionKey)) {
existing.conversions.push(conversion);
}
return;
}
buckets.set(bucketKey, {
label: fromUnit,
conversions: [conversion],
});
}
$: categoryUnitGroups = Object.entries(categories).map(([key, meta]) => { $: categoryUnitGroups = Object.entries(categories).map(([key, meta]) => {
const buckets = new Map<string, UnitBucket>(); const buckets = new Map<string, UnitBucket>();
const calcs = getCalculatorsByCategory(key);
if (!isLoaded) {
return { key, meta, units: [] };
}
const calcs = allCalculators.filter(c => c.category === key && !c.hidden);
const canonicalByPair = new Map<string, CalculatorDef>();
calcs.forEach(calc => { calcs.forEach(calc => {
[calc.labels.in1, calc.labels.in2].forEach(unit => { const pairKey = toPairKey(calc.labels.in1, calc.labels.in2);
const key = unit.toLowerCase(); const existing = canonicalByPair.get(pairKey);
const existing = buckets.get(key); if (!existing || calc.slug.localeCompare(existing.slug) < 0) {
if (existing) { canonicalByPair.set(pairKey, calc);
existing.conversions.push(calc); }
} else { });
buckets.set(key, {
label: unit, canonicalByPair.forEach(calc => {
conversions: [calc], addConversion(buckets, calc.labels.in1, calc.labels.in2, calc.slug);
}); addConversion(buckets, calc.labels.in2, calc.labels.in1, calc.slug);
}
});
}); });
const units = [...buckets.entries()] const units = [...buckets.entries()]
.sort(([a], [b]) => a.localeCompare(b)) .sort(([a], [b]) => a.localeCompare(b))
.map(([, bucket]) => ({ .map(([, bucket]) => ({
label: bucket.label, label: bucket.label,
conversions: sortConversionsForUnit(bucket.conversions, bucket.label), conversions: sortConversionsForUnit(bucket.conversions),
})); }));
return { key, meta, units }; return { key, meta, units };
@@ -131,6 +177,11 @@
} }
export let open = false; export let open = false;
$: if (browser && (isDesktop || open)) {
loadData();
}
$: isSidebarHidden = !isDesktop && !open; $: isSidebarHidden = !isDesktop && !open;
function closeSidebar() { function closeSidebar() {
@@ -188,14 +239,14 @@
</button> </button>
{#if expandedUnits[group.key] === unit.label} {#if expandedUnits[group.key] === unit.label}
<ul class="unit-list"> <ul class="unit-list">
{#each unit.conversions as calc} {#each unit.conversions as conversion}
<li> <li>
<a <a
href="/{calc.slug}" href="/{conversion.slug}"
class:current={currentPath === `/${calc.slug}`} class:current={currentPath === `/${conversion.slug}`}
aria-current={currentPath === `/${calc.slug}` ? 'page' : undefined} aria-current={currentPath === `/${conversion.slug}` ? 'page' : undefined}
> >
{calc.name} {conversion.name}
</a> </a>
</li> </li>
{/each} {/each}

View File

@@ -0,0 +1,53 @@
// Shared lazy loader fetches /data/calculators.json exactly once.
// Because this is a plain fetch (not a JS dynamic import), Vite will NOT
// emit a modulepreload for it, keeping the homepage bundle small.
export interface CalculatorDef {
slug: string;
name: string;
category: string;
type: string;
teaser: string;
labels: { in1: string; in2: string };
factor?: number;
offset?: number;
hidden?: boolean;
}
let cache: CalculatorDef[] | null = null;
let pending: Promise<CalculatorDef[]> | null = null;
const runtimeHost =
import.meta.env.PUBLIC_SITE_URL ??
(import.meta.env.DEV ? 'http://localhost:5173' : 'https://howdoyouconvert.com');
const getCalculatorsUrl = (): string =>
import.meta.env.SSR
? new URL('/data/calculators.json', runtimeHost).toString()
: '/data/calculators.json';
export async function loadCalculators(): Promise<CalculatorDef[]> {
if (cache) return cache;
if (pending) return pending;
const url = getCalculatorsUrl();
pending = fetch(url)
.then(r => r.json())
.then((data: CalculatorDef[]) => {
cache = data;
pending = null;
return data;
});
return pending;
}
export function searchCalculators(calcs: CalculatorDef[], query: string): CalculatorDef[] {
const q = query.toLowerCase();
return calcs.filter(c =>
(c.name.toLowerCase().includes(q) ||
c.slug.includes(q) ||
c.labels.in1.toLowerCase().includes(q) ||
c.labels.in2.toLowerCase().includes(q)) && !c.hidden
);
}

View File

@@ -17,26 +17,26 @@ export interface CalculatorDef {
} }
export const categories: Record<string, { label: string; icon: string }> = { export const categories: Record<string, { label: string; icon: string }> = {
length: { label: 'Length / Distance', icon: '📏' }, 'length': { "label": "Length / Distance", "icon": "📏" },
weight: { label: 'Weight / Mass', icon: '⚖️' }, 'weight': { "label": "Weight / Mass", "icon": "⚖️" },
temperature: { label: 'Temperature', icon: '🌡️' }, 'temperature': { "label": "Temperature", "icon": "🌡️" },
volume: { label: 'Volume', icon: '🧪' }, 'volume': { "label": "Volume", "icon": "🧪" },
fluids: { label: 'Fluids', icon: '💧' }, 'fluids': { "label": "Fluids", "icon": "💧" },
area: { label: 'Area', icon: '🔳' }, 'area': { "label": "Area", "icon": "🔳" },
speed: { label: 'Speed / Velocity', icon: '💨' }, 'speed': { "label": "Speed / Velocity", "icon": "💨" },
pressure: { label: 'Pressure', icon: '🔽' }, 'pressure': { "label": "Pressure", "icon": "🔽" },
energy: { label: 'Energy', icon: '⚡' }, 'energy': { "label": "Energy", "icon": "⚡" },
magnetism: { label: 'Magnetism', icon: '🧲' }, 'magnetism': { "label": "Magnetism", "icon": "🧲" },
power: { label: 'Power', icon: '🔌' }, 'power': { "label": "Power", "icon": "🔌" },
data: { label: 'Data Storage', icon: '💾' }, 'data': { "label": "Data Storage", "icon": "💾" },
time: { label: 'Time', icon: '⏱️' }, 'time': { "label": "Time", "icon": "⏱️" },
angle: { label: 'Angle', icon: '📐' }, 'angle': { "label": "Angle", "icon": "📐" },
'number-systems':{ label: 'Number Systems', icon: '🔢' }, 'number-systems': { "label": "Number Systems", "icon": "🔢" },
radiation: { label: 'Radiation', icon: '☢️' }, 'radiation': { "label": "Radiation", "icon": "☢️" },
electrical: { label: 'Electrical', icon: '🔋' }, 'electrical': { "label": "Electrical", "icon": "🔋" },
force: { label: 'Force / Torque', icon: '💪' }, 'force': { "label": "Force / Torque", "icon": "💪" },
light: { label: 'Light', icon: '💡' }, 'light': { "label": "Light", "icon": "💡" },
other: { label: 'Other', icon: '🔄' }, 'other': { "label": "Other", "icon": "🔄" },
}; };
export const calculators: CalculatorDef[] = [ export const calculators: CalculatorDef[] = [
@@ -374,12 +374,12 @@ export const calculators: CalculatorDef[] = [
{"slug": "microtesla-to-gauss", "name": "Microtesla to Gauss", "category": "magnetism", "type": "standard", "teaser": "Bring microtesla data into Gauss when studying legacy equipment.", "labels": {"in1": "Microtesla", "in2": "Gauss"}, "factor": 0.01, "hidden": true}, {"slug": "microtesla-to-gauss", "name": "Microtesla to Gauss", "category": "magnetism", "type": "standard", "teaser": "Bring microtesla data into Gauss when studying legacy equipment.", "labels": {"in1": "Microtesla", "in2": "Gauss"}, "factor": 0.01, "hidden": true},
{"slug": "gauss-to-nanotesla", "name": "Gauss to Nanotesla", "category": "magnetism", "type": "standard", "teaser": "Convert Gauss values into high-resolution nanotesla.", "labels": {"in1": "Gauss", "in2": "Nanotesla"}, "factor": 100000.0}, {"slug": "gauss-to-nanotesla", "name": "Gauss to Nanotesla", "category": "magnetism", "type": "standard", "teaser": "Convert Gauss values into high-resolution nanotesla.", "labels": {"in1": "Gauss", "in2": "Nanotesla"}, "factor": 100000.0},
{"slug": "nanotesla-to-gauss", "name": "Nanotesla to Gauss", "category": "magnetism", "type": "standard", "teaser": "Return nanotesla measurements back into Gauss.", "labels": {"in1": "Nanotesla", "in2": "Gauss"}, "factor": 1e-05, "hidden": true}, {"slug": "nanotesla-to-gauss", "name": "Nanotesla to Gauss", "category": "magnetism", "type": "standard", "teaser": "Return nanotesla measurements back into Gauss.", "labels": {"in1": "Nanotesla", "in2": "Gauss"}, "factor": 1e-05, "hidden": true},
{"slug": "webers-per-square-meter-to-teslas", "name": "Webers per square meter to Teslas", "category": "magnetism", "type": "standard", "teaser": "A Weber per square meter is exactly one Tesla.", "labels": {"in1": "Webers per square meter", "in2": "Teslas"}, "hidden": true}, {"slug": "webers-per-square-meter-to-teslas", "name": "Webers per square meter to Teslas", "category": "magnetism", "type": "standard", "teaser": "A Weber per square meter is exactly one Tesla.", "labels": {"in1": "Webers per square meter", "in2": "Teslas"}, "hidden": true, "factor": 1.0},
{"slug": "teslas-to-webers-per-square-meter", "name": "Teslas to Webers per square meter", "category": "magnetism", "type": "standard", "teaser": "Each Tesla equals one Weber per square meter.", "labels": {"in1": "Teslas", "in2": "Webers per square meter"}}, {"slug": "teslas-to-webers-per-square-meter", "name": "Teslas to Webers per square meter", "category": "magnetism", "type": "standard", "teaser": "Each Tesla equals one Weber per square meter.", "labels": {"in1": "Teslas", "in2": "Webers per square meter"}, "factor": 1.0},
{"slug": "webers-per-square-centimeter-to-gauss", "name": "Webers per square centimeter to Gauss", "category": "magnetism", "type": "standard", "teaser": "Scale the small-area flux into Gauss.", "labels": {"in1": "Webers per square centimeter", "in2": "Gauss"}, "factor": 100000000.0}, {"slug": "webers-per-square-centimeter-to-gauss", "name": "Webers per square centimeter to Gauss", "category": "magnetism", "type": "standard", "teaser": "Scale the small-area flux into Gauss.", "labels": {"in1": "Webers per square centimeter", "in2": "Gauss"}, "factor": 100000000.0},
{"slug": "gauss-to-webers-per-square-centimeter", "name": "Gauss to Webers per square centimeter", "category": "magnetism", "type": "standard", "teaser": "Convert Gauss back into Weber per square centimeter.", "labels": {"in1": "Gauss", "in2": "Webers per square centimeter"}, "factor": 1e-08, "hidden": true}, {"slug": "gauss-to-webers-per-square-centimeter", "name": "Gauss to Webers per square centimeter", "category": "magnetism", "type": "standard", "teaser": "Convert Gauss back into Weber per square centimeter.", "labels": {"in1": "Gauss", "in2": "Webers per square centimeter"}, "factor": 1e-08, "hidden": true},
{"slug": "oersted-to-gauss", "name": "Oersted to Gauss", "category": "magnetism", "type": "standard", "teaser": "In vacuum, the numeric values for Oersted and Gauss match.", "labels": {"in1": "Oersted", "in2": "Gauss"}, "hidden": true}, {"slug": "oersted-to-gauss", "name": "Oersted to Gauss", "category": "magnetism", "type": "standard", "teaser": "In vacuum, the numeric values for Oersted and Gauss match.", "labels": {"in1": "Oersted", "in2": "Gauss"}, "hidden": true, "factor": 1.0},
{"slug": "gauss-to-oersted", "name": "Gauss to Oersted", "category": "magnetism", "type": "standard", "teaser": "Translate the flux density version back into the magnetizing force scale.", "labels": {"in1": "Gauss", "in2": "Oersted"}}, {"slug": "gauss-to-oersted", "name": "Gauss to Oersted", "category": "magnetism", "type": "standard", "teaser": "Translate the flux density version back into the magnetizing force scale.", "labels": {"in1": "Gauss", "in2": "Oersted"}, "factor": 1.0},
{"slug": "kilogauss-to-microtesla", "name": "Kilogauss to Microtesla", "category": "magnetism", "type": "standard", "teaser": "A kilogauss field equals 100,000 microtesla.", "labels": {"in1": "Kilogauss", "in2": "Microtesla"}, "factor": 100000.0}, {"slug": "kilogauss-to-microtesla", "name": "Kilogauss to Microtesla", "category": "magnetism", "type": "standard", "teaser": "A kilogauss field equals 100,000 microtesla.", "labels": {"in1": "Kilogauss", "in2": "Microtesla"}, "factor": 100000.0},
{"slug": "microtesla-to-kilogauss", "name": "Microtesla to Kilogauss", "category": "magnetism", "type": "standard", "teaser": "Convert microtesla readings into kilogauss.", "labels": {"in1": "Microtesla", "in2": "Kilogauss"}, "factor": 1e-05, "hidden": true}, {"slug": "microtesla-to-kilogauss", "name": "Microtesla to Kilogauss", "category": "magnetism", "type": "standard", "teaser": "Convert microtesla readings into kilogauss.", "labels": {"in1": "Microtesla", "in2": "Kilogauss"}, "factor": 1e-05, "hidden": true},
{"slug": "kilogauss-to-nanotesla", "name": "Kilogauss to Nanotesla", "category": "magnetism", "type": "standard", "teaser": "Express kilogauss values in nanotesla for sensitive instrumentation.", "labels": {"in1": "Kilogauss", "in2": "Nanotesla"}, "factor": 100000000.0}, {"slug": "kilogauss-to-nanotesla", "name": "Kilogauss to Nanotesla", "category": "magnetism", "type": "standard", "teaser": "Express kilogauss values in nanotesla for sensitive instrumentation.", "labels": {"in1": "Kilogauss", "in2": "Nanotesla"}, "factor": 100000000.0},
@@ -606,11 +606,11 @@ export const calculators: CalculatorDef[] = [
{"slug": "horsepower-to-btumin", "name": "Horsepower to BTU/min", "category": "energy", "type": "standard", "teaser": "Get BTUs per minute from mechanical horsepower.", "labels": {"in1": "Horsepower", "in2": "BTU/min"}, "factor": 42.4072263427}, {"slug": "horsepower-to-btumin", "name": "Horsepower to BTU/min", "category": "energy", "type": "standard", "teaser": "Get BTUs per minute from mechanical horsepower.", "labels": {"in1": "Horsepower", "in2": "BTU/min"}, "factor": 42.4072263427},
{"slug": "horsepower-to-calories-per-second", "name": "Horsepower to Calories Per Second", "category": "energy", "type": "standard", "teaser": "Translate mechanical horsepower into calories per second.", "labels": {"in1": "Horsepower", "in2": "Calories Per Second"}, "factor": 178.226546845}, {"slug": "horsepower-to-calories-per-second", "name": "Horsepower to Calories Per Second", "category": "energy", "type": "standard", "teaser": "Translate mechanical horsepower into calories per second.", "labels": {"in1": "Horsepower", "in2": "Calories Per Second"}, "factor": 178.226546845},
{"slug": "horsepower-to-electrical-horsepower", "name": "Horsepower to Electrical Horsepower", "category": "power", "type": "standard", "teaser": "Convert mechanical horsepower into electrical horsepower.", "labels": {"in1": "Horsepower", "in2": "Electrical Horsepower"}, "factor": 0.999597683646, "hidden": true}, {"slug": "horsepower-to-electrical-horsepower", "name": "Horsepower to Electrical Horsepower", "category": "power", "type": "standard", "teaser": "Convert mechanical horsepower into electrical horsepower.", "labels": {"in1": "Horsepower", "in2": "Electrical Horsepower"}, "factor": 0.999597683646, "hidden": true},
{"slug": "kilowatts-to-kva", "name": "Kilowatts to kVA", "category": "power", "type": "standard", "labels": {"in1": "Kilowatts", "in2": "kVA"}}, {"slug": "kilowatts-to-kva", "name": "Kilowatts to kVA", "category": "power", "type": "standard", "labels": {"in1": "Kilowatts", "in2": "kVA"}, "factor": 1.0},
{"slug": "lusec-to-watts", "name": "Lusec to Watts", "category": "power", "type": "standard", "labels": {"in1": "Lusec", "in2": "Watts"}, "factor": 0.000133322}, {"slug": "lusec-to-watts", "name": "Lusec to Watts", "category": "power", "type": "standard", "labels": {"in1": "Lusec", "in2": "Watts"}, "factor": 0.000133322},
{"slug": "watts-to-btumin", "name": "Watts to BTU/min", "category": "energy", "type": "standard", "labels": {"in1": "Watts", "in2": "BTU/min"}, "factor": 0.0568690272522}, {"slug": "watts-to-btumin", "name": "Watts to BTU/min", "category": "energy", "type": "standard", "labels": {"in1": "Watts", "in2": "BTU/min"}, "factor": 0.0568690272522},
{"slug": "horsepower-to-lusec", "name": "Horsepower to Lusec", "category": "power", "type": "standard", "labels": {"in1": "Horsepower", "in2": "Lusec"}, "factor": 5593224.46408}, {"slug": "horsepower-to-lusec", "name": "Horsepower to Lusec", "category": "power", "type": "standard", "labels": {"in1": "Horsepower", "in2": "Lusec"}, "factor": 5593224.46408},
{"slug": "horsepower-to-mechanical-hp", "name": "Horsepower to Mechanical Hp", "category": "power", "type": "standard", "labels": {"in1": "Horsepower", "in2": "Mechanical Hp"}}, {"slug": "horsepower-to-mechanical-hp", "name": "Horsepower to Mechanical Hp", "category": "power", "type": "standard", "labels": {"in1": "Horsepower", "in2": "Mechanical Hp"}, "factor": 1.0},
{"slug": "horsepower-to-megawatts", "name": "Horsepower to Megawatts", "category": "power", "type": "standard", "labels": {"in1": "Horsepower", "in2": "Megawatts"}, "factor": 0.000745699872, "hidden": true}, {"slug": "horsepower-to-megawatts", "name": "Horsepower to Megawatts", "category": "power", "type": "standard", "labels": {"in1": "Horsepower", "in2": "Megawatts"}, "factor": 0.000745699872, "hidden": true},
{"slug": "horsepower-to-metric-horsepower-ps", "name": "Horsepower to Metric Horsepower (ps)", "category": "power", "type": "standard", "labels": {"in1": "Horsepower", "in2": "Metric Horsepower (ps)"}, "factor": 1.01386966599}, {"slug": "horsepower-to-metric-horsepower-ps", "name": "Horsepower to Metric Horsepower (ps)", "category": "power", "type": "standard", "labels": {"in1": "Horsepower", "in2": "Metric Horsepower (ps)"}, "factor": 1.01386966599},
{"slug": "kilowatts-to-boiler-horsepower", "name": "Kilowatts to Boiler Horsepower", "category": "power", "type": "standard", "labels": {"in1": "Kilowatts", "in2": "Boiler Horsepower"}, "factor": 0.101941995005, "hidden": true}, {"slug": "kilowatts-to-boiler-horsepower", "name": "Kilowatts to Boiler Horsepower", "category": "power", "type": "standard", "labels": {"in1": "Kilowatts", "in2": "Boiler Horsepower"}, "factor": 0.101941995005, "hidden": true},
@@ -622,7 +622,7 @@ export const calculators: CalculatorDef[] = [
{"slug": "lusec-to-metric-horsepower-ps", "name": "Lusec to Metric Horsepower (ps)", "category": "power", "type": "standard", "labels": {"in1": "Lusec", "in2": "Metric Horsepower (ps)"}, "factor": 1.81267473262e-07}, {"slug": "lusec-to-metric-horsepower-ps", "name": "Lusec to Metric Horsepower (ps)", "category": "power", "type": "standard", "labels": {"in1": "Lusec", "in2": "Metric Horsepower (ps)"}, "factor": 1.81267473262e-07},
{"slug": "mechanical-hp-to-boiler-horsepower", "name": "Mechanical Hp to Boiler Horsepower", "category": "power", "type": "standard", "labels": {"in1": "Mechanical Hp", "in2": "Boiler Horsepower"}, "factor": 0.0760181326265, "hidden": true}, {"slug": "mechanical-hp-to-boiler-horsepower", "name": "Mechanical Hp to Boiler Horsepower", "category": "power", "type": "standard", "labels": {"in1": "Mechanical Hp", "in2": "Boiler Horsepower"}, "factor": 0.0760181326265, "hidden": true},
{"slug": "mechanical-hp-to-electrical-horsepower", "name": "Mechanical Hp to Electrical Horsepower", "category": "power", "type": "standard", "labels": {"in1": "Mechanical Hp", "in2": "Electrical Horsepower"}, "factor": 0.999597683646, "hidden": true}, {"slug": "mechanical-hp-to-electrical-horsepower", "name": "Mechanical Hp to Electrical Horsepower", "category": "power", "type": "standard", "labels": {"in1": "Mechanical Hp", "in2": "Electrical Horsepower"}, "factor": 0.999597683646, "hidden": true},
{"slug": "mechanical-hp-to-horsepower", "name": "Mechanical Hp to Horsepower", "category": "power", "type": "standard", "labels": {"in1": "Mechanical Hp", "in2": "Horsepower"}, "hidden": true}, {"slug": "mechanical-hp-to-horsepower", "name": "Mechanical Hp to Horsepower", "category": "power", "type": "standard", "labels": {"in1": "Mechanical Hp", "in2": "Horsepower"}, "hidden": true, "factor": 1.0},
{"slug": "mechanical-hp-to-metric-horsepower-ps", "name": "Mechanical Hp to Metric Horsepower (ps)", "category": "power", "type": "standard", "labels": {"in1": "Mechanical Hp", "in2": "Metric Horsepower (ps)"}, "factor": 1.01386966599}, {"slug": "mechanical-hp-to-metric-horsepower-ps", "name": "Mechanical Hp to Metric Horsepower (ps)", "category": "power", "type": "standard", "labels": {"in1": "Mechanical Hp", "in2": "Metric Horsepower (ps)"}, "factor": 1.01386966599},
{"slug": "megawatts-to-boiler-horsepower", "name": "Megawatts to Boiler Horsepower", "category": "power", "type": "standard", "labels": {"in1": "Megawatts", "in2": "Boiler Horsepower"}, "factor": 101.941995005}, {"slug": "megawatts-to-boiler-horsepower", "name": "Megawatts to Boiler Horsepower", "category": "power", "type": "standard", "labels": {"in1": "Megawatts", "in2": "Boiler Horsepower"}, "factor": 101.941995005},
{"slug": "megawatts-to-electrical-horsepower", "name": "Megawatts to Electrical Horsepower", "category": "power", "type": "standard", "labels": {"in1": "Megawatts", "in2": "Electrical Horsepower"}, "factor": 1340.48257373}, {"slug": "megawatts-to-electrical-horsepower", "name": "Megawatts to Electrical Horsepower", "category": "power", "type": "standard", "labels": {"in1": "Megawatts", "in2": "Electrical Horsepower"}, "factor": 1340.48257373},
@@ -854,7 +854,7 @@ export const calculators: CalculatorDef[] = [
{"slug": "liters-to-gallons", "name": "Liters to Gallons", "category": "volume", "type": "standard", "labels": {"in1": "Liters", "in2": "Gallons"}, "factor": 0.264172052, "hidden": true}, {"slug": "liters-to-gallons", "name": "Liters to Gallons", "category": "volume", "type": "standard", "labels": {"in1": "Liters", "in2": "Gallons"}, "factor": 0.264172052, "hidden": true},
{"slug": "liters-to-pints", "name": "Liters to Pints", "category": "volume", "type": "standard", "labels": {"in1": "Liters", "in2": "Pints"}, "factor": 2.11337642}, {"slug": "liters-to-pints", "name": "Liters to Pints", "category": "volume", "type": "standard", "labels": {"in1": "Liters", "in2": "Pints"}, "factor": 2.11337642},
{"slug": "liters-to-quarts", "name": "Liters to Quarts", "category": "volume", "type": "standard", "labels": {"in1": "Liters", "in2": "Quarts"}, "factor": 1.05668821}, {"slug": "liters-to-quarts", "name": "Liters to Quarts", "category": "volume", "type": "standard", "labels": {"in1": "Liters", "in2": "Quarts"}, "factor": 1.05668821},
{"slug": "liters-per-100-km-to-kilometers-per-liter", "name": "Liters per 100 km to Kilometers per liter", "category": "fluids", "type": "standard", "teaser": "Turn consumption from L/100 km into km per liter.", "labels": {"in1": "Liters per 100 km", "in2": "Kilometers per liter"}}, {"slug": "liters-per-100-km-to-kilometers-per-liter", "name": "Liters per 100 km to Kilometers per liter", "category": "fluids", "type": "inverse", "teaser": "Turn consumption from L/100 km into km per liter.", "labels": {"in1": "Liters per 100 km", "in2": "Kilometers per liter"}, "factor": 100.0},
{"slug": "liters-per-100-km-to-miles-per-gallon", "name": "Liters per 100 km to Miles per gallon", "category": "fluids", "type": "inverse", "teaser": "Convert L/100 km into MPG.", "labels": {"in1": "Liters per 100 km", "in2": "Miles per gallon"}}, {"slug": "liters-per-100-km-to-miles-per-gallon", "name": "Liters per 100 km to Miles per gallon", "category": "fluids", "type": "inverse", "teaser": "Convert L/100 km into MPG.", "labels": {"in1": "Liters per 100 km", "in2": "Miles per gallon"}},
{"slug": "liters-per-minute-to-gallons-per-minute", "name": "Liters per minute to Gallons per minute", "category": "fluids", "type": "standard", "labels": {"in1": "Liters per minute", "in2": "Gallons per minute"}, "factor": 0.264172052, "hidden": true}, {"slug": "liters-per-minute-to-gallons-per-minute", "name": "Liters per minute to Gallons per minute", "category": "fluids", "type": "standard", "labels": {"in1": "Liters per minute", "in2": "Gallons per minute"}, "factor": 0.264172052, "hidden": true},
{"slug": "liters-per-second-to-cms", "name": "Liters per second to CMS", "category": "fluids", "type": "standard", "teaser": "Convert liters per second into cubic meters per second.", "labels": {"in1": "Liters per second", "in2": "CMS"}, "factor": 0.001}, {"slug": "liters-per-second-to-cms", "name": "Liters per second to CMS", "category": "fluids", "type": "standard", "teaser": "Convert liters per second into cubic meters per second.", "labels": {"in1": "Liters per second", "in2": "CMS"}, "factor": 0.001},
@@ -864,12 +864,12 @@ export const calculators: CalculatorDef[] = [
{"slug": "candela-to-lumens", "name": "Candela to Lumens", "category": "light", "type": "standard", "labels": {"in1": "Candela", "in2": "Lumens"}, "factor": 12.5663706}, {"slug": "candela-to-lumens", "name": "Candela to Lumens", "category": "light", "type": "standard", "labels": {"in1": "Candela", "in2": "Lumens"}, "factor": 12.5663706},
{"slug": "decimal-to-binary", "name": "Decimal to Binary", "category": "number-systems", "type": "base", "labels": {"in1": "Decimal", "in2": "Binary"}, "toBase": 2, "fromBase": 10, "hidden": true}, {"slug": "decimal-to-binary", "name": "Decimal to Binary", "category": "number-systems", "type": "base", "labels": {"in1": "Decimal", "in2": "Binary"}, "toBase": 2, "fromBase": 10, "hidden": true},
{"slug": "decimal-to-hex", "name": "Decimal to Hex", "category": "number-systems", "type": "base", "labels": {"in1": "Decimal", "in2": "Hex"}, "toBase": 16, "fromBase": 10}, {"slug": "decimal-to-hex", "name": "Decimal to Hex", "category": "number-systems", "type": "base", "labels": {"in1": "Decimal", "in2": "Hex"}, "toBase": 16, "fromBase": 10},
{"slug": "decimal-to-octal", "name": "Decimal to Octal", "category": "number-systems", "type": "standard", "labels": {"in1": "Decimal", "in2": "Octal"}}, {"slug": "decimal-to-octal", "name": "Decimal to Octal", "category": "number-systems", "type": "base", "labels": {"in1": "Decimal", "in2": "Octal"}, "fromBase": 10, "toBase": 8},
{"slug": "hex-to-binary", "name": "Hex to Binary", "category": "number-systems", "type": "base", "labels": {"in1": "Hex", "in2": "Binary"}, "toBase": 2, "fromBase": 16, "hidden": true}, {"slug": "hex-to-binary", "name": "Hex to Binary", "category": "number-systems", "type": "base", "labels": {"in1": "Hex", "in2": "Binary"}, "toBase": 2, "fromBase": 16, "hidden": true},
{"slug": "hex-to-decimal", "name": "Hex to Decimal", "category": "number-systems", "type": "standard", "labels": {"in1": "Hex", "in2": "Decimal"}, "hidden": true}, {"slug": "hex-to-decimal", "name": "Hex to Decimal", "category": "number-systems", "type": "base", "labels": {"in1": "Hex", "in2": "Decimal"}, "hidden": true, "fromBase": 16, "toBase": 10},
{"slug": "octal-to-binary", "name": "Octal to Binary", "category": "number-systems", "type": "standard", "teaser": "Convert base-8 digits into binary sequences.", "labels": {"in1": "Octal", "in2": "Binary"}}, {"slug": "octal-to-binary", "name": "Octal to Binary", "category": "number-systems", "type": "base", "teaser": "Convert base-8 digits into binary sequences.", "labels": {"in1": "Octal", "in2": "Binary"}, "fromBase": 8, "toBase": 2},
{"slug": "octal-to-decimal", "name": "Octal to Decimal", "category": "number-systems", "type": "standard", "teaser": "Convert octal numbers into decimal values.", "labels": {"in1": "Octal", "in2": "Decimal"}, "hidden": true}, {"slug": "octal-to-decimal", "name": "Octal to Decimal", "category": "number-systems", "type": "base", "teaser": "Convert octal numbers into decimal values.", "labels": {"in1": "Octal", "in2": "Decimal"}, "hidden": true, "fromBase": 8, "toBase": 10},
{"slug": "octal-to-hex", "name": "Octal to Hex", "category": "number-systems", "type": "standard", "teaser": "Express octal numbers as hexadecimal digits.", "labels": {"in1": "Octal", "in2": "Hex"}, "hidden": true}, {"slug": "octal-to-hex", "name": "Octal to Hex", "category": "number-systems", "type": "base", "teaser": "Express octal numbers as hexadecimal digits.", "labels": {"in1": "Octal", "in2": "Hex"}, "hidden": true, "fromBase": 8, "toBase": 16},
{"slug": "base-2-to-base-3", "name": "Base 2 to Base 3", "category": "number-systems", "type": "base", "teaser": "Translate binary digits into ternary format for alternate radix comparisons.", "labels": {"in1": "Base 2", "in2": "Base 3"}, "fromBase": 2, "toBase": 3}, {"slug": "base-2-to-base-3", "name": "Base 2 to Base 3", "category": "number-systems", "type": "base", "teaser": "Translate binary digits into ternary format for alternate radix comparisons.", "labels": {"in1": "Base 2", "in2": "Base 3"}, "fromBase": 2, "toBase": 3},
{"slug": "base-2-to-base-4", "name": "Base 2 to Base 4", "category": "number-systems", "type": "base", "teaser": "Group binary bits into quaternary digits for compact notation.", "labels": {"in1": "Base 2", "in2": "Base 4"}, "fromBase": 2, "toBase": 4}, {"slug": "base-2-to-base-4", "name": "Base 2 to Base 4", "category": "number-systems", "type": "base", "teaser": "Group binary bits into quaternary digits for compact notation.", "labels": {"in1": "Base 2", "in2": "Base 4"}, "fromBase": 2, "toBase": 4},
{"slug": "base-2-to-base-5", "name": "Base 2 to Base 5", "category": "number-systems", "type": "base", "teaser": "Reframe base-2 quantities as base-5 digits when analyzing quinary systems.", "labels": {"in1": "Base 2", "in2": "Base 5"}, "fromBase": 2, "toBase": 5}, {"slug": "base-2-to-base-5", "name": "Base 2 to Base 5", "category": "number-systems", "type": "base", "teaser": "Reframe base-2 quantities as base-5 digits when analyzing quinary systems.", "labels": {"in1": "Base 2", "in2": "Base 5"}, "fromBase": 2, "toBase": 5},
@@ -1200,7 +1200,7 @@ export const calculators: CalculatorDef[] = [
{"slug": "square-miles-to-dunams", "name": "Square Miles to Dunams", "category": "area", "type": "standard", "teaser": "Convert square miles into dunams for regional survey math.", "labels": {"in1": "Square Miles", "in2": "Dunams"}, "factor": 2589.98811034}, {"slug": "square-miles-to-dunams", "name": "Square Miles to Dunams", "category": "area", "type": "standard", "teaser": "Convert square miles into dunams for regional survey math.", "labels": {"in1": "Square Miles", "in2": "Dunams"}, "factor": 2589.98811034},
{"slug": "square-miles-to-hectares", "name": "Square Miles to Hectares", "category": "area", "type": "standard", "teaser": "Translate square-mile areas into hectares.", "labels": {"in1": "Square Miles", "in2": "Hectares"}, "factor": 258.998811034}, {"slug": "square-miles-to-hectares", "name": "Square Miles to Hectares", "category": "area", "type": "standard", "teaser": "Translate square-mile areas into hectares.", "labels": {"in1": "Square Miles", "in2": "Hectares"}, "factor": 258.998811034},
{"slug": "square-miles-to-roods", "name": "Square Miles to Roods", "category": "area", "type": "standard", "teaser": "Relate square miles to roods when referencing old English land grants.", "labels": {"in1": "Square Miles", "in2": "Roods"}, "factor": 2560.0}, {"slug": "square-miles-to-roods", "name": "Square Miles to Roods", "category": "area", "type": "standard", "teaser": "Relate square miles to roods when referencing old English land grants.", "labels": {"in1": "Square Miles", "in2": "Roods"}, "factor": 2560.0},
{"slug": "square-miles-to-sections", "name": "Square Miles to Sections", "category": "area", "type": "standard", "teaser": "Show how many survey sections fit into a square mile.", "labels": {"in1": "Square Miles", "in2": "Sections"}}, {"slug": "square-miles-to-sections", "name": "Square Miles to Sections", "category": "area", "type": "standard", "teaser": "Show how many survey sections fit into a square mile.", "labels": {"in1": "Square Miles", "in2": "Sections"}, "factor": 1.0},
{"slug": "square-miles-to-square-mils", "name": "Square Miles to Square Mils", "category": "area", "type": "standard", "teaser": "Break square miles into square mils for micro-scale analogies.", "labels": {"in1": "Square Miles", "in2": "Square Mils"}, "factor": 4014489600000000.0}, {"slug": "square-miles-to-square-mils", "name": "Square Miles to Square Mils", "category": "area", "type": "standard", "teaser": "Break square miles into square mils for micro-scale analogies.", "labels": {"in1": "Square Miles", "in2": "Square Mils"}, "factor": 4014489600000000.0},
{"slug": "square-miles-to-townships", "name": "Square Miles to Townships", "category": "area", "type": "standard", "teaser": "Express square miles as fractional townships.", "labels": {"in1": "Square Miles", "in2": "Townships"}, "factor": 0.0277777777778, "hidden": true}, {"slug": "square-miles-to-townships", "name": "Square Miles to Townships", "category": "area", "type": "standard", "teaser": "Express square miles as fractional townships.", "labels": {"in1": "Square Miles", "in2": "Townships"}, "factor": 0.0277777777778, "hidden": true},
{"slug": "square-mils-to-acres", "name": "Square Mils to Acres", "category": "area", "type": "standard", "teaser": "Convert square mils into acres for micrometer-level land analogies.", "labels": {"in1": "Square Mils", "in2": "Acres"}, "factor": 1.59422507907e-13}, {"slug": "square-mils-to-acres", "name": "Square Mils to Acres", "category": "area", "type": "standard", "teaser": "Convert square mils into acres for micrometer-level land analogies.", "labels": {"in1": "Square Mils", "in2": "Acres"}, "factor": 1.59422507907e-13},
@@ -1287,7 +1287,7 @@ export const calculators: CalculatorDef[] = [
{"slug": "torr-to-cmhg", "name": "Torr to cmHg", "category": "pressure", "type": "standard", "teaser": "Convert torr into centimeters of mercury.", "labels": {"in1": "Torr", "in2": "cmHg"}, "factor": 0.1}, {"slug": "torr-to-cmhg", "name": "Torr to cmHg", "category": "pressure", "type": "standard", "teaser": "Convert torr into centimeters of mercury.", "labels": {"in1": "Torr", "in2": "cmHg"}, "factor": 0.1},
{"slug": "torr-to-kpa", "name": "Torr to kPa", "category": "pressure", "type": "standard", "teaser": "Convert torr into kilopascals.", "labels": {"in1": "Torr", "in2": "kPa"}, "factor": 0.13332236842105263}, {"slug": "torr-to-kpa", "name": "Torr to kPa", "category": "pressure", "type": "standard", "teaser": "Convert torr into kilopascals.", "labels": {"in1": "Torr", "in2": "kPa"}, "factor": 0.13332236842105263},
{"slug": "torr-to-psi", "name": "Torr to PSI", "category": "pressure", "type": "standard", "teaser": "Convert torr into pounds per square inch.", "labels": {"in1": "Torr", "in2": "PSI"}, "factor": 0.019336776, "hidden": true}, {"slug": "torr-to-psi", "name": "Torr to PSI", "category": "pressure", "type": "standard", "teaser": "Convert torr into pounds per square inch.", "labels": {"in1": "Torr", "in2": "PSI"}, "factor": 0.019336776, "hidden": true},
{"slug": "ppi-to-dpi", "name": "PPI to DPI", "category": "other", "type": "standard", "labels": {"in1": "PPI", "in2": "DPI"}}, {"slug": "ppi-to-dpi", "name": "PPI to DPI", "category": "other", "type": "standard", "labels": {"in1": "PPI", "in2": "DPI"}, "factor": 1.0},
{"slug": "pounds-to-tons", "name": "Pounds to tons", "category": "weight", "type": "standard", "labels": {"in1": "Pounds", "in2": "tons"}, "factor": 0.0005, "hidden": true}, {"slug": "pounds-to-tons", "name": "Pounds to tons", "category": "weight", "type": "standard", "labels": {"in1": "Pounds", "in2": "tons"}, "factor": 0.0005, "hidden": true},
{"slug": "pounds-to-stones", "name": "Pounds to stones", "category": "weight", "type": "standard", "labels": {"in1": "Pounds", "in2": "stones"}, "factor": 0.071428, "hidden": true}, {"slug": "pounds-to-stones", "name": "Pounds to stones", "category": "weight", "type": "standard", "labels": {"in1": "Pounds", "in2": "stones"}, "factor": 0.071428, "hidden": true},
{"slug": "pounds-to-ounces", "name": "Pounds to ounces", "category": "weight", "type": "standard", "labels": {"in1": "Pounds", "in2": "ounces"}, "factor": 16.0}, {"slug": "pounds-to-ounces", "name": "Pounds to ounces", "category": "weight", "type": "standard", "labels": {"in1": "Pounds", "in2": "ounces"}, "factor": 16.0},
@@ -1300,7 +1300,7 @@ export const calculators: CalculatorDef[] = [
{"slug": "percent-abv-to-proof", "name": "Percent ABV to Proof", "category": "other", "type": "standard", "teaser": "Convert alcohol by volume into US proof values (Proof = 2 \u00d7 ABV).", "labels": {"in1": "Percent ABV", "in2": "Proof"}, "factor": 2.0}, {"slug": "percent-abv-to-proof", "name": "Percent ABV to Proof", "category": "other", "type": "standard", "teaser": "Convert alcohol by volume into US proof values (Proof = 2 \u00d7 ABV).", "labels": {"in1": "Percent ABV", "in2": "Proof"}, "factor": 2.0},
{"slug": "proof-to-percent-abv", "name": "Proof to Percent ABV", "category": "other", "type": "standard", "teaser": "Convert US proof back into ABV (ABV = Proof / 2).", "labels": {"in1": "Proof", "in2": "Percent ABV"}, "factor": 0.5, "hidden": true}, {"slug": "proof-to-percent-abv", "name": "Proof to Percent ABV", "category": "other", "type": "standard", "teaser": "Convert US proof back into ABV (ABV = Proof / 2).", "labels": {"in1": "Proof", "in2": "Percent ABV"}, "factor": 0.5, "hidden": true},
{"slug": "ppb-to-ppm", "name": "PPB to PPM", "category": "fluids", "type": "standard", "teaser": "Convert parts per billion into ppm.", "labels": {"in1": "PPB", "in2": "PPM"}, "factor": 0.001, "hidden": true}, {"slug": "ppb-to-ppm", "name": "PPB to PPM", "category": "fluids", "type": "standard", "teaser": "Convert parts per billion into ppm.", "labels": {"in1": "PPB", "in2": "PPM"}, "factor": 0.001, "hidden": true},
{"slug": "ppm-to-mg-per-liter", "name": "PPM to mg/L", "category": "fluids", "type": "standard", "teaser": "For dilute aqueous solutions, treat ppm as mg per liter.", "labels": {"in1": "PPM", "in2": "mg/L"}}, {"slug": "ppm-to-mg-per-liter", "name": "PPM to mg/L", "category": "fluids", "type": "standard", "teaser": "For dilute aqueous solutions, treat ppm as mg per liter.", "labels": {"in1": "PPM", "in2": "mg/L"}, "factor": 1.0},
{"slug": "ppm-to-percent", "name": "PPM to Percent", "category": "fluids", "type": "standard", "teaser": "Convert ppm values into percent by mass.", "labels": {"in1": "PPM", "in2": "Percent"}, "factor": 0.0001, "hidden": true}, {"slug": "ppm-to-percent", "name": "PPM to Percent", "category": "fluids", "type": "standard", "teaser": "Convert ppm values into percent by mass.", "labels": {"in1": "PPM", "in2": "Percent"}, "factor": 0.0001, "hidden": true},
{"slug": "ppm-to-ppb", "name": "PPM to PPB", "category": "fluids", "type": "standard", "teaser": "Convert parts per million into parts per billion.", "labels": {"in1": "PPM", "in2": "PPB"}, "factor": 1000.0}, {"slug": "ppm-to-ppb", "name": "PPM to PPB", "category": "fluids", "type": "standard", "teaser": "Convert parts per million into parts per billion.", "labels": {"in1": "PPM", "in2": "PPB"}, "factor": 1000.0},
{"slug": "planck-mass-to-kilograms", "name": "Planck mass to Kilograms", "category": "weight", "type": "standard", "teaser": "Convert the Planck mass into SI kilograms.", "labels": {"in1": "Planck mass", "in2": "Kilograms"}, "factor": 2.176434e-08}, {"slug": "planck-mass-to-kilograms", "name": "Planck mass to Kilograms", "category": "weight", "type": "standard", "teaser": "Convert the Planck mass into SI kilograms.", "labels": {"in1": "Planck mass", "in2": "Kilograms"}, "factor": 2.176434e-08},
@@ -1485,7 +1485,7 @@ export const calculators: CalculatorDef[] = [
{"slug": "imperial-fl-oz-to-cubic-yards", "name": "Imperial fl oz to Cubic Yards", "category": "volume", "type": "standard", "teaser": "Express Imperial ounces in cubic yards for bulk yardage.", "labels": {"in1": "Imperial fl oz", "in2": "Cubic Yards"}, "factor": 3.7162882693493535e-05, "hidden": true}, {"slug": "imperial-fl-oz-to-cubic-yards", "name": "Imperial fl oz to Cubic Yards", "category": "volume", "type": "standard", "teaser": "Express Imperial ounces in cubic yards for bulk yardage.", "labels": {"in1": "Imperial fl oz", "in2": "Cubic Yards"}, "factor": 3.7162882693493535e-05, "hidden": true},
{"slug": "imperial-fl-oz-to-cups", "name": "Imperial fl oz to Cups", "category": "volume", "type": "standard", "teaser": "Show Imperial ounces in cups for recipe crossovers.", "labels": {"in1": "Imperial fl oz", "in2": "Cups"}, "factor": 0.12009499255048549, "hidden": true}, {"slug": "imperial-fl-oz-to-cups", "name": "Imperial fl oz to Cups", "category": "volume", "type": "standard", "teaser": "Show Imperial ounces in cups for recipe crossovers.", "labels": {"in1": "Imperial fl oz", "in2": "Cups"}, "factor": 0.12009499255048549, "hidden": true},
{"slug": "imperial-fl-oz-to-drams-fluid", "name": "Imperial fl oz to Drams (fluid)", "category": "volume", "type": "standard", "teaser": "Convert Imperial ounces to fluid drams for precise dosing.", "labels": {"in1": "Imperial fl oz", "in2": "Drams (fluid)"}, "factor": 8.0}, {"slug": "imperial-fl-oz-to-drams-fluid", "name": "Imperial fl oz to Drams (fluid)", "category": "volume", "type": "standard", "teaser": "Convert Imperial ounces to fluid drams for precise dosing.", "labels": {"in1": "Imperial fl oz", "in2": "Drams (fluid)"}, "factor": 8.0},
{"slug": "imperial-fl-oz-to-fluid-ounces", "name": "Imperial fl oz to Fluid Ounces", "category": "volume", "type": "standard", "teaser": "Keep Imperial ounces expressed in themselves for clarity.", "labels": {"in1": "Imperial fl oz", "in2": "Fluid Ounces"}, "hidden": true}, {"slug": "imperial-fl-oz-to-fluid-ounces", "name": "Imperial fl oz to Fluid Ounces", "category": "volume", "type": "standard", "teaser": "Keep Imperial ounces expressed in themselves for clarity.", "labels": {"in1": "Imperial fl oz", "in2": "Fluid Ounces"}, "hidden": true, "factor": 1.0},
{"slug": "imperial-fl-oz-to-gallons", "name": "Imperial fl oz to Gallons", "category": "volume", "type": "standard", "teaser": "Convert Imperial fluid ounces into Imperial gallons for British volume checks.", "labels": {"in1": "Imperial fl oz", "in2": "Gallons"}, "factor": 0.00625}, {"slug": "imperial-fl-oz-to-gallons", "name": "Imperial fl oz to Gallons", "category": "volume", "type": "standard", "teaser": "Convert Imperial fluid ounces into Imperial gallons for British volume checks.", "labels": {"in1": "Imperial fl oz", "in2": "Gallons"}, "factor": 0.00625},
{"slug": "imperial-fl-oz-to-gill", "name": "Imperial fl oz to Gill", "category": "volume", "type": "standard", "teaser": "Relate Imperial ounces to gills when crafting classic cocktails.", "labels": {"in1": "Imperial fl oz", "in2": "Gill"}, "factor": 0.2, "hidden": true}, {"slug": "imperial-fl-oz-to-gill", "name": "Imperial fl oz to Gill", "category": "volume", "type": "standard", "teaser": "Relate Imperial ounces to gills when crafting classic cocktails.", "labels": {"in1": "Imperial fl oz", "in2": "Gill"}, "factor": 0.2, "hidden": true},
{"slug": "imperial-fl-oz-to-hogshead", "name": "Imperial fl oz to Hogshead", "category": "volume", "type": "standard", "teaser": "Express Imperial ounces as a fraction of a hogshead for barrel planning.", "labels": {"in1": "Imperial fl oz", "in2": "Hogshead"}, "factor": 0.00011574074074074075, "hidden": true}, {"slug": "imperial-fl-oz-to-hogshead", "name": "Imperial fl oz to Hogshead", "category": "volume", "type": "standard", "teaser": "Express Imperial ounces as a fraction of a hogshead for barrel planning.", "labels": {"in1": "Imperial fl oz", "in2": "Hogshead"}, "factor": 0.00011574074074074075, "hidden": true},
@@ -1638,7 +1638,7 @@ export const calculators: CalculatorDef[] = [
{"slug": "gallons-to-fluid-ounces", "name": "Gallons to Fluid Ounces", "category": "weight", "type": "standard", "teaser": "A pitcher pours three gallons; how many fluid ounces is that?", "labels": {"in1": "Gallons", "in2": "Fluid Ounces"}, "factor": 128.0}, {"slug": "gallons-to-fluid-ounces", "name": "Gallons to Fluid Ounces", "category": "weight", "type": "standard", "teaser": "A pitcher pours three gallons; how many fluid ounces is that?", "labels": {"in1": "Gallons", "in2": "Fluid Ounces"}, "factor": 128.0},
{"slug": "gallons-to-pints", "name": "Gallons to Pints", "category": "volume", "type": "standard", "teaser": "Pouring one gallon equals how many pints for serving?", "labels": {"in1": "Gallons", "in2": "Pints"}, "factor": 8.0}, {"slug": "gallons-to-pints", "name": "Gallons to Pints", "category": "volume", "type": "standard", "teaser": "Pouring one gallon equals how many pints for serving?", "labels": {"in1": "Gallons", "in2": "Pints"}, "factor": 8.0},
{"slug": "gallons-to-quarts", "name": "Gallons to Quarts", "category": "volume", "type": "standard", "teaser": "Four gallons convert to how many quarts for canning?", "labels": {"in1": "Gallons", "in2": "Quarts"}, "factor": 4.0}, {"slug": "gallons-to-quarts", "name": "Gallons to Quarts", "category": "volume", "type": "standard", "teaser": "Four gallons convert to how many quarts for canning?", "labels": {"in1": "Gallons", "in2": "Quarts"}, "factor": 4.0},
{"slug": "gamma-mass-to-micrograms", "name": "Gamma (mass) to Micrograms", "category": "weight", "type": "standard", "teaser": "A lab note shows gamma units; what is that in micrograms?", "labels": {"in1": "Gamma (mass)", "in2": "Micrograms"}}, {"slug": "gamma-mass-to-micrograms", "name": "Gamma (mass) to Micrograms", "category": "weight", "type": "standard", "teaser": "A lab note shows gamma units; what is that in micrograms?", "labels": {"in1": "Gamma (mass)", "in2": "Micrograms"}, "factor": 1.0},
{"slug": "gauss-to-ampere-turns-per-meter", "name": "Gauss / Ampere-turns per meter", "category": "magnetism", "type": "3col", "teaser": "Convert magnetic flux density into magnetomotive force per meter when the medium's permeability is known.", "labels": {"in1": "Gauss", "in2": "Ampere-turns per meter", "in3": "Gauss / Ampere-turns per meter"}}, {"slug": "gauss-to-ampere-turns-per-meter", "name": "Gauss / Ampere-turns per meter", "category": "magnetism", "type": "3col", "teaser": "Convert magnetic flux density into magnetomotive force per meter when the medium's permeability is known.", "labels": {"in1": "Gauss", "in2": "Ampere-turns per meter", "in3": "Gauss / Ampere-turns per meter"}},
{"slug": "gauss-to-tesla", "name": "Gauss to Tesla", "category": "magnetism", "type": "standard", "teaser": "A field reads five thousand gauss; what is that in tesla?", "labels": {"in1": "Gauss", "in2": "Tesla"}, "factor": 0.0001, "hidden": true}, {"slug": "gauss-to-tesla", "name": "Gauss to Tesla", "category": "magnetism", "type": "standard", "teaser": "A field reads five thousand gauss; what is that in tesla?", "labels": {"in1": "Gauss", "in2": "Tesla"}, "factor": 0.0001, "hidden": true},
{"slug": "millitesla-to-tesla", "name": "Millitesla to Tesla", "category": "magnetism", "type": "standard", "teaser": "Convert milliteslas into teslas for electromagnetism work.", "labels": {"in1": "Millitesla", "in2": "Tesla"}, "factor": 0.001, "hidden": true}, {"slug": "millitesla-to-tesla", "name": "Millitesla to Tesla", "category": "magnetism", "type": "standard", "teaser": "Convert milliteslas into teslas for electromagnetism work.", "labels": {"in1": "Millitesla", "in2": "Tesla"}, "factor": 0.001, "hidden": true},
@@ -1729,10 +1729,10 @@ export const calculators: CalculatorDef[] = [
{"slug": "grains-to-troy-ounces", "name": "Grains to Troy Ounces", "category": "weight", "type": "standard", "teaser": "Blend grain counts into troy ounces for precious metals.", "labels": {"in1": "Grains", "in2": "Troy Ounces"}, "factor": 0.0020833333333333333}, {"slug": "grains-to-troy-ounces", "name": "Grains to Troy Ounces", "category": "weight", "type": "standard", "teaser": "Blend grain counts into troy ounces for precious metals.", "labels": {"in1": "Grains", "in2": "Troy Ounces"}, "factor": 0.0020833333333333333},
{"slug": "grains-to-yoctograms", "name": "Grains to Yoctograms", "category": "weight", "type": "standard", "teaser": "Push a grain\u2019s mass into yoctograms for extreme microscale reports.", "labels": {"in1": "Grains", "in2": "Yoctograms"}, "factor": 6.479891e+22}, {"slug": "grains-to-yoctograms", "name": "Grains to Yoctograms", "category": "weight", "type": "standard", "teaser": "Push a grain\u2019s mass into yoctograms for extreme microscale reports.", "labels": {"in1": "Grains", "in2": "Yoctograms"}, "factor": 6.479891e+22},
{"slug": "grains-to-zeptograms", "name": "Grains to Zeptograms", "category": "weight", "type": "standard", "teaser": "Continue down to zeptogram counts to highlight the minute mass.", "labels": {"in1": "Grains", "in2": "Zeptograms"}, "factor": 6.479891e+19}, {"slug": "grains-to-zeptograms", "name": "Grains to Zeptograms", "category": "weight", "type": "standard", "teaser": "Continue down to zeptogram counts to highlight the minute mass.", "labels": {"in1": "Grains", "in2": "Zeptograms"}, "factor": 6.479891e+19},
{"slug": "grams-per-cubic-centimeter-to-grams-per-milliliter", "name": "Grams per cubic centimeter to Grams per Milliliter", "category": "fluids", "type": "standard", "teaser": "Recognize that 1 g/cm\u00b3 equals 1 g/mL for density clarity.", "labels": {"in1": "Grams per cubic centimeter", "in2": "Grams per Milliliter"}}, {"slug": "grams-per-cubic-centimeter-to-grams-per-milliliter", "name": "Grams per cubic centimeter to Grams per Milliliter", "category": "fluids", "type": "standard", "teaser": "Recognize that 1 g/cm\u00b3 equals 1 g/mL for density clarity.", "labels": {"in1": "Grams per cubic centimeter", "in2": "Grams per Milliliter"}, "factor": 1.0},
{"slug": "grams-per-cubic-centimeter-to-kilograms-per-cubic-meter", "name": "Grams per cubic centimeter to Kilograms per Cubic Meter", "category": "fluids", "type": "standard", "teaser": "Convert grams per cubic centimeter into kg/m\u00b3 for engineering spec sheets.", "labels": {"in1": "Grams per cubic centimeter", "in2": "Kilograms per Cubic Meter"}, "factor": 1000.0}, {"slug": "grams-per-cubic-centimeter-to-kilograms-per-cubic-meter", "name": "Grams per cubic centimeter to Kilograms per Cubic Meter", "category": "fluids", "type": "standard", "teaser": "Convert grams per cubic centimeter into kg/m\u00b3 for engineering spec sheets.", "labels": {"in1": "Grams per cubic centimeter", "in2": "Kilograms per Cubic Meter"}, "factor": 1000.0},
{"slug": "grams-per-cubic-centimeter-to-pounds-per-cubic-foot", "name": "Grams per cubic centimeter to Pounds per Cubic Foot", "category": "fluids", "type": "standard", "teaser": "Turn metric density into imperial pounds per cubic foot for building specs.", "labels": {"in1": "Grams per cubic centimeter", "in2": "Pounds per Cubic Foot"}, "factor": 62.4279605743}, {"slug": "grams-per-cubic-centimeter-to-pounds-per-cubic-foot", "name": "Grams per cubic centimeter to Pounds per Cubic Foot", "category": "fluids", "type": "standard", "teaser": "Turn metric density into imperial pounds per cubic foot for building specs.", "labels": {"in1": "Grams per cubic centimeter", "in2": "Pounds per Cubic Foot"}, "factor": 62.4279605743},
{"slug": "grams-per-cubic-centimeter-to-kilograms-per-liter", "name": "Grams per cubic centimeter to Kilograms per liter", "category": "fluids", "type": "standard", "teaser": "A fluid density of 1 g/cm\u00b3 equals how many kg/L?", "labels": {"in1": "Grams per cubic centimeter", "in2": "Kilograms per liter"}}, {"slug": "grams-per-cubic-centimeter-to-kilograms-per-liter", "name": "Grams per cubic centimeter to Kilograms per liter", "category": "fluids", "type": "standard", "teaser": "A fluid density of 1 g/cm\u00b3 equals how many kg/L?", "labels": {"in1": "Grams per cubic centimeter", "in2": "Kilograms per liter"}, "factor": 1.0},
{"slug": "grams-per-liter-to-milligrams-per-liter", "name": "Grams per liter to Milligrams per Liter", "category": "fluids", "type": "standard", "teaser": "Convert grams per liter into milligrams per liter for water-quality reporting.", "labels": {"in1": "Grams per liter", "in2": "Milligrams per Liter"}, "factor": 1000.0}, {"slug": "grams-per-liter-to-milligrams-per-liter", "name": "Grams per liter to Milligrams per Liter", "category": "fluids", "type": "standard", "teaser": "Convert grams per liter into milligrams per liter for water-quality reporting.", "labels": {"in1": "Grams per liter", "in2": "Milligrams per Liter"}, "factor": 1000.0},
{"slug": "grams-per-liter-to-molarity", "name": "Grams per liter to Molarity", "category": "fluids", "type": "standard", "teaser": "Turn a grams-per-liter reading into molarity when a molar mass is supplied.", "labels": {"in1": "Grams per liter", "in2": "Molarity"}}, {"slug": "grams-per-liter-to-molarity", "name": "Grams per liter to Molarity", "category": "fluids", "type": "standard", "teaser": "Turn a grams-per-liter reading into molarity when a molar mass is supplied.", "labels": {"in1": "Grams per liter", "in2": "Molarity"}},
{"slug": "grams-per-liter-to-percent", "name": "Grams per liter / Percent", "category": "fluids", "type": "3col", "teaser": "Map grams per liter into mass percent when the solution density is known.", "labels": {"in1": "Grams per liter", "in2": "Percent", "in3": "Grams per liter / Percent"}}, {"slug": "grams-per-liter-to-percent", "name": "Grams per liter / Percent", "category": "fluids", "type": "3col", "teaser": "Map grams per liter into mass percent when the solution density is known.", "labels": {"in1": "Grams per liter", "in2": "Percent", "in3": "Grams per liter / Percent"}},
@@ -1741,8 +1741,8 @@ export const calculators: CalculatorDef[] = [
{"slug": "grams-per-liter-to-ppm", "name": "Grams per liter to Ppm", "category": "fluids", "type": "standard", "teaser": "Express grams per liter as parts per million for environmental windows.", "labels": {"in1": "Grams per liter", "in2": "Ppm"}, "factor": 1000.0}, {"slug": "grams-per-liter-to-ppm", "name": "Grams per liter to Ppm", "category": "fluids", "type": "standard", "teaser": "Express grams per liter as parts per million for environmental windows.", "labels": {"in1": "Grams per liter", "in2": "Ppm"}, "factor": 1000.0},
{"slug": "grams-per-liter-to-proof", "name": "Grams per liter / Proof", "category": "fluids", "type": "3col", "teaser": "Translate grams per liter into proof when alcohol density and ABV are known.", "labels": {"in1": "Grams per liter", "in2": "Proof", "in3": "Grams per liter / Proof"}}, {"slug": "grams-per-liter-to-proof", "name": "Grams per liter / Proof", "category": "fluids", "type": "3col", "teaser": "Translate grams per liter into proof when alcohol density and ABV are known.", "labels": {"in1": "Grams per liter", "in2": "Proof", "in3": "Grams per liter / Proof"}},
{"slug": "grams-per-milliliter-to-kilograms-per-cubic-meter", "name": "Grams per milliliter to Kilograms per cubic meter", "category": "fluids", "type": "standard", "teaser": "A solution at 1.2 g/mL corresponds to how many kg/m\u00b3?", "labels": {"in1": "Grams per milliliter", "in2": "Kilograms per cubic meter"}, "factor": 1000.0}, {"slug": "grams-per-milliliter-to-kilograms-per-cubic-meter", "name": "Grams per milliliter to Kilograms per cubic meter", "category": "fluids", "type": "standard", "teaser": "A solution at 1.2 g/mL corresponds to how many kg/m\u00b3?", "labels": {"in1": "Grams per milliliter", "in2": "Kilograms per cubic meter"}, "factor": 1000.0},
{"slug": "grams-per-milliliter-to-grams-per-cubic-centimeter", "name": "Grams per milliliter to Grams per Cubic Centimeter", "category": "fluids", "type": "standard", "teaser": "Since a milliliter equals a cubic centimeter, this is a 1:1 conversion.", "labels": {"in1": "Grams per milliliter", "in2": "Grams per Cubic Centimeter"}, "hidden": true}, {"slug": "grams-per-milliliter-to-grams-per-cubic-centimeter", "name": "Grams per milliliter to Grams per Cubic Centimeter", "category": "fluids", "type": "standard", "teaser": "Since a milliliter equals a cubic centimeter, this is a 1:1 conversion.", "labels": {"in1": "Grams per milliliter", "in2": "Grams per Cubic Centimeter"}, "hidden": true, "factor": 1.0},
{"slug": "grams-per-milliliter-to-kilograms-per-liter", "name": "Grams per milliliter to Kilograms per Liter", "category": "fluids", "type": "standard", "teaser": "Translate grams per milliliter into kilograms per liter for lab data sheets.", "labels": {"in1": "Grams per milliliter", "in2": "Kilograms per Liter"}}, {"slug": "grams-per-milliliter-to-kilograms-per-liter", "name": "Grams per milliliter to Kilograms per Liter", "category": "fluids", "type": "standard", "teaser": "Translate grams per milliliter into kilograms per liter for lab data sheets.", "labels": {"in1": "Grams per milliliter", "in2": "Kilograms per Liter"}, "factor": 1.0},
{"slug": "grams-per-milliliter-to-pounds-per-cubic-foot", "name": "Grams per milliliter to Pounds per Cubic Foot", "category": "fluids", "type": "standard", "teaser": "Scale grams per milliliter into pounds per cubic foot for imperial references.", "labels": {"in1": "Grams per milliliter", "in2": "Pounds per Cubic Foot"}, "factor": 62.4279605743}, {"slug": "grams-per-milliliter-to-pounds-per-cubic-foot", "name": "Grams per milliliter to Pounds per Cubic Foot", "category": "fluids", "type": "standard", "teaser": "Scale grams per milliliter into pounds per cubic foot for imperial references.", "labels": {"in1": "Grams per milliliter", "in2": "Pounds per Cubic Foot"}, "factor": 62.4279605743},
{"slug": "grams-to-amu", "name": "Grams to Amu", "category": "weight", "type": "standard", "teaser": "Convert a gram into atomic mass units for precise reaction counts.", "labels": {"in1": "Grams", "in2": "Amu"}, "factor": 6.022140762081123e+23}, {"slug": "grams-to-amu", "name": "Grams to Amu", "category": "weight", "type": "standard", "teaser": "Convert a gram into atomic mass units for precise reaction counts.", "labels": {"in1": "Grams", "in2": "Amu"}, "factor": 6.022140762081123e+23},
{"slug": "grams-to-atomic-mass-units", "name": "Grams to Atomic Mass Units", "category": "weight", "type": "standard", "teaser": "Translate grams into atomic mass units when bridging scales.", "labels": {"in1": "Grams", "in2": "Atomic Mass Units"}, "factor": 6.022140762081123e+23}, {"slug": "grams-to-atomic-mass-units", "name": "Grams to Atomic Mass Units", "category": "weight", "type": "standard", "teaser": "Translate grams into atomic mass units when bridging scales.", "labels": {"in1": "Grams", "in2": "Atomic Mass Units"}, "factor": 6.022140762081123e+23},
@@ -1787,9 +1787,9 @@ export const calculators: CalculatorDef[] = [
{"slug": "grams-to-pounds", "name": "Grams to Pounds", "category": "weight", "type": "standard", "teaser": "A container measures 200 grams; how many pounds does the load represent?", "labels": {"in1": "Grams", "in2": "Pounds"}, "factor": 0.00220462}, {"slug": "grams-to-pounds", "name": "Grams to Pounds", "category": "weight", "type": "standard", "teaser": "A container measures 200 grams; how many pounds does the load represent?", "labels": {"in1": "Grams", "in2": "Pounds"}, "factor": 0.00220462},
{"slug": "grams-to-scruples", "name": "Grams to Scruples", "category": "weight", "type": "standard", "teaser": "A pharmacy formula needs 10 grams; what is that in scruples?", "labels": {"in1": "Grams", "in2": "Scruples"}, "factor": 0.771605, "hidden": true}, {"slug": "grams-to-scruples", "name": "Grams to Scruples", "category": "weight", "type": "standard", "teaser": "A pharmacy formula needs 10 grams; what is that in scruples?", "labels": {"in1": "Grams", "in2": "Scruples"}, "factor": 0.771605, "hidden": true},
{"slug": "grams-to-tolas", "name": "Grams to Tolas", "category": "weight", "type": "standard", "teaser": "A gold bar weighs 5 grams; how many tolas is that mass?", "labels": {"in1": "Grams", "in2": "Tolas"}, "factor": 0.085735}, {"slug": "grams-to-tolas", "name": "Grams to Tolas", "category": "weight", "type": "standard", "teaser": "A gold bar weighs 5 grams; how many tolas is that mass?", "labels": {"in1": "Grams", "in2": "Tolas"}, "factor": 0.085735},
{"slug": "gray-to-sievert", "name": "Gray to Sievert", "category": "radiation", "type": "standard", "teaser": "A gamma exposure is 3 gray; how many sieverts is that for gamma/beta?", "labels": {"in1": "Gray", "in2": "Sievert"}}, {"slug": "gray-to-sievert", "name": "Gray to Sievert", "category": "radiation", "type": "standard", "teaser": "A gamma exposure is 3 gray; how many sieverts is that for gamma/beta?", "labels": {"in1": "Gray", "in2": "Sievert"}, "factor": 1.0},
{"slug": "fahrenheit-to-newton", "name": "Fahrenheit to Newton", "category": "force", "type": "standard", "teaser": "Convert Fahrenheit to Newtons on the scale where 0\u202f\u00b0C equals 0\u202f\u00b0N and 100\u202f\u00b0C equals 33\u202f\u00b0N.", "labels": {"in1": "Fahrenheit", "in2": "Newton"}, "factor": 0.183333, "offset": -5.867}, {"slug": "fahrenheit-to-newton", "name": "Fahrenheit to Newton", "category": "force", "type": "standard", "teaser": "Convert Fahrenheit to Newtons on the scale where 0\u202f\u00b0C equals 0\u202f\u00b0N and 100\u202f\u00b0C equals 33\u202f\u00b0N.", "labels": {"in1": "Fahrenheit", "in2": "Newton"}, "factor": 0.183333, "offset": -5.867},
{"slug": "fahrenheit-to-rankine", "name": "Fahrenheit to Rankine", "category": "temperature", "type": "standard", "teaser": "Convert Fahrenheit to the absolute Rankine scale.", "labels": {"in1": "Fahrenheit", "in2": "Rankine"}, "offset": 459.67}, {"slug": "fahrenheit-to-rankine", "name": "Fahrenheit to Rankine", "category": "temperature", "type": "standard", "teaser": "Convert Fahrenheit to the absolute Rankine scale.", "labels": {"in1": "Fahrenheit", "in2": "Rankine"}, "offset": 459.67, "factor": 1.0},
{"slug": "celsius-to-newton", "name": "Celsius to Newton (temp Scale)", "category": "temperature", "type": "standard", "teaser": "Convert Celsius into Newton's temperature scale (0\u202f\u00b0C \u2248 0\u202f\u00b0N, 100\u202f\u00b0C \u2248 33\u202f\u00b0N).", "labels": {"in1": "Celsius", "in2": "Newton (temp Scale)"}, "factor": 0.33, "offset": 0.0}, {"slug": "celsius-to-newton", "name": "Celsius to Newton (temp Scale)", "category": "temperature", "type": "standard", "teaser": "Convert Celsius into Newton's temperature scale (0\u202f\u00b0C \u2248 0\u202f\u00b0N, 100\u202f\u00b0C \u2248 33\u202f\u00b0N).", "labels": {"in1": "Celsius", "in2": "Newton (temp Scale)"}, "factor": 0.33, "offset": 0.0},
{"slug": "delisle-to-fahrenheit", "name": "Delisle to Fahrenheit", "category": "temperature", "type": "standard", "teaser": "Turn antique Delisle readings into modern Fahrenheit.", "labels": {"in1": "Delisle", "in2": "Fahrenheit"}, "factor": -1.2, "offset": 212.0, "hidden": true}, {"slug": "delisle-to-fahrenheit", "name": "Delisle to Fahrenheit", "category": "temperature", "type": "standard", "teaser": "Turn antique Delisle readings into modern Fahrenheit.", "labels": {"in1": "Delisle", "in2": "Fahrenheit"}, "factor": -1.2, "offset": 212.0, "hidden": true},
{"slug": "delisle-to-kelvin", "name": "Delisle to Kelvin", "category": "temperature", "type": "standard", "teaser": "Translate Delisle degrees into the Kelvin scale for absolute comparisons.", "labels": {"in1": "Delisle", "in2": "Kelvin"}, "factor": -0.666666666667, "offset": 373.15, "hidden": true}, {"slug": "delisle-to-kelvin", "name": "Delisle to Kelvin", "category": "temperature", "type": "standard", "teaser": "Translate Delisle degrees into the Kelvin scale for absolute comparisons.", "labels": {"in1": "Delisle", "in2": "Kelvin"}, "factor": -0.666666666667, "offset": 373.15, "hidden": true},
@@ -1907,7 +1907,7 @@ export const calculators: CalculatorDef[] = [
{"slug": "kpa-to-bar", "name": "kPa to Bar", "category": "pressure", "type": "standard", "teaser": "Convert kilopascals to bar.", "labels": {"in1": "kPa", "in2": "Bar"}, "factor": 0.01}, {"slug": "kpa-to-bar", "name": "kPa to Bar", "category": "pressure", "type": "standard", "teaser": "Convert kilopascals to bar.", "labels": {"in1": "kPa", "in2": "Bar"}, "factor": 0.01},
{"slug": "kpa-to-pascal", "name": "kPa to Pascal", "category": "pressure", "type": "standard", "teaser": "Convert kilopascals to pascals.", "labels": {"in1": "kPa", "in2": "Pascal"}, "factor": 1000.0}, {"slug": "kpa-to-pascal", "name": "kPa to Pascal", "category": "pressure", "type": "standard", "teaser": "Convert kilopascals to pascals.", "labels": {"in1": "kPa", "in2": "Pascal"}, "factor": 1000.0},
{"slug": "kpa-to-psi", "name": "kPa to PSI", "category": "pressure", "type": "standard", "teaser": "Convert kilopascals to pounds per square inch.", "labels": {"in1": "kPa", "in2": "PSI"}, "factor": 0.1450377377, "hidden": true}, {"slug": "kpa-to-psi", "name": "kPa to PSI", "category": "pressure", "type": "standard", "teaser": "Convert kilopascals to pounds per square inch.", "labels": {"in1": "kPa", "in2": "PSI"}, "factor": 0.1450377377, "hidden": true},
{"slug": "kva-to-kilowatts", "name": "kVA to Kilowatts", "category": "power", "type": "standard", "teaser": "Convert apparent power to real power assuming unity power factor.", "labels": {"in1": "kVA", "in2": "Kilowatts"}, "hidden": true}, {"slug": "kva-to-kilowatts", "name": "kVA to Kilowatts", "category": "power", "type": "standard", "teaser": "Convert apparent power to real power assuming unity power factor.", "labels": {"in1": "kVA", "in2": "Kilowatts"}, "hidden": true, "factor": 1.0},
{"slug": "leagues-per-hour-to-kmh", "name": "Leagues per Hour to Kilometers per Hour", "category": "length", "type": "standard", "teaser": "Convert leagues per hour into kilometers per hour.", "labels": {"in1": "Leagues per Hour", "in2": "Kilometers per Hour"}, "factor": 4.82803}, {"slug": "leagues-per-hour-to-kmh", "name": "Leagues per Hour to Kilometers per Hour", "category": "length", "type": "standard", "teaser": "Convert leagues per hour into kilometers per hour.", "labels": {"in1": "Leagues per Hour", "in2": "Kilometers per Hour"}, "factor": 4.82803},
{"slug": "leagues-to-kilometers", "name": "Leagues to Kilometers", "category": "length", "type": "standard", "teaser": "Convert leagues to kilometers.", "labels": {"in1": "Leagues", "in2": "Kilometers"}, "factor": 4.82803}, {"slug": "leagues-to-kilometers", "name": "Leagues to Kilometers", "category": "length", "type": "standard", "teaser": "Convert leagues to kilometers.", "labels": {"in1": "Leagues", "in2": "Kilometers"}, "factor": 4.82803},
{"slug": "leagues-to-miles", "name": "Leagues to Miles", "category": "length", "type": "standard", "teaser": "Convert leagues to miles.", "labels": {"in1": "Leagues", "in2": "Miles"}, "factor": 3.0}, {"slug": "leagues-to-miles", "name": "Leagues to Miles", "category": "length", "type": "standard", "teaser": "Convert leagues to miles.", "labels": {"in1": "Leagues", "in2": "Miles"}, "factor": 3.0},
@@ -1919,12 +1919,12 @@ export const calculators: CalculatorDef[] = [
{"slug": "long-tons-to-pounds", "name": "Long Tons to Pounds", "category": "weight", "type": "standard", "teaser": "Convert long tons to pounds.", "labels": {"in1": "Long Tons", "in2": "Pounds"}, "factor": 2240.0}, {"slug": "long-tons-to-pounds", "name": "Long Tons to Pounds", "category": "weight", "type": "standard", "teaser": "Convert long tons to pounds.", "labels": {"in1": "Long Tons", "in2": "Pounds"}, "factor": 2240.0},
{"slug": "long-tons-to-short-tons", "name": "Long Tons to Short Tons", "category": "weight", "type": "standard", "teaser": "Convert long tons to short tons.", "labels": {"in1": "Long Tons", "in2": "Short Tons"}, "factor": 1.12}, {"slug": "long-tons-to-short-tons", "name": "Long Tons to Short Tons", "category": "weight", "type": "standard", "teaser": "Convert long tons to short tons.", "labels": {"in1": "Long Tons", "in2": "Short Tons"}, "factor": 1.12},
{"slug": "lux-to-foot-candles", "name": "Lux to Foot-candles", "category": "light", "type": "standard", "teaser": "Convert illuminance from lux to foot-candles.", "labels": {"in1": "Lux", "in2": "Foot-candles"}, "factor": 0.09290304, "hidden": true}, {"slug": "lux-to-foot-candles", "name": "Lux to Foot-candles", "category": "light", "type": "standard", "teaser": "Convert illuminance from lux to foot-candles.", "labels": {"in1": "Lux", "in2": "Foot-candles"}, "factor": 0.09290304, "hidden": true},
{"slug": "candela-to-foot-candles", "name": "Candela to Foot-candles", "category": "light", "type": "standard", "teaser": "At 1 foot away, one candela yields one foot-candle of illuminance.", "labels": {"in1": "Candela", "in2": "Foot-candles"}}, {"slug": "candela-to-foot-candles", "name": "Candela to Foot-candles", "category": "light", "type": "standard", "teaser": "At 1 foot away, one candela yields one foot-candle of illuminance.", "labels": {"in1": "Candela", "in2": "Foot-candles"}, "factor": 1.0},
{"slug": "foot-candles-to-candela", "name": "Foot-candles to Candela", "category": "light", "type": "standard", "teaser": "Convert illuminance in foot-candles into a luminous intensity at 1 foot.", "labels": {"in1": "Foot-candles", "in2": "Candela"}, "hidden": true}, {"slug": "foot-candles-to-candela", "name": "Foot-candles to Candela", "category": "light", "type": "standard", "teaser": "Convert illuminance in foot-candles into a luminous intensity at 1 foot.", "labels": {"in1": "Foot-candles", "in2": "Candela"}, "hidden": true, "factor": 1.0},
{"slug": "candela-to-lux-at-foot", "name": "Candela to Lux (1 ft)", "category": "light", "type": "standard", "teaser": "One candela at a 1-foot distance produces about 10.7639 lux on the target plane.", "labels": {"in1": "Candela", "in2": "Lux (1 ft)"}, "factor": 10.7639}, {"slug": "candela-to-lux-at-foot", "name": "Candela to Lux (1 ft)", "category": "light", "type": "standard", "teaser": "One candela at a 1-foot distance produces about 10.7639 lux on the target plane.", "labels": {"in1": "Candela", "in2": "Lux (1 ft)"}, "factor": 10.7639},
{"slug": "lux-to-candela-at-foot", "name": "Lux to Candela (1 ft)", "category": "light", "type": "standard", "teaser": "Turn lux measured 1 foot away into the equivalent candela luminous intensity.", "labels": {"in1": "Lux", "in2": "Candela (1 ft)"}, "factor": 0.09290304}, {"slug": "lux-to-candela-at-foot", "name": "Lux to Candela (1 ft)", "category": "light", "type": "standard", "teaser": "Turn lux measured 1 foot away into the equivalent candela luminous intensity.", "labels": {"in1": "Lux", "in2": "Candela (1 ft)"}, "factor": 0.09290304},
{"slug": "candela-to-nits", "name": "Candela to Nits", "category": "light", "type": "standard", "teaser": "Treat 1 candela distributed over 1 square meter as 1 nit for comparing luminance.", "labels": {"in1": "Candela", "in2": "Nits"}}, {"slug": "candela-to-nits", "name": "Candela to Nits", "category": "light", "type": "standard", "teaser": "Treat 1 candela distributed over 1 square meter as 1 nit for comparing luminance.", "labels": {"in1": "Candela", "in2": "Nits"}, "factor": 1.0},
{"slug": "nits-to-candela", "name": "Nits to Candela", "category": "light", "type": "standard", "teaser": "Assume a 1-square-meter patch to convert nits into candela.", "labels": {"in1": "Nits", "in2": "Candela"}, "hidden": true}, {"slug": "nits-to-candela", "name": "Nits to Candela", "category": "light", "type": "standard", "teaser": "Assume a 1-square-meter patch to convert nits into candela.", "labels": {"in1": "Nits", "in2": "Candela"}, "hidden": true, "factor": 1.0},
{"slug": "candela-to-candlepower", "name": "Candela to Candlepower", "category": "light", "type": "standard", "teaser": "Translate candela intensity into the legacy candlepower scale (1 cd \u2248 1.01937 cp).", "labels": {"in1": "Candela", "in2": "Candlepower"}, "factor": 1.019367}, {"slug": "candela-to-candlepower", "name": "Candela to Candlepower", "category": "light", "type": "standard", "teaser": "Translate candela intensity into the legacy candlepower scale (1 cd \u2248 1.01937 cp).", "labels": {"in1": "Candela", "in2": "Candlepower"}, "factor": 1.019367},
{"slug": "candlepower-to-candela", "name": "Candlepower to Candela", "category": "light", "type": "standard", "teaser": "Flip historical candlepower values back into modern candelas (1 cp \u2248 0.981 cd).", "labels": {"in1": "Candlepower", "in2": "Candela"}, "factor": 0.981, "hidden": true}, {"slug": "candlepower-to-candela", "name": "Candlepower to Candela", "category": "light", "type": "standard", "teaser": "Flip historical candlepower values back into modern candelas (1 cp \u2248 0.981 cd).", "labels": {"in1": "Candlepower", "in2": "Candela"}, "factor": 0.981, "hidden": true},
{"slug": "candlepower-to-lumens", "name": "Candlepower to Lumens", "category": "light", "type": "standard", "teaser": "Treat candlepower as a luminous intensity, then multiply by 4\u03c0 to get flux.", "labels": {"in1": "Candlepower", "in2": "Lumens"}, "factor": 12.3269}, {"slug": "candlepower-to-lumens", "name": "Candlepower to Lumens", "category": "light", "type": "standard", "teaser": "Treat candlepower as a luminous intensity, then multiply by 4\u03c0 to get flux.", "labels": {"in1": "Candlepower", "in2": "Lumens"}, "factor": 12.3269},
@@ -1943,12 +1943,12 @@ export const calculators: CalculatorDef[] = [
{"slug": "nits-to-foot-lamberts", "name": "Nits to Foot-lamberts", "category": "other", "type": "standard", "teaser": "Translate luminance measured in nits into foot-lamberts.", "labels": {"in1": "Nits", "in2": "Foot-lamberts"}, "factor": 0.292, "hidden": true}, {"slug": "nits-to-foot-lamberts", "name": "Nits to Foot-lamberts", "category": "other", "type": "standard", "teaser": "Translate luminance measured in nits into foot-lamberts.", "labels": {"in1": "Nits", "in2": "Foot-lamberts"}, "factor": 0.292, "hidden": true},
{"slug": "phot-to-lux", "name": "Phot to Lux", "category": "light", "type": "standard", "teaser": "One phot equals 10,000 lux for film-lighting references.", "labels": {"in1": "Phot", "in2": "Lux"}, "factor": 10000.0}, {"slug": "phot-to-lux", "name": "Phot to Lux", "category": "light", "type": "standard", "teaser": "One phot equals 10,000 lux for film-lighting references.", "labels": {"in1": "Phot", "in2": "Lux"}, "factor": 10000.0},
{"slug": "lux-to-phot", "name": "Lux to Phot", "category": "light", "type": "standard", "teaser": "Convert lux into phots to match old photographic light charts.", "labels": {"in1": "Lux", "in2": "Phot"}, "factor": 0.0001, "hidden": true}, {"slug": "lux-to-phot", "name": "Lux to Phot", "category": "light", "type": "standard", "teaser": "Convert lux into phots to match old photographic light charts.", "labels": {"in1": "Lux", "in2": "Phot"}, "factor": 0.0001, "hidden": true},
{"slug": "phot-to-lamberts", "name": "Phot to Lamberts", "category": "other", "type": "standard", "teaser": "Phot and lambert coincide (1 phot = 1 lambert).", "labels": {"in1": "Phot", "in2": "Lamberts"}, "hidden": true}, {"slug": "phot-to-lamberts", "name": "Phot to Lamberts", "category": "other", "type": "standard", "teaser": "Phot and lambert coincide (1 phot = 1 lambert).", "labels": {"in1": "Phot", "in2": "Lamberts"}, "hidden": true, "factor": 1.0},
{"slug": "lamberts-to-phot", "name": "Lamberts to Phot", "category": "other", "type": "standard", "teaser": "Flip lamberts back into phots.", "labels": {"in1": "Lamberts", "in2": "Phot"}}, {"slug": "lamberts-to-phot", "name": "Lamberts to Phot", "category": "other", "type": "standard", "teaser": "Flip lamberts back into phots.", "labels": {"in1": "Lamberts", "in2": "Phot"}, "factor": 1.0},
{"slug": "phot-to-foot-lamberts", "name": "Phot to Foot-lamberts", "category": "other", "type": "standard", "teaser": "One phot (10,000 lux) equals about 929 foot-lamberts.", "labels": {"in1": "Phot", "in2": "Foot-lamberts"}, "factor": 929.03}, {"slug": "phot-to-foot-lamberts", "name": "Phot to Foot-lamberts", "category": "other", "type": "standard", "teaser": "One phot (10,000 lux) equals about 929 foot-lamberts.", "labels": {"in1": "Phot", "in2": "Foot-lamberts"}, "factor": 929.03},
{"slug": "foot-lamberts-to-phot", "name": "Foot-lamberts to Phot", "category": "other", "type": "standard", "teaser": "Express foot-lamberts as phots (1 ft-L \u2248 0.0010764 phot).", "labels": {"in1": "Foot-lamberts", "in2": "Phot"}, "factor": 0.00107639, "hidden": true}, {"slug": "foot-lamberts-to-phot", "name": "Foot-lamberts to Phot", "category": "other", "type": "standard", "teaser": "Express foot-lamberts as phots (1 ft-L \u2248 0.0010764 phot).", "labels": {"in1": "Foot-lamberts", "in2": "Phot"}, "factor": 0.00107639, "hidden": true},
{"slug": "lumens-to-lux-per-square-meter", "name": "Lumens to Lux per square meter", "category": "fluids", "type": "standard", "teaser": "Spread lumens over 1 m\u00b2 to get lux.", "labels": {"in1": "Lumens", "in2": "Lux per square meter"}}, {"slug": "lumens-to-lux-per-square-meter", "name": "Lumens to Lux per square meter", "category": "fluids", "type": "standard", "teaser": "Spread lumens over 1 m\u00b2 to get lux.", "labels": {"in1": "Lumens", "in2": "Lux per square meter"}, "factor": 1.0},
{"slug": "lux-to-lumens-per-square-meter", "name": "Lux to Lumens per square meter", "category": "fluids", "type": "standard", "teaser": "Treat lux as lumens on each square meter.", "labels": {"in1": "Lux", "in2": "Lumens per square meter"}}, {"slug": "lux-to-lumens-per-square-meter", "name": "Lux to Lumens per square meter", "category": "fluids", "type": "standard", "teaser": "Treat lux as lumens on each square meter.", "labels": {"in1": "Lux", "in2": "Lumens per square meter"}, "factor": 1.0},
{"slug": "foot-candles-to-phot", "name": "Foot-candles to Phot", "category": "other", "type": "standard", "teaser": "Convert foot-candles into phots via the 10,000 lux anchor.", "labels": {"in1": "Foot-candles", "in2": "Phot"}, "factor": 0.00107639, "hidden": true}, {"slug": "foot-candles-to-phot", "name": "Foot-candles to Phot", "category": "other", "type": "standard", "teaser": "Convert foot-candles into phots via the 10,000 lux anchor.", "labels": {"in1": "Foot-candles", "in2": "Phot"}, "factor": 0.00107639, "hidden": true},
{"slug": "phot-to-foot-candles", "name": "Phot to Foot-candles", "category": "other", "type": "standard", "teaser": "Express phots as foot-candles for practical light meter use.", "labels": {"in1": "Phot", "in2": "Foot-candles"}, "factor": 929.03}, {"slug": "phot-to-foot-candles", "name": "Phot to Foot-candles", "category": "other", "type": "standard", "teaser": "Express phots as foot-candles for practical light meter use.", "labels": {"in1": "Phot", "in2": "Foot-candles"}, "factor": 929.03},
{"slug": "lumens-to-nits-per-square-meter", "name": "Lumens to Nits per square meter", "category": "fluids", "type": "standard", "teaser": "A lumen/m\u00b2 (lux) equals about 0.31831 nits under Lambertian lighting.", "labels": {"in1": "Lumens", "in2": "Nits per square meter"}, "factor": 0.318309886}, {"slug": "lumens-to-nits-per-square-meter", "name": "Lumens to Nits per square meter", "category": "fluids", "type": "standard", "teaser": "A lumen/m\u00b2 (lux) equals about 0.31831 nits under Lambertian lighting.", "labels": {"in1": "Lumens", "in2": "Nits per square meter"}, "factor": 0.318309886},
@@ -1973,7 +1973,7 @@ export const calculators: CalculatorDef[] = [
{"slug": "metric-horsepower-to-watts", "name": "Metric Horsepower (PS) to Watts", "category": "power", "type": "standard", "teaser": "Convert metric horsepower into watts for European ratings.", "labels": {"in1": "Metric Horsepower (PS)", "in2": "Watts"}, "factor": 735.49875}, {"slug": "metric-horsepower-to-watts", "name": "Metric Horsepower (PS) to Watts", "category": "power", "type": "standard", "teaser": "Convert metric horsepower into watts for European ratings.", "labels": {"in1": "Metric Horsepower (PS)", "in2": "Watts"}, "factor": 735.49875},
{"slug": "metric-tons-to-kilograms", "name": "Metric Tons to Kilograms", "category": "weight", "type": "standard", "teaser": "Convert metric tons into kilograms for bulk weights.", "labels": {"in1": "Metric Tons", "in2": "Kilograms"}, "factor": 1000.0}, {"slug": "metric-tons-to-kilograms", "name": "Metric Tons to Kilograms", "category": "weight", "type": "standard", "teaser": "Convert metric tons into kilograms for bulk weights.", "labels": {"in1": "Metric Tons", "in2": "Kilograms"}, "factor": 1000.0},
{"slug": "metric-tons-to-pounds", "name": "Metric Tons to Pounds", "category": "weight", "type": "standard", "teaser": "Convert metric tons into pounds.", "labels": {"in1": "Metric Tons", "in2": "Pounds"}, "factor": 2204.62262}, {"slug": "metric-tons-to-pounds", "name": "Metric Tons to Pounds", "category": "weight", "type": "standard", "teaser": "Convert metric tons into pounds.", "labels": {"in1": "Metric Tons", "in2": "Pounds"}, "factor": 2204.62262},
{"slug": "mg-per-liter-to-ppm", "name": "mg/L to PPM", "category": "fluids", "type": "standard", "teaser": "Treat milligrams per liter as parts per million for dilute solutions.", "labels": {"in1": "mg/L", "in2": "PPM"}}, {"slug": "mg-per-liter-to-ppm", "name": "mg/L to PPM", "category": "fluids", "type": "standard", "teaser": "Treat milligrams per liter as parts per million for dilute solutions.", "labels": {"in1": "mg/L", "in2": "PPM"}, "factor": 1.0},
{"slug": "microfarads-to-picofarads", "name": "Microfarads to Picofarads", "category": "radiation", "type": "standard", "teaser": "Convert microfarads to picofarads.", "labels": {"in1": "Microfarads", "in2": "Picofarads"}, "factor": 1000000.0}, {"slug": "microfarads-to-picofarads", "name": "Microfarads to Picofarads", "category": "radiation", "type": "standard", "teaser": "Convert microfarads to picofarads.", "labels": {"in1": "Microfarads", "in2": "Picofarads"}, "factor": 1000000.0},
{"slug": "microhenries-to-millihenries", "name": "Microhenries to Millihenries", "category": "angle", "type": "standard", "teaser": "Convert inductance from microhenries to millihenries.", "labels": {"in1": "Microhenries", "in2": "Millihenries"}, "factor": 0.001, "hidden": true}, {"slug": "microhenries-to-millihenries", "name": "Microhenries to Millihenries", "category": "angle", "type": "standard", "teaser": "Convert inductance from microhenries to millihenries.", "labels": {"in1": "Microhenries", "in2": "Millihenries"}, "factor": 0.001, "hidden": true},
{"slug": "microns-to-millimeters", "name": "Microns to Millimeters", "category": "length", "type": "standard", "teaser": "Convert microns (micrometers) to millimeters.", "labels": {"in1": "Microns", "in2": "Millimeters"}, "factor": 0.001, "hidden": true}, {"slug": "microns-to-millimeters", "name": "Microns to Millimeters", "category": "length", "type": "standard", "teaser": "Convert microns (micrometers) to millimeters.", "labels": {"in1": "Microns", "in2": "Millimeters"}, "factor": 0.001, "hidden": true},
@@ -2203,7 +2203,7 @@ export const calculators: CalculatorDef[] = [
{"slug": "radians-to-mils", "name": "Radians to Mils", "category": "angle", "type": "standard", "teaser": "Express radians as mils for precision shooting.", "labels": {"in1": "Radians", "in2": "Mils"}, "factor": 1000.0}, {"slug": "radians-to-mils", "name": "Radians to Mils", "category": "angle", "type": "standard", "teaser": "Express radians as mils for precision shooting.", "labels": {"in1": "Radians", "in2": "Mils"}, "factor": 1000.0},
{"slug": "radians-to-turns", "name": "Radians to Turns", "category": "angle", "type": "standard", "teaser": "Convert radians into turns of a circle.", "labels": {"in1": "Radians", "in2": "Turns"}, "factor": 0.15915494309189535}, {"slug": "radians-to-turns", "name": "Radians to Turns", "category": "angle", "type": "standard", "teaser": "Convert radians into turns of a circle.", "labels": {"in1": "Radians", "in2": "Turns"}, "factor": 0.15915494309189535},
{"slug": "rankine-to-celsius", "name": "Rankine to Celsius", "category": "temperature", "type": "standard", "teaser": "Convert Rankine into Celsius degrees.", "labels": {"in1": "Rankine", "in2": "Celsius"}, "factor": 0.5555555555555556, "offset": -273.15}, {"slug": "rankine-to-celsius", "name": "Rankine to Celsius", "category": "temperature", "type": "standard", "teaser": "Convert Rankine into Celsius degrees.", "labels": {"in1": "Rankine", "in2": "Celsius"}, "factor": 0.5555555555555556, "offset": -273.15},
{"slug": "rankine-to-fahrenheit", "name": "Rankine to Fahrenheit", "category": "temperature", "type": "standard", "teaser": "Show Rankine as Fahrenheit.", "labels": {"in1": "Rankine", "in2": "Fahrenheit"}, "offset": -459.67, "hidden": true}, {"slug": "rankine-to-fahrenheit", "name": "Rankine to Fahrenheit", "category": "temperature", "type": "standard", "teaser": "Show Rankine as Fahrenheit.", "labels": {"in1": "Rankine", "in2": "Fahrenheit"}, "offset": -459.67, "hidden": true, "factor": 1.0},
{"slug": "rankine-to-kelvin", "name": "Rankine to Kelvin", "category": "temperature", "type": "standard", "teaser": "Turn Rankine into Kelvin.", "labels": {"in1": "Rankine", "in2": "Kelvin"}, "factor": 0.5555555555555556}, {"slug": "rankine-to-kelvin", "name": "Rankine to Kelvin", "category": "temperature", "type": "standard", "teaser": "Turn Rankine into Kelvin.", "labels": {"in1": "Rankine", "in2": "Kelvin"}, "factor": 0.5555555555555556},
{"slug": "rem-to-millisievert", "name": "Rem to Millisievert", "category": "angle", "type": "standard", "teaser": "Convert rems into millisieverts.", "labels": {"in1": "Rem", "in2": "Millisievert"}, "factor": 10.0}, {"slug": "rem-to-millisievert", "name": "Rem to Millisievert", "category": "angle", "type": "standard", "teaser": "Convert rems into millisieverts.", "labels": {"in1": "Rem", "in2": "Millisievert"}, "factor": 10.0},
{"slug": "rem-to-rad", "name": "Rem to Rad", "category": "radiation", "type": "standard", "teaser": "Treat rems as rads with a 1:1 ratio.", "labels": {"in1": "Rem", "in2": "Rad"}, "factor": 1.0}, {"slug": "rem-to-rad", "name": "Rem to Rad", "category": "radiation", "type": "standard", "teaser": "Treat rems as rads with a 1:1 ratio.", "labels": {"in1": "Rem", "in2": "Rad"}, "factor": 1.0},
@@ -2267,8 +2267,8 @@ export const calculators: CalculatorDef[] = [
{"slug": "moles-per-hour-to-moles-per-second", "name": "Moles per hour to Moles per second", "category": "other", "type": "standard", "teaser": "Return molar flow rates back to per-second units.", "labels": {"in1": "Moles per hour", "in2": "Moles per second"}, "factor": 0.0002777778, "hidden": true}, {"slug": "moles-per-hour-to-moles-per-second", "name": "Moles per hour to Moles per second", "category": "other", "type": "standard", "teaser": "Return molar flow rates back to per-second units.", "labels": {"in1": "Moles per hour", "in2": "Moles per second"}, "factor": 0.0002777778, "hidden": true},
{"slug": "kilograms-per-second-per-square-meter-to-grams-per-second-per-square-centimeter", "name": "Kilograms per second per square meter to Grams per second per square centimeter", "category": "other", "type": "standard", "teaser": "Express mass flux density using metric subunits.", "labels": {"in1": "Kilograms per second per square meter", "in2": "Grams per second per square centimeter"}, "factor": 0.1, "hidden": true}, {"slug": "kilograms-per-second-per-square-meter-to-grams-per-second-per-square-centimeter", "name": "Kilograms per second per square meter to Grams per second per square centimeter", "category": "other", "type": "standard", "teaser": "Express mass flux density using metric subunits.", "labels": {"in1": "Kilograms per second per square meter", "in2": "Grams per second per square centimeter"}, "factor": 0.1, "hidden": true},
{"slug": "grams-per-second-per-square-centimeter-to-kilograms-per-second-per-square-meter", "name": "Grams per second per square centimeter to Kilograms per second per square meter", "category": "other", "type": "standard", "teaser": "Convert compact mass flux into the SI-friendly base.", "labels": {"in1": "Grams per second per square centimeter", "in2": "Kilograms per second per square meter"}, "factor": 10.0}, {"slug": "grams-per-second-per-square-centimeter-to-kilograms-per-second-per-square-meter", "name": "Grams per second per square centimeter to Kilograms per second per square meter", "category": "other", "type": "standard", "teaser": "Convert compact mass flux into the SI-friendly base.", "labels": {"in1": "Grams per second per square centimeter", "in2": "Kilograms per second per square meter"}, "factor": 10.0},
{"slug": "mol-per-cubic-meter-to-mmol-per-liter", "name": "Mol per cubic meter to mmol per liter", "category": "fluids", "type": "standard", "teaser": "Translate molar concentrations across common volume units.", "labels": {"in1": "Mol per cubic meter", "in2": "mmol per liter"}, "hidden": true}, {"slug": "mol-per-cubic-meter-to-mmol-per-liter", "name": "Mol per cubic meter to mmol per liter", "category": "fluids", "type": "standard", "teaser": "Translate molar concentrations across common volume units.", "labels": {"in1": "Mol per cubic meter", "in2": "mmol per liter"}, "hidden": true, "factor": 1.0},
{"slug": "mmol-per-liter-to-mol-per-cubic-meter", "name": "mmol per liter to mol per cubic meter", "category": "fluids", "type": "standard", "teaser": "Convert molar concentration back into SI cubic meters.", "labels": {"in1": "mmol per liter", "in2": "mol per cubic meter"}}, {"slug": "mmol-per-liter-to-mol-per-cubic-meter", "name": "mmol per liter to mol per cubic meter", "category": "fluids", "type": "standard", "teaser": "Convert molar concentration back into SI cubic meters.", "labels": {"in1": "mmol per liter", "in2": "mol per cubic meter"}, "factor": 1.0},
{"slug": "percent-by-mass-to-ppm", "name": "Percent by mass to ppm", "category": "other", "type": "standard", "teaser": "Turn mass-percent concentrations into parts-per-million.", "labels": {"in1": "Percent by mass", "in2": "ppm"}, "factor": 10000.0}, {"slug": "percent-by-mass-to-ppm", "name": "Percent by mass to ppm", "category": "other", "type": "standard", "teaser": "Turn mass-percent concentrations into parts-per-million.", "labels": {"in1": "Percent by mass", "in2": "ppm"}, "factor": 10000.0},
{"slug": "ppm-to-percent-by-mass", "name": "ppm to Percent by mass", "category": "other", "type": "standard", "teaser": "Return ppm values to mass-percent.", "labels": {"in1": "ppm", "in2": "Percent by mass"}, "factor": 0.0001, "hidden": true}, {"slug": "ppm-to-percent-by-mass", "name": "ppm to Percent by mass", "category": "other", "type": "standard", "teaser": "Return ppm values to mass-percent.", "labels": {"in1": "ppm", "in2": "Percent by mass"}, "factor": 0.0001, "hidden": true},
{"slug": "pascal-second-to-poise", "name": "Pascal-second to Poise", "category": "pressure", "type": "standard", "teaser": "Convert SI dynamic viscosity into CGS poise.", "labels": {"in1": "Pascal-second", "in2": "Poise"}, "factor": 10.0}, {"slug": "pascal-second-to-poise", "name": "Pascal-second to Poise", "category": "pressure", "type": "standard", "teaser": "Convert SI dynamic viscosity into CGS poise.", "labels": {"in1": "Pascal-second", "in2": "Poise"}, "factor": 10.0},
@@ -2277,8 +2277,8 @@ export const calculators: CalculatorDef[] = [
{"slug": "centistokes-to-square-meter-per-second", "name": "Centistokes to Square meter per second", "category": "other", "type": "standard", "teaser": "Return centistokes back to base square meters per second.", "labels": {"in1": "Centistokes", "in2": "Square meter per second"}, "factor": 0.01, "hidden": true}, {"slug": "centistokes-to-square-meter-per-second", "name": "Centistokes to Square meter per second", "category": "other", "type": "standard", "teaser": "Return centistokes back to base square meters per second.", "labels": {"in1": "Centistokes", "in2": "Square meter per second"}, "factor": 0.01, "hidden": true},
{"slug": "newton-per-meter-to-dyne-per-centimeter", "name": "Newton per meter to Dyne per centimeter", "category": "force", "type": "standard", "teaser": "Express surface tension on the CGS scale.", "labels": {"in1": "Newton per meter", "in2": "Dyne per centimeter"}, "factor": 1000.0}, {"slug": "newton-per-meter-to-dyne-per-centimeter", "name": "Newton per meter to Dyne per centimeter", "category": "force", "type": "standard", "teaser": "Express surface tension on the CGS scale.", "labels": {"in1": "Newton per meter", "in2": "Dyne per centimeter"}, "factor": 1000.0},
{"slug": "dyne-per-centimeter-to-newton-per-meter", "name": "Dyne per centimeter to Newton per meter", "category": "force", "type": "standard", "teaser": "Convert surface tension back into SI.", "labels": {"in1": "Dyne per centimeter", "in2": "Newton per meter"}, "factor": 0.001, "hidden": true}, {"slug": "dyne-per-centimeter-to-newton-per-meter", "name": "Dyne per centimeter to Newton per meter", "category": "force", "type": "standard", "teaser": "Convert surface tension back into SI.", "labels": {"in1": "Dyne per centimeter", "in2": "Newton per meter"}, "factor": 0.001, "hidden": true},
{"slug": "henry-per-meter-to-tesla-meter-per-ampere", "name": "Henry per meter to Tesla-meter per ampere", "category": "magnetism", "type": "standard", "teaser": "Equate magnetic permeability units across SI conventions.", "labels": {"in1": "Henry per meter", "in2": "Tesla-meter per ampere"}}, {"slug": "henry-per-meter-to-tesla-meter-per-ampere", "name": "Henry per meter to Tesla-meter per ampere", "category": "magnetism", "type": "standard", "teaser": "Equate magnetic permeability units across SI conventions.", "labels": {"in1": "Henry per meter", "in2": "Tesla-meter per ampere"}, "factor": 1.0},
{"slug": "tesla-meter-per-ampere-to-henry-per-meter", "name": "Tesla-meter per ampere to Henry per meter", "category": "magnetism", "type": "standard", "teaser": "Return permeability back to henry per meter.", "labels": {"in1": "Tesla-meter per ampere", "in2": "Henry per meter"}, "hidden": true}, {"slug": "tesla-meter-per-ampere-to-henry-per-meter", "name": "Tesla-meter per ampere to Henry per meter", "category": "magnetism", "type": "standard", "teaser": "Return permeability back to henry per meter.", "labels": {"in1": "Tesla-meter per ampere", "in2": "Henry per meter"}, "hidden": true, "factor": 1.0},
{"slug": "yards-per-second-to-centimeters-per-second", "name": "Yards Per Second to Centimeters Per Second", "category": "speed", "type": "standard", "teaser": "Express yards-per-second speeds as centimeters per second.", "labels": {"in1": "Yards Per Second", "in2": "Centimeters Per Second"}, "factor": 91.44}, {"slug": "yards-per-second-to-centimeters-per-second", "name": "Yards Per Second to Centimeters Per Second", "category": "speed", "type": "standard", "teaser": "Express yards-per-second speeds as centimeters per second.", "labels": {"in1": "Yards Per Second", "in2": "Centimeters Per Second"}, "factor": 91.44},
{"slug": "yards-per-second-to-feet-per-second", "name": "Yards Per Second to Feet Per Second", "category": "speed", "type": "standard", "teaser": "Convert yards per second into feet per second for incremental comparisons.", "labels": {"in1": "Yards Per Second", "in2": "Feet Per Second"}, "factor": 3.0}, {"slug": "yards-per-second-to-feet-per-second", "name": "Yards Per Second to Feet Per Second", "category": "speed", "type": "standard", "teaser": "Convert yards per second into feet per second for incremental comparisons.", "labels": {"in1": "Yards Per Second", "in2": "Feet Per Second"}, "factor": 3.0},
{"slug": "yards-per-second-to-furlongs-per-fortnight", "name": "Yards Per Second to Furlongs Per Fortnight", "category": "speed", "type": "standard", "teaser": "Turn yards-per-second pacing into furlongs per fortnight for playful analogies.", "labels": {"in1": "Yards Per Second", "in2": "Furlongs Per Fortnight"}, "factor": 5498.181818181818}, {"slug": "yards-per-second-to-furlongs-per-fortnight", "name": "Yards Per Second to Furlongs Per Fortnight", "category": "speed", "type": "standard", "teaser": "Turn yards-per-second pacing into furlongs per fortnight for playful analogies.", "labels": {"in1": "Yards Per Second", "in2": "Furlongs Per Fortnight"}, "factor": 5498.181818181818},
@@ -2368,7 +2368,7 @@ export const calculators: CalculatorDef[] = [
{"slug": "em-to-pixels", "name": "Em to Pixels", "category": "other", "type": "standard", "teaser": "Convert CSS em units to pixels assuming a 16px base size.", "labels": {"in1": "Em", "in2": "Pixels"}, "factor": 16.0}, {"slug": "em-to-pixels", "name": "Em to Pixels", "category": "other", "type": "standard", "teaser": "Convert CSS em units to pixels assuming a 16px base size.", "labels": {"in1": "Em", "in2": "Pixels"}, "factor": 16.0},
{"slug": "ev-to-lux", "name": "EV (exposure value) to Lux", "category": "light", "type": "ev-lux", "teaser": "Estimate scene illuminance at ISO 100.", "labels": {"in1": "EV (exposure value)", "in2": "Lux"}}, {"slug": "ev-to-lux", "name": "EV (exposure value) to Lux", "category": "light", "type": "ev-lux", "teaser": "Estimate scene illuminance at ISO 100.", "labels": {"in1": "EV (exposure value)", "in2": "Lux"}},
{"slug": "lux-to-ev", "name": "Lux to EV", "category": "light", "type": "ev-lux", "teaser": "Convert lux readings to exposure value at ISO 100.", "labels": {"in1": "Lux", "in2": "EV"}, "hidden": true}, {"slug": "lux-to-ev", "name": "Lux to EV", "category": "light", "type": "ev-lux", "teaser": "Convert lux readings to exposure value at ISO 100.", "labels": {"in1": "Lux", "in2": "EV"}, "hidden": true},
{"slug": "f-stops-to-t-stops", "name": "f-stops to T-stops", "category": "other", "type": "standard", "teaser": "Treat f-number as t-stop under ideal transmission.", "labels": {"in1": "f-stops", "in2": "T-stops"}}, {"slug": "f-stops-to-t-stops", "name": "f-stops to T-stops", "category": "other", "type": "standard", "teaser": "Treat f-number as t-stop under ideal transmission.", "labels": {"in1": "f-stops", "in2": "T-stops"}, "factor": 1.0},
{"slug": "focal-length-to-angle-of-view", "name": "Focal length to Angle of view", "category": "other", "type": "aov", "teaser": "Approximate horizontal angle on 35mm full-frame (36mm width).", "labels": {"in1": "Focal length", "in2": "Angle of view"}}, {"slug": "focal-length-to-angle-of-view", "name": "Focal length to Angle of view", "category": "other", "type": "aov", "teaser": "Approximate horizontal angle on 35mm full-frame (36mm width).", "labels": {"in1": "Focal length", "in2": "Angle of view"}},
{"slug": "millimeters-to-awg", "name": "Millimeters to AWG", "category": "electrical", "type": "awg", "teaser": "Convert conductor diameter in millimeters to AWG gauge.", "labels": {"in1": "Millimeters", "in2": "AWG"}}, {"slug": "millimeters-to-awg", "name": "Millimeters to AWG", "category": "electrical", "type": "awg", "teaser": "Convert conductor diameter in millimeters to AWG gauge.", "labels": {"in1": "Millimeters", "in2": "AWG"}},
{"slug": "molarity-to-grams-per-liter", "name": "Molarity to Grams per liter", "category": "other", "type": "molarity", "teaser": "Convert molar concentration to grams per liter using molar mass.", "labels": {"in1": "Molarity (mol/L)", "in2": "Grams per liter", "in3": "Molar mass (g/mol)"}, "hidden": true}, {"slug": "molarity-to-grams-per-liter", "name": "Molarity to Grams per liter", "category": "other", "type": "molarity", "teaser": "Convert molar concentration to grams per liter using molar mass.", "labels": {"in1": "Molarity (mol/L)", "in2": "Grams per liter", "in3": "Molar mass (g/mol)"}, "hidden": true},
@@ -2684,7 +2684,7 @@ export const calculators: CalculatorDef[] = [
{"slug": "barye-to-atmosphere", "name": "Barye to Atmosphere", "category": "pressure", "type": "standard", "labels": {"in1": "Barye", "in2": "Atmosphere"}, "factor": 9.86923266716013e-07}, {"slug": "barye-to-atmosphere", "name": "Barye to Atmosphere", "category": "pressure", "type": "standard", "labels": {"in1": "Barye", "in2": "Atmosphere"}, "factor": 9.86923266716013e-07},
{"slug": "barye-to-bar", "name": "Barye to Bar", "category": "pressure", "type": "standard", "labels": {"in1": "Barye", "in2": "Bar"}, "factor": 1e-06, "hidden": true}, {"slug": "barye-to-bar", "name": "Barye to Bar", "category": "pressure", "type": "standard", "labels": {"in1": "Barye", "in2": "Bar"}, "factor": 1e-06, "hidden": true},
{"slug": "barye-to-cmhg", "name": "Barye to Cmhg", "category": "pressure", "type": "standard", "labels": {"in1": "Barye", "in2": "Cmhg"}, "factor": 7.500637554192107e-05}, {"slug": "barye-to-cmhg", "name": "Barye to Cmhg", "category": "pressure", "type": "standard", "labels": {"in1": "Barye", "in2": "Cmhg"}, "factor": 7.500637554192107e-05},
{"slug": "barye-to-dynes-per-sq-cm", "name": "Barye to Dynes Per Sq Cm", "category": "pressure", "type": "standard", "labels": {"in1": "Barye", "in2": "Dynes Per Sq Cm"}}, {"slug": "barye-to-dynes-per-sq-cm", "name": "Barye to Dynes Per Sq Cm", "category": "pressure", "type": "standard", "labels": {"in1": "Barye", "in2": "Dynes Per Sq Cm"}, "factor": 1.0},
{"slug": "barye-to-feet-of-seawater", "name": "Barye to Feet Of Seawater", "category": "length", "type": "standard", "labels": {"in1": "Barye", "in2": "Feet Of Seawater"}, "factor": 3.2594581222958305e-05}, {"slug": "barye-to-feet-of-seawater", "name": "Barye to Feet Of Seawater", "category": "length", "type": "standard", "labels": {"in1": "Barye", "in2": "Feet Of Seawater"}, "factor": 3.2594581222958305e-05},
{"slug": "barye-to-feet-of-water", "name": "Barye to Feet Of Water", "category": "length", "type": "standard", "labels": {"in1": "Barye", "in2": "Feet Of Water"}, "factor": 3.349601334800432e-05, "hidden": true}, {"slug": "barye-to-feet-of-water", "name": "Barye to Feet Of Water", "category": "length", "type": "standard", "labels": {"in1": "Barye", "in2": "Feet Of Water"}, "factor": 3.349601334800432e-05, "hidden": true},
{"slug": "barye-to-hectopascals", "name": "Barye to Hectopascals", "category": "pressure", "type": "standard", "labels": {"in1": "Barye", "in2": "Hectopascals"}, "factor": 0.001, "hidden": true}, {"slug": "barye-to-hectopascals", "name": "Barye to Hectopascals", "category": "pressure", "type": "standard", "labels": {"in1": "Barye", "in2": "Hectopascals"}, "factor": 0.001, "hidden": true},
@@ -2941,7 +2941,7 @@ export const calculators: CalculatorDef[] = [
{"slug": "inch-pounds-to-dyne-centimeters", "name": "Inch-pounds to Dyne-centimeters", "category": "energy", "type": "standard", "teaser": "Express inch-pound torque as dyne-centimeters for CGS-friendly references.", "labels": {"in1": "Inch-pounds", "in2": "Dyne-centimeters"}, "factor": 1129848.2902762}, {"slug": "inch-pounds-to-dyne-centimeters", "name": "Inch-pounds to Dyne-centimeters", "category": "energy", "type": "standard", "teaser": "Express inch-pound torque as dyne-centimeters for CGS-friendly references.", "labels": {"in1": "Inch-pounds", "in2": "Dyne-centimeters"}, "factor": 1129848.2902762},
{"slug": "dynes-per-sq-cm-to-atmosphere", "name": "Dynes Per Sq Cm to Atmosphere", "category": "pressure", "type": "standard", "labels": {"in1": "Dynes Per Sq Cm", "in2": "Atmosphere"}, "factor": 9.86923266716013e-07, "hidden": true}, {"slug": "dynes-per-sq-cm-to-atmosphere", "name": "Dynes Per Sq Cm to Atmosphere", "category": "pressure", "type": "standard", "labels": {"in1": "Dynes Per Sq Cm", "in2": "Atmosphere"}, "factor": 9.86923266716013e-07, "hidden": true},
{"slug": "dynes-per-sq-cm-to-bar", "name": "Dynes Per Sq Cm to Bar", "category": "pressure", "type": "standard", "labels": {"in1": "Dynes Per Sq Cm", "in2": "Bar"}, "factor": 1e-06, "hidden": true}, {"slug": "dynes-per-sq-cm-to-bar", "name": "Dynes Per Sq Cm to Bar", "category": "pressure", "type": "standard", "labels": {"in1": "Dynes Per Sq Cm", "in2": "Bar"}, "factor": 1e-06, "hidden": true},
{"slug": "dynes-per-sq-cm-to-barye", "name": "Dynes Per Sq Cm to Barye", "category": "pressure", "type": "standard", "labels": {"in1": "Dynes Per Sq Cm", "in2": "Barye"}, "hidden": true}, {"slug": "dynes-per-sq-cm-to-barye", "name": "Dynes Per Sq Cm to Barye", "category": "pressure", "type": "standard", "labels": {"in1": "Dynes Per Sq Cm", "in2": "Barye"}, "hidden": true, "factor": 1.0},
{"slug": "dynes-per-sq-cm-to-cmhg", "name": "Dynes Per Sq Cm to Cmhg", "category": "pressure", "type": "standard", "labels": {"in1": "Dynes Per Sq Cm", "in2": "Cmhg"}, "factor": 7.500637554192107e-05}, {"slug": "dynes-per-sq-cm-to-cmhg", "name": "Dynes Per Sq Cm to Cmhg", "category": "pressure", "type": "standard", "labels": {"in1": "Dynes Per Sq Cm", "in2": "Cmhg"}, "factor": 7.500637554192107e-05},
{"slug": "dynes-per-sq-cm-to-feet-of-seawater", "name": "Dynes Per Sq Cm to Feet Of Seawater", "category": "pressure", "type": "standard", "labels": {"in1": "Dynes Per Sq Cm", "in2": "Feet Of Seawater"}, "factor": 3.2594581222958305e-05}, {"slug": "dynes-per-sq-cm-to-feet-of-seawater", "name": "Dynes Per Sq Cm to Feet Of Seawater", "category": "pressure", "type": "standard", "labels": {"in1": "Dynes Per Sq Cm", "in2": "Feet Of Seawater"}, "factor": 3.2594581222958305e-05},
{"slug": "dynes-per-sq-cm-to-feet-of-water", "name": "Dynes Per Sq Cm to Feet Of Water", "category": "pressure", "type": "standard", "labels": {"in1": "Dynes Per Sq Cm", "in2": "Feet Of Water"}, "factor": 3.349601334800432e-05, "hidden": true}, {"slug": "dynes-per-sq-cm-to-feet-of-water", "name": "Dynes Per Sq Cm to Feet Of Water", "category": "pressure", "type": "standard", "labels": {"in1": "Dynes Per Sq Cm", "in2": "Feet Of Water"}, "factor": 3.349601334800432e-05, "hidden": true},
@@ -3167,7 +3167,10 @@ export const calculators: CalculatorDef[] = [
]; ];
const slugIndex = new Map(calculators.map(c => [c.slug, c])); const slugIndex: Map<string, CalculatorDef> = new Map(
calculators.map(calc => [calc.slug, calc])
);
export function getCalculatorBySlug(slug: string): CalculatorDef | undefined { export function getCalculatorBySlug(slug: string): CalculatorDef | undefined {
return slugIndex.get(slug); return slugIndex.get(slug);

View File

@@ -0,0 +1,85 @@
// THIS FILE IS AUTO-GENERATED BY migrate.py
export const categories: Record<string, { label: string; icon: string }> = {
"length": {
"label": "Length / Distance",
"icon": "📏"
},
"weight": {
"label": "Weight / Mass",
"icon": "⚖️"
},
"temperature": {
"label": "Temperature",
"icon": "🌡️"
},
"volume": {
"label": "Volume",
"icon": "🧪"
},
"fluids": {
"label": "Fluids",
"icon": "💧"
},
"area": {
"label": "Area",
"icon": "🔳"
},
"speed": {
"label": "Speed / Velocity",
"icon": "💨"
},
"pressure": {
"label": "Pressure",
"icon": "🔽"
},
"energy": {
"label": "Energy",
"icon": "⚡"
},
"magnetism": {
"label": "Magnetism",
"icon": "🧲"
},
"power": {
"label": "Power",
"icon": "🔌"
},
"data": {
"label": "Data Storage",
"icon": "💾"
},
"time": {
"label": "Time",
"icon": "⏱️"
},
"angle": {
"label": "Angle",
"icon": "📐"
},
"number-systems": {
"label": "Number Systems",
"icon": "🔢"
},
"radiation": {
"label": "Radiation",
"icon": "☢️"
},
"electrical": {
"label": "Electrical",
"icon": "🔋"
},
"force": {
"label": "Force / Torque",
"icon": "💪"
},
"light": {
"label": "Light",
"icon": "💡"
},
"other": {
"label": "Other",
"icon": "🔄"
}
};
export const totalCalculators = 3124;

View File

@@ -1,4 +1,4 @@
import { calculators } from './calculators'; import { loadCalculators, type CalculatorDef } from './calculatorLoader';
const domainDefinitions: Record<string, { summary: string; context: string }> = { const domainDefinitions: Record<string, { summary: string; context: string }> = {
length: { length: {
@@ -97,7 +97,6 @@ const normalizeLabel = (label?: string): string | undefined => {
return alias ?? trimmed; return alias ?? trimmed;
}; };
const definitions: Record<string, Record<string, string>> = {};
const categoryPriority = [...Object.keys(domainDefinitions)]; const categoryPriority = [...Object.keys(domainDefinitions)];
const buildDefinition = (label: string, categoryKey: string): string => { const buildDefinition = (label: string, categoryKey: string): string => {
@@ -107,17 +106,36 @@ const buildDefinition = (label: string, categoryKey: string): string => {
return `${label} ${description}`; return `${label} ${description}`;
}; };
calculators.forEach(calc => { // Lazily built definitions cache
const { category, labels } = calc; let definitions: Record<string, Record<string, string>> | null = null;
Object.values(labels).forEach(label => { let buildPromise: Promise<void> | null = null;
const normalized = normalizeLabel(label);
if (!normalized) return; async function ensureBuilt(): Promise<Record<string, Record<string, string>>> {
const bucket = definitions[normalized] || {}; if (definitions) return definitions;
const text = buildDefinition(normalized, category); if (buildPromise) {
bucket[category] = text; await buildPromise;
definitions[normalized] = bucket; return definitions!;
}
buildPromise = loadCalculators().then(calculators => {
const defs: Record<string, Record<string, string>> = {};
calculators.forEach(calc => {
const { category, labels } = calc;
Object.values(labels).forEach(label => {
const normalized = normalizeLabel(label);
if (!normalized) return;
const bucket = defs[normalized] || {};
const text = buildDefinition(normalized, category);
bucket[category] = text;
defs[normalized] = bucket;
});
});
definitions = defs;
}); });
});
await buildPromise;
return definitions!;
}
const findByPriority = (entries: Record<string, string>, preferred?: string): string | undefined => { const findByPriority = (entries: Record<string, string>, preferred?: string): string | undefined => {
if (!entries) return undefined; if (!entries) return undefined;
@@ -129,11 +147,10 @@ const findByPriority = (entries: Record<string, string>, preferred?: string): st
return fallback.length ? fallback[0] : undefined; return fallback.length ? fallback[0] : undefined;
}; };
export function getDefinition(label: string, category?: string): string | undefined { export async function getDefinition(label: string, category?: string): Promise<string | undefined> {
const normalized = normalizeLabel(label); const normalized = normalizeLabel(label);
if (!normalized) return undefined; if (!normalized) return undefined;
const entries = definitions[normalized]; const defs = await ensureBuilt();
const entries = defs[normalized];
return findByPriority(entries, category); return findByPriority(entries, category);
} }
export const unitDefinitions = definitions;

View File

@@ -0,0 +1,244 @@
export type ThemeMode = 'light' | 'dark';
export type PaletteVar =
| 'bg'
| 'bg-elevated'
| 'sidebar-bg'
| 'card-bg'
| 'input-bg'
| 'hover-bg'
| 'border'
| 'text'
| 'text-muted'
| 'accent'
| 'accent-dark'
| 'accent-glow'
| 'accent-gradient'
| 'header-bg';
export type PaletteTheme = Record<PaletteVar, string>;
export type Palette = {
slug: string;
label: string;
light: PaletteTheme;
dark: PaletteTheme;
};
export const palettes: Palette[] = [
{
slug: 'classic',
label: 'Classic',
light: {
bg: '#f8fafc',
'bg-elevated': '#ffffff',
'sidebar-bg': '#ffffff',
'card-bg': '#ffffff',
'input-bg': 'rgba(15, 23, 42, 0.04)',
'hover-bg': 'rgba(15, 23, 42, 0.08)',
border: 'rgba(15, 23, 42, 0.12)',
text: '#0f172a',
'text-muted': '#475569',
accent: '#10b981',
'accent-dark': '#059669',
'accent-glow': 'rgba(16, 185, 129, 0.15)',
'accent-gradient': 'linear-gradient(135deg, #10b981, #06b6d4)',
'header-bg': 'rgba(255, 255, 255, 0.95)',
},
dark: {
bg: '#0c0f14',
'bg-elevated': '#12161e',
'sidebar-bg': '#10141b',
'card-bg': 'rgba(18, 22, 30, 0.85)',
'input-bg': 'rgba(255, 255, 255, 0.04)',
'hover-bg': 'rgba(255, 255, 255, 0.06)',
border: 'rgba(255, 255, 255, 0.08)',
text: '#e8ecf4',
'text-muted': '#7b8498',
accent: '#10b981',
'accent-dark': '#059669',
'accent-glow': 'rgba(16, 185, 129, 0.15)',
'accent-gradient': 'linear-gradient(135deg, #10b981, #06b6d4)',
'header-bg': 'rgba(12, 15, 20, 0.85)',
},
},
{
slug: 'emerald',
label: 'Emerald',
light: {
'bg': '#f6fbf9',
'bg-elevated': '#ffffff',
'sidebar-bg': '#ffffff',
'card-bg': '#ffffff',
'input-bg': '#ecf7f1',
'hover-bg': '#d5f0df',
'border': 'rgba(4, 120, 87, 0.25)',
'text': '#0b2c1f',
'text-muted': '#4a6b5c',
'accent': '#047857',
'accent-dark': '#065f46',
'accent-glow': 'rgba(4, 120, 87, 0.2)',
'accent-gradient': 'linear-gradient(135deg, #047857, #0ea5e9)',
'header-bg': 'rgba(255, 255, 255, 0.95)',
},
dark: {
'bg': '#0b1313',
'bg-elevated': 'rgba(4, 20, 15, 0.85)',
'sidebar-bg': '#08110f',
'card-bg': 'rgba(6, 19, 13, 0.75)',
'input-bg': 'rgba(16, 185, 129, 0.08)',
'hover-bg': 'rgba(16, 185, 129, 0.12)',
'border': 'rgba(16, 185, 129, 0.35)',
'text': '#e9fcea',
'text-muted': '#9fdac4',
'accent': '#10b981',
'accent-dark': '#059669',
'accent-glow': 'rgba(16, 185, 129, 0.25)',
'accent-gradient': 'linear-gradient(135deg, #10b981, #0ea5e9)',
'header-bg': 'rgba(12, 15, 20, 0.85)',
},
},
{
slug: 'sunset',
label: 'Sunset',
light: {
'bg': '#fff8f2',
'bg-elevated': '#ffffff',
'sidebar-bg': '#ffffff',
'card-bg': '#fff4ef',
'input-bg': '#ffe3d8',
'hover-bg': '#ffd3bf',
'border': 'rgba(249, 115, 22, 0.25)',
'text': '#3d1b0b',
'text-muted': '#7a4a37',
'accent': '#f97316',
'accent-dark': '#c2410c',
'accent-glow': 'rgba(249, 115, 22, 0.25)',
'accent-gradient': 'linear-gradient(135deg, #f97316, #ec4899)',
'header-bg': 'rgba(255, 255, 255, 0.96)',
},
dark: {
'bg': '#0f0505',
'bg-elevated': 'rgba(15, 5, 5, 0.85)',
'sidebar-bg': '#0c0404',
'card-bg': 'rgba(19, 6, 6, 0.7)',
'input-bg': 'rgba(251, 113, 133, 0.08)',
'hover-bg': 'rgba(251, 113, 133, 0.14)',
'border': 'rgba(251, 113, 133, 0.35)',
'text': '#ffe7e0',
'text-muted': '#f9a6aa',
'accent': '#fb7185',
'accent-dark': '#be123c',
'accent-glow': 'rgba(251, 113, 133, 0.25)',
'accent-gradient': 'linear-gradient(135deg, #fb7185, #f97316)',
'header-bg': 'rgba(12, 8, 6, 0.85)',
},
},
{
slug: 'ocean',
label: 'Ocean',
light: {
'bg': '#f4fbff',
'bg-elevated': '#ffffff',
'sidebar-bg': '#ffffff',
'card-bg': '#f0f7ff',
'input-bg': '#dcefff',
'hover-bg': '#cae8ff',
'border': 'rgba(14, 165, 233, 0.25)',
'text': '#06274e',
'text-muted': '#4d6993',
'accent': '#0ea5e9',
'accent-dark': '#0369a1',
'accent-glow': 'rgba(14, 165, 233, 0.25)',
'accent-gradient': 'linear-gradient(135deg, #0ea5e9, #4753ff)',
'header-bg': 'rgba(255, 255, 255, 0.95)',
},
dark: {
'bg': '#030b12',
'bg-elevated': 'rgba(2, 9, 20, 0.85)',
'sidebar-bg': '#050c16',
'card-bg': 'rgba(3, 13, 26, 0.75)',
'input-bg': 'rgba(14, 165, 233, 0.08)',
'hover-bg': 'rgba(14, 165, 233, 0.15)',
'border': 'rgba(14, 165, 233, 0.4)',
'text': '#e6f6ff',
'text-muted': '#a1c4e8',
'accent': '#38bdf8',
'accent-dark': '#0369a1',
'accent-glow': 'rgba(14, 165, 233, 0.35)',
'accent-gradient': 'linear-gradient(135deg, #38bdf8, #0f172a)',
'header-bg': 'rgba(6, 15, 30, 0.85)',
},
},
{
slug: 'orchid',
label: 'Orchid',
light: {
'bg': '#fdf6ff',
'bg-elevated': '#ffffff',
'sidebar-bg': '#ffffff',
'card-bg': '#fdf2ff',
'input-bg': '#f5e4ff',
'hover-bg': '#e9d4ff',
'border': 'rgba(168, 85, 247, 0.25)',
'text': '#2c0a3a',
'text-muted': '#6a5277',
'accent': '#a855f7',
'accent-dark': '#6d28d9',
'accent-glow': 'rgba(168, 85, 247, 0.25)',
'accent-gradient': 'linear-gradient(135deg, #c084fc, #a855f7)',
'header-bg': 'rgba(255, 255, 255, 0.97)',
},
dark: {
'bg': '#0c0215',
'bg-elevated': 'rgba(10, 3, 30, 0.85)',
'sidebar-bg': '#090118',
'card-bg': 'rgba(12, 2, 25, 0.75)',
'input-bg': 'rgba(168, 85, 247, 0.08)',
'hover-bg': 'rgba(168, 85, 247, 0.16)',
'border': 'rgba(168, 85, 247, 0.35)',
'text': '#f5e6ff',
'text-muted': '#c5a3e8',
'accent': '#d946ef',
'accent-dark': '#831843',
'accent-glow': 'rgba(217, 70, 239, 0.25)',
'accent-gradient': 'linear-gradient(135deg, #d946ef, #fb7185)',
'header-bg': 'rgba(13, 6, 23, 0.95)',
},
},
{
slug: 'citrus',
label: 'Citrus',
light: {
'bg': '#fffdf5',
'bg-elevated': '#ffffff',
'sidebar-bg': '#ffffff',
'card-bg': '#fffaf0',
'input-bg': '#fff4d8',
'hover-bg': '#ffeec1',
'border': 'rgba(250, 204, 21, 0.25)',
'text': '#1f1505',
'text-muted': '#5b4a1e',
'accent': '#fbbf24',
'accent-dark': '#c2410c',
'accent-glow': 'rgba(250, 204, 21, 0.3)',
'accent-gradient': 'linear-gradient(135deg, #fbbf24, #d97706)',
'header-bg': 'rgba(255, 255, 255, 0.98)',
},
dark: {
'bg': '#1a1203',
'bg-elevated': 'rgba(26, 18, 3, 0.9)',
'sidebar-bg': '#130e02',
'card-bg': 'rgba(26, 18, 3, 0.75)',
'input-bg': 'rgba(250, 204, 21, 0.08)',
'hover-bg': 'rgba(250, 204, 21, 0.14)',
'border': 'rgba(250, 204, 21, 0.35)',
'text': '#fff8e7',
'text-muted': '#f6dea1',
'accent': '#fbbf24',
'accent-dark': '#b45309',
'accent-glow': 'rgba(250, 204, 21, 0.25)',
'accent-gradient': 'linear-gradient(135deg, #fbbf24, #f97316)',
'header-bg': 'rgba(15, 9, 2, 0.9)',
},
},
];

View File

@@ -0,0 +1,21 @@
import type { CalculatorDef } from '$lib/data/calculators';
import { formatConversionValue } from '$lib/utils/formatConversionValue';
type RateConfig = Pick<CalculatorDef, 'type' | 'factor' | 'offset' | 'labels'>;
export const getConversionRateText = (config: RateConfig): string | null => {
if (config.type !== 'standard' || !config.factor) {
return null;
}
const { in1, in2 } = config.labels;
if (!in1 || !in2) {
return null;
}
const formattedFactor = formatConversionValue(config.factor);
const hasOffset = Boolean(config.offset);
const formattedOffset = formatConversionValue(config.offset ?? 0);
return `1 ${in1} = ${formattedFactor}${hasOffset ? ` + ${formattedOffset}` : ''} ${in2}`;
};

View File

@@ -0,0 +1,24 @@
function formatConversionValue(value: number | null | undefined): string {
if (value === null || value === undefined || Number.isNaN(value)) {
return '—';
}
if (!Number.isFinite(value)) {
return value.toString();
}
if (value === 0) {
return '0';
}
const rounded = parseFloat(value.toFixed(6));
if (rounded !== 0) {
return rounded.toString();
}
const precise = value.toFixed(12).replace(/\.?0+$/, '');
if (precise !== '0' && precise !== '') {
return precise;
}
// Fallback for extremely small values: use scientific notation but clean it up
const scientific = value.toExponential();
return scientific.replace(/\.?0+e/, 'e');
}
module.exports = { formatConversionValue };

View File

@@ -0,0 +1,23 @@
export function formatConversionValue(value: number | null | undefined): string {
if (value === null || value === undefined || Number.isNaN(value)) {
return '—';
}
if (!Number.isFinite(value)) {
return value.toString();
}
if (value === 0) {
return '0';
}
const rounded = parseFloat(value.toFixed(6));
if (rounded !== 0) {
return rounded.toString();
}
const precise = value.toFixed(12).replace(/\.?0+$/, '');
if (precise !== '0' && precise !== '') {
return precise;
}
// Fallback for extremely small values: use scientific notation but clean it up
const scientific = value.toExponential();
return scientific.replace(/\.?0+e/, 'e');
}

View File

@@ -7,251 +7,8 @@
import '../app.css'; import '../app.css';
import Sidebar from '$lib/components/Sidebar.svelte'; import Sidebar from '$lib/components/Sidebar.svelte';
import SearchBar from '$lib/components/SearchBar.svelte'; import SearchBar from '$lib/components/SearchBar.svelte';
import { palettes, type ThemeMode, type Palette } from '$lib/palettes';
type ThemeMode = 'light' | 'dark';
type PaletteVar =
| 'bg'
| 'bg-elevated'
| 'sidebar-bg'
| 'card-bg'
| 'input-bg'
| 'hover-bg'
| 'border'
| 'text'
| 'text-muted'
| 'accent'
| 'accent-dark'
| 'accent-glow'
| 'accent-gradient'
| 'header-bg';
type PaletteTheme = Record<PaletteVar, string>;
type Palette = {
slug: string;
label: string;
light: PaletteTheme;
dark: PaletteTheme;
};
const palettes: Palette[] = [
{
slug: 'classic',
label: 'Classic',
light: {
bg: '#f8fafc',
'bg-elevated': '#ffffff',
'sidebar-bg': '#ffffff',
'card-bg': '#ffffff',
'input-bg': 'rgba(15, 23, 42, 0.04)',
'hover-bg': 'rgba(15, 23, 42, 0.08)',
border: 'rgba(15, 23, 42, 0.12)',
text: '#0f172a',
'text-muted': '#475569',
accent: '#10b981',
'accent-dark': '#059669',
'accent-glow': 'rgba(16, 185, 129, 0.15)',
'accent-gradient': 'linear-gradient(135deg, #10b981, #06b6d4)',
'header-bg': 'rgba(255, 255, 255, 0.95)',
},
dark: {
bg: '#0c0f14',
'bg-elevated': '#12161e',
'sidebar-bg': '#10141b',
'card-bg': 'rgba(18, 22, 30, 0.85)',
'input-bg': 'rgba(255, 255, 255, 0.04)',
'hover-bg': 'rgba(255, 255, 255, 0.06)',
border: 'rgba(255, 255, 255, 0.08)',
text: '#e8ecf4',
'text-muted': '#7b8498',
accent: '#10b981',
'accent-dark': '#059669',
'accent-glow': 'rgba(16, 185, 129, 0.15)',
'accent-gradient': 'linear-gradient(135deg, #10b981, #06b6d4)',
'header-bg': 'rgba(12, 15, 20, 0.85)',
},
},
{
slug: 'emerald',
label: 'Emerald',
light: {
'bg': '#f6fbf9',
'bg-elevated': '#ffffff',
'sidebar-bg': '#ffffff',
'card-bg': '#ffffff',
'input-bg': '#ecf7f1',
'hover-bg': '#d5f0df',
'border': 'rgba(4, 120, 87, 0.25)',
'text': '#0b2c1f',
'text-muted': '#4a6b5c',
'accent': '#047857',
'accent-dark': '#065f46',
'accent-glow': 'rgba(4, 120, 87, 0.2)',
'accent-gradient': 'linear-gradient(135deg, #047857, #0ea5e9)',
'header-bg': 'rgba(255, 255, 255, 0.95)',
},
dark: {
'bg': '#0b1313',
'bg-elevated': 'rgba(4, 20, 15, 0.85)',
'sidebar-bg': '#08110f',
'card-bg': 'rgba(6, 19, 13, 0.75)',
'input-bg': 'rgba(16, 185, 129, 0.08)',
'hover-bg': 'rgba(16, 185, 129, 0.12)',
'border': 'rgba(16, 185, 129, 0.35)',
'text': '#e9fcea',
'text-muted': '#9fdac4',
'accent': '#10b981',
'accent-dark': '#059669',
'accent-glow': 'rgba(16, 185, 129, 0.25)',
'accent-gradient': 'linear-gradient(135deg, #10b981, #0ea5e9)',
'header-bg': 'rgba(12, 15, 20, 0.85)',
},
},
{
slug: 'sunset',
label: 'Sunset',
light: {
'bg': '#fff8f2',
'bg-elevated': '#ffffff',
'sidebar-bg': '#ffffff',
'card-bg': '#fff4ef',
'input-bg': '#ffe3d8',
'hover-bg': '#ffd3bf',
'border': 'rgba(249, 115, 22, 0.25)',
'text': '#3d1b0b',
'text-muted': '#7a4a37',
'accent': '#f97316',
'accent-dark': '#c2410c',
'accent-glow': 'rgba(249, 115, 22, 0.25)',
'accent-gradient': 'linear-gradient(135deg, #f97316, #ec4899)',
'header-bg': 'rgba(255, 255, 255, 0.96)',
},
dark: {
'bg': '#0f0505',
'bg-elevated': 'rgba(15, 5, 5, 0.85)',
'sidebar-bg': '#0c0404',
'card-bg': 'rgba(19, 6, 6, 0.7)',
'input-bg': 'rgba(251, 113, 133, 0.08)',
'hover-bg': 'rgba(251, 113, 133, 0.14)',
'border': 'rgba(251, 113, 133, 0.35)',
'text': '#ffe7e0',
'text-muted': '#f9a6aa',
'accent': '#fb7185',
'accent-dark': '#be123c',
'accent-glow': 'rgba(251, 113, 133, 0.25)',
'accent-gradient': 'linear-gradient(135deg, #fb7185, #f97316)',
'header-bg': 'rgba(12, 8, 6, 0.85)',
},
},
{
slug: 'ocean',
label: 'Ocean',
light: {
'bg': '#f4fbff',
'bg-elevated': '#ffffff',
'sidebar-bg': '#ffffff',
'card-bg': '#f0f7ff',
'input-bg': '#dcefff',
'hover-bg': '#cae8ff',
'border': 'rgba(14, 165, 233, 0.25)',
'text': '#06274e',
'text-muted': '#4d6993',
'accent': '#0ea5e9',
'accent-dark': '#0369a1',
'accent-glow': 'rgba(14, 165, 233, 0.25)',
'accent-gradient': 'linear-gradient(135deg, #0ea5e9, #4753ff)',
'header-bg': 'rgba(255, 255, 255, 0.95)',
},
dark: {
'bg': '#030b12',
'bg-elevated': 'rgba(2, 9, 20, 0.85)',
'sidebar-bg': '#050c16',
'card-bg': 'rgba(3, 13, 26, 0.75)',
'input-bg': 'rgba(14, 165, 233, 0.08)',
'hover-bg': 'rgba(14, 165, 233, 0.15)',
'border': 'rgba(14, 165, 233, 0.4)',
'text': '#e6f6ff',
'text-muted': '#a1c4e8',
'accent': '#38bdf8',
'accent-dark': '#0369a1',
'accent-glow': 'rgba(14, 165, 233, 0.35)',
'accent-gradient': 'linear-gradient(135deg, #38bdf8, #0f172a)',
'header-bg': 'rgba(6, 15, 30, 0.85)',
},
},
{
slug: 'orchid',
label: 'Orchid',
light: {
'bg': '#fdf6ff',
'bg-elevated': '#ffffff',
'sidebar-bg': '#ffffff',
'card-bg': '#fdf2ff',
'input-bg': '#f5e4ff',
'hover-bg': '#e9d4ff',
'border': 'rgba(168, 85, 247, 0.25)',
'text': '#2c0a3a',
'text-muted': '#6a5277',
'accent': '#a855f7',
'accent-dark': '#6d28d9',
'accent-glow': 'rgba(168, 85, 247, 0.25)',
'accent-gradient': 'linear-gradient(135deg, #c084fc, #a855f7)',
'header-bg': 'rgba(255, 255, 255, 0.97)',
},
dark: {
'bg': '#0c0215',
'bg-elevated': 'rgba(10, 3, 30, 0.85)',
'sidebar-bg': '#090118',
'card-bg': 'rgba(12, 2, 25, 0.75)',
'input-bg': 'rgba(168, 85, 247, 0.08)',
'hover-bg': 'rgba(168, 85, 247, 0.16)',
'border': 'rgba(168, 85, 247, 0.35)',
'text': '#f5e6ff',
'text-muted': '#c5a3e8',
'accent': '#d946ef',
'accent-dark': '#831843',
'accent-glow': 'rgba(217, 70, 239, 0.25)',
'accent-gradient': 'linear-gradient(135deg, #d946ef, #fb7185)',
'header-bg': 'rgba(13, 6, 23, 0.95)',
},
},
{
slug: 'citrus',
label: 'Citrus',
light: {
'bg': '#fffdf5',
'bg-elevated': '#ffffff',
'sidebar-bg': '#ffffff',
'card-bg': '#fffaf0',
'input-bg': '#fff4d8',
'hover-bg': '#ffeec1',
'border': 'rgba(250, 204, 21, 0.25)',
'text': '#1f1505',
'text-muted': '#5b4a1e',
'accent': '#fbbf24',
'accent-dark': '#c2410c',
'accent-glow': 'rgba(250, 204, 21, 0.3)',
'accent-gradient': 'linear-gradient(135deg, #fbbf24, #d97706)',
'header-bg': 'rgba(255, 255, 255, 0.98)',
},
dark: {
'bg': '#1a1203',
'bg-elevated': 'rgba(26, 18, 3, 0.9)',
'sidebar-bg': '#130e02',
'card-bg': 'rgba(26, 18, 3, 0.75)',
'input-bg': 'rgba(250, 204, 21, 0.08)',
'hover-bg': 'rgba(250, 204, 21, 0.14)',
'border': 'rgba(250, 204, 21, 0.35)',
'text': '#fff8e7',
'text-muted': '#f6dea1',
'accent': '#fbbf24',
'accent-dark': '#b45309',
'accent-glow': 'rgba(250, 204, 21, 0.25)',
'accent-gradient': 'linear-gradient(135deg, #fbbf24, #f97316)',
'header-bg': 'rgba(15, 9, 2, 0.9)',
},
},
];
const matomoContainerSrc = 'https://matomo.howdoyouconvert.com/js/container_B3r877Kn.js'; const matomoContainerSrc = 'https://matomo.howdoyouconvert.com/js/container_B3r877Kn.js';
type WindowWithAnalytics = Window & { type WindowWithAnalytics = Window & {
@@ -265,6 +22,7 @@
let isMobileHeader = false; let isMobileHeader = false;
let theme: ThemeMode = 'dark'; let theme: ThemeMode = 'dark';
let selectedPaletteIndex = 0; let selectedPaletteIndex = 0;
let savedScrollRestoration: ScrollRestoration | null = null;
$: isHomepage = $page.url.pathname === '/'; $: isHomepage = $page.url.pathname === '/';
$: if (isHomepage && (sidebarOpen || headerSearchOpen)) { $: if (isHomepage && (sidebarOpen || headerSearchOpen)) {
sidebarOpen = false; sidebarOpen = false;
@@ -326,14 +84,30 @@
document.head.appendChild(script); document.head.appendChild(script);
}; };
afterNavigate(() => { const scrollToTop = () => {
if (!browser) return;
window.scrollTo({ top: 0, behavior: 'auto' });
};
afterNavigate(({ from, to, type }) => {
sidebarOpen = false; sidebarOpen = false;
headerSearchOpen = false; headerSearchOpen = false;
if (!browser) return;
if (type === 'popstate') return;
if (!from || !to) return;
if (from.url.pathname === to.url.pathname) return;
scrollToTop();
}); });
onMount(() => { onMount(() => {
if (!browser) return; if (!browser) return;
const appWindow = window as WindowWithAnalytics; const appWindow = window as WindowWithAnalytics;
if ('scrollRestoration' in window.history) {
savedScrollRestoration = window.history.scrollRestoration;
window.history.scrollRestoration = 'manual';
}
let idleCallbackId: number | null = null; let idleCallbackId: number | null = null;
let fallbackTimeoutId: number | null = null; let fallbackTimeoutId: number | null = null;
@@ -411,6 +185,9 @@
window.clearTimeout(fallbackTimeoutId); window.clearTimeout(fallbackTimeoutId);
} }
window.removeEventListener('keydown', handleEscape); window.removeEventListener('keydown', handleEscape);
if (savedScrollRestoration !== null) {
window.history.scrollRestoration = savedScrollRestoration;
}
}; };
if ('addEventListener' in mediaQuery) { if ('addEventListener' in mediaQuery) {
@@ -461,7 +238,7 @@
</button> </button>
{/if} {/if}
<a href="/" class="site-logo"> <a href="/" class="site-logo">
<span>How Do You</span><span class="logo-accent">Convert</span><span class="logo-domain">.com</span> <span>How</span><span>Do</span><span>You</span><span class="logo-accent">Convert</span><span class="logo-domain">.com</span>
</a> </a>
</div> </div>
<div class="header-right"> <div class="header-right">

View File

@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { categories, calculators } from '$lib/data/calculators'; import { categories, totalCalculators } from '$lib/data/stats';
import CategoryCard from '$lib/components/CategoryCard.svelte'; import CategoryCard from '$lib/components/CategoryCard.svelte';
import SearchBar from '$lib/components/SearchBar.svelte'; import SearchBar from '$lib/components/SearchBar.svelte';
import { buildSeoMeta, SITE_NAME, SITE_URL, toJsonLd } from '$lib/seo'; import { buildSeoMeta, SITE_NAME, SITE_URL, toJsonLd } from '$lib/seo';
@@ -18,8 +18,8 @@
key, key,
...meta, ...meta,
})); }));
const totalCalculators = calculators.length; const totalConversions = totalCalculators;
const totalCategories = Object.keys(homepageCategories).length; const totalCategoriesCount = Object.keys(homepageCategories).length;
const pageTitle = `${SITE_NAME} — Free Unit Conversion Calculators`; const pageTitle = `${SITE_NAME} — Free Unit Conversion Calculators`;
const pageDescription = 'Convert between hundreds of units instantly. Free online calculators for length, weight, temperature, volume, area, speed, energy, power, data and more.'; const pageDescription = 'Convert between hundreds of units instantly. Free online calculators for length, weight, temperature, volume, area, speed, energy, power, data and more.';
const seo = buildSeoMeta({ const seo = buildSeoMeta({
@@ -49,7 +49,7 @@
<meta name="twitter:card" content={seo.twitter.card} /> <meta name="twitter:card" content={seo.twitter.card} />
<meta name="twitter:title" content={seo.twitter.title} /> <meta name="twitter:title" content={seo.twitter.title} />
<meta name="twitter:description" content={seo.twitter.description} /> <meta name="twitter:description" content={seo.twitter.description} />
<script type="application/ld+json">{websiteJsonLd}</script> {@html `<script type="application/ld+json">${websiteJsonLd}</script>`}
</svelte:head> </svelte:head>
<section class="hero"> <section class="hero">
@@ -62,11 +62,11 @@
<div class="stats-row"> <div class="stats-row">
<div class="stat"> <div class="stat">
<div class="stat-num">{totalCalculators}</div> <div class="stat-num">{totalConversions}</div>
<div class="stat-label">Converters</div> <div class="stat-label">Converters</div>
</div> </div>
<div class="stat"> <div class="stat">
<div class="stat-num">{totalCategories}</div> <div class="stat-num">{totalCategoriesCount}</div>
<div class="stat-label">Categories</div> <div class="stat-label">Categories</div>
</div> </div>
</div> </div>

View File

@@ -0,0 +1,3 @@
// Prerender the homepage as static HTML at build time.
// adapter-node will serve this as a static file — no SSR round-trip.
export const prerender = true;

View File

@@ -1,8 +1,5 @@
<script lang="ts"> <script lang="ts">
import Calculator from '$lib/components/Calculator.svelte'; import Calculator from '$lib/components/Calculator.svelte';
import { afterNavigate } from '$app/navigation';
import { browser } from '$app/environment';
import { onMount } from 'svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { buildSeoMeta, canonicalUrl, SITE_NAME, SITE_URL, toJsonLd } from '$lib/seo'; import { buildSeoMeta, canonicalUrl, SITE_NAME, SITE_URL, toJsonLd } from '$lib/seo';
@@ -56,15 +53,6 @@
}, },
}); });
afterNavigate(() => {
if (!browser) return;
window.scrollTo({ top: 0 });
});
onMount(() => {
if (!browser) return;
window.scrollTo({ top: 0 });
});
</script> </script>
<svelte:head> <svelte:head>
@@ -80,8 +68,8 @@
<meta name="twitter:card" content={seo.twitter.card} /> <meta name="twitter:card" content={seo.twitter.card} />
<meta name="twitter:title" content={seo.twitter.title} /> <meta name="twitter:title" content={seo.twitter.title} />
<meta name="twitter:description" content={seo.twitter.description} /> <meta name="twitter:description" content={seo.twitter.description} />
<script type="application/ld+json">{breadcrumbJsonLd}</script> {@html `<script type="application/ld+json">${breadcrumbJsonLd}</script>`}
<script type="application/ld+json">{webPageJsonLd}</script> {@html `<script type="application/ld+json">${webPageJsonLd}</script>`}
</svelte:head> </svelte:head>
<nav class="breadcrumbs" aria-label="Breadcrumb"> <nav class="breadcrumbs" aria-label="Breadcrumb">
@@ -94,7 +82,9 @@
<h1 class="page-title calculator-page-title">{calc.name}</h1> <h1 class="page-title calculator-page-title">{calc.name}</h1>
<Calculator config={calc} showTitle={false} /> {#key calc.slug}
<Calculator config={calc} showTitle={false} />
{/key}
<div class="seo-content"> <div class="seo-content">
{#if calc.descriptionHTML} {#if calc.descriptionHTML}

View File

@@ -1,6 +1,28 @@
<script lang="ts"> <script lang="ts">
import type { PageData } from './$types'; import type { PageData } from './$types';
import { buildSeoMeta, canonicalUrl, SITE_NAME, SITE_URL, toJsonLd } from '$lib/seo'; import { buildSeoMeta, canonicalUrl, SITE_NAME, SITE_URL, toJsonLd } from '$lib/seo';
import { getConversionRateText } from '$lib/utils/conversionRate';
const handleCalcTooltipMousemove = (event: MouseEvent) => {
const card = event.currentTarget as HTMLElement | null;
if (!card) return;
const rect = card.getBoundingClientRect();
const x = Math.min(Math.max(event.clientX - rect.left, 0), rect.width);
const y = Math.min(Math.max(event.clientY - rect.top, 0), rect.height);
card.style.setProperty('--calc-tooltip-left', `${x}px`);
card.style.setProperty('--calc-tooltip-top', `${y}px`);
card.style.setProperty('--calc-tooltip-bottom', 'auto');
card.style.setProperty('--calc-tooltip-translate', 'calc(-100% - 0.55rem)');
};
const resetCalcTooltipPosition = (event: MouseEvent) => {
const card = event.currentTarget as HTMLElement | null;
if (!card) return;
card.style.removeProperty('--calc-tooltip-left');
card.style.removeProperty('--calc-tooltip-top');
card.style.removeProperty('--calc-tooltip-bottom');
card.style.removeProperty('--calc-tooltip-translate');
};
export let data: PageData; export let data: PageData;
@@ -66,8 +88,8 @@
<meta name="twitter:card" content={seo.twitter.card} /> <meta name="twitter:card" content={seo.twitter.card} />
<meta name="twitter:title" content={seo.twitter.title} /> <meta name="twitter:title" content={seo.twitter.title} />
<meta name="twitter:description" content={seo.twitter.description} /> <meta name="twitter:description" content={seo.twitter.description} />
<script type="application/ld+json">{breadcrumbJsonLd}</script> {@html `<script type="application/ld+json">${breadcrumbJsonLd}</script>`}
<script type="application/ld+json">{collectionJsonLd}</script> {@html `<script type="application/ld+json">${collectionJsonLd}</script>`}
</svelte:head> </svelte:head>
<nav class="breadcrumbs" aria-label="Breadcrumb"> <nav class="breadcrumbs" aria-label="Breadcrumb">
@@ -85,8 +107,18 @@
<div class="calc-list"> <div class="calc-list">
{#each data.calculators as calc} {#each data.calculators as calc}
<a href="/{calc.slug}" class="calc-list-item"> {@const conversionRateText = getConversionRateText(calc)}
<a
href="/{calc.slug}"
class="calc-list-item"
on:mousemove={handleCalcTooltipMousemove}
on:mouseleave={resetCalcTooltipPosition}
on:focus={resetCalcTooltipPosition}
>
{calc.name} {calc.name}
{#if conversionRateText}
<span class="calc-list-tooltip" role="tooltip">{conversionRateText}</span>
{/if}
</a> </a>
{/each} {/each}
</div> </div>

View File

@@ -3,8 +3,7 @@ import { calculators, categories } from '$lib/data/calculators';
export const GET: RequestHandler = async () => { export const GET: RequestHandler = async () => {
const calculatorUrls = calculators.map( const calculatorUrls = calculators.map(
(calc) => ` (calc) => ` <url>
<url>
<loc>https://howdoyouconvert.com/${calc.slug}</loc> <loc>https://howdoyouconvert.com/${calc.slug}</loc>
<changefreq>monthly</changefreq> <changefreq>monthly</changefreq>
<priority>0.8</priority> <priority>0.8</priority>
@@ -12,8 +11,7 @@ export const GET: RequestHandler = async () => {
); );
const categoryUrls = Object.keys(categories).map( const categoryUrls = Object.keys(categories).map(
(category) => ` (category) => ` <url>
<url>
<loc>https://howdoyouconvert.com/category/${category}</loc> <loc>https://howdoyouconvert.com/category/${category}</loc>
<changefreq>weekly</changefreq> <changefreq>weekly</changefreq>
<priority>0.9</priority> <priority>0.9</priority>
@@ -27,8 +25,8 @@ export const GET: RequestHandler = async () => {
<changefreq>weekly</changefreq> <changefreq>weekly</changefreq>
<priority>1.0</priority> <priority>1.0</priority>
</url> </url>
${categoryUrls.join('')} ${categoryUrls.join('\n')}
${calculatorUrls.join('')} ${calculatorUrls.join('\n')}
</urlset>`; </urlset>`;
return new Response(sitemap, { return new Response(sitemap, {

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,13 @@
import json import json
import re import re
import os
from pathlib import Path from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent BASE_DIR = Path(__file__).resolve().parent
CALCLIST = BASE_DIR / 'calculators_list.md' CALCLIST = BASE_DIR / 'calculators_list.md'
OUTPUT_FILE = BASE_DIR / 'hdyc-svelte/src/lib/data/calculators.ts' OUTPUT_FILE = BASE_DIR / 'hdyc-svelte/src/lib/data/calculators.ts'
STATS_FILE = BASE_DIR / 'hdyc-svelte/src/lib/data/stats.ts'
CALCULATORS_JSON = BASE_DIR / 'hdyc-svelte/static/data/calculators.json'
CATEGORY_KEYS = [ CATEGORY_KEYS = [
'length', 'length',
@@ -29,6 +32,29 @@ CATEGORY_KEYS = [
'other', 'other',
] ]
CATEGORIES = {
'length': {'label': 'Length / Distance', 'icon': '📏'},
'weight': {'label': 'Weight / Mass', 'icon': '⚖️'},
'temperature': {'label': 'Temperature', 'icon': '🌡️'},
'volume': {'label': 'Volume', 'icon': '🧪'},
'fluids': {'label': 'Fluids', 'icon': '💧'},
'area': {'label': 'Area', 'icon': '🔳'},
'speed': {'label': 'Speed / Velocity', 'icon': '💨'},
'pressure': {'label': 'Pressure', 'icon': '🔽'},
'energy': {'label': 'Energy', 'icon': ''},
'magnetism': {'label': 'Magnetism', 'icon': '🧲'},
'power': {'label': 'Power', 'icon': '🔌'},
'data': {'label': 'Data Storage', 'icon': '💾'},
'time': {'label': 'Time', 'icon': '⏱️'},
'angle': {'label': 'Angle', 'icon': '📐'},
'number-systems': {'label': 'Number Systems', 'icon': '🔢'},
'radiation': {'label': 'Radiation', 'icon': '☢️'},
'electrical': {'label': 'Electrical', 'icon': '🔋'},
'force': {'label': 'Force / Torque', 'icon': '💪'},
'light': {'label': 'Light', 'icon': '💡'},
'other': {'label': 'Other', 'icon': '🔄'},
}
CATEGORY_SET = set(CATEGORY_KEYS) CATEGORY_SET = set(CATEGORY_KEYS)
# Lightweight label normalization to catch duplicate/identity conversions # Lightweight label normalization to catch duplicate/identity conversions
@@ -418,28 +444,12 @@ export interface CalculatorDef {
} }
export const categories: Record<string, { label: string; icon: string }> = { export const categories: Record<string, { label: string; icon: string }> = {
length: { label: 'Length / Distance', icon: '📏' }, """
weight: { label: 'Weight / Mass', icon: '⚖️' }, for k, v in CATEGORIES.items():
temperature: { label: 'Temperature', icon: '🌡️' }, out += f" '{k}': {json.dumps(v, ensure_ascii=False).replace('{', '{ ').replace('}', ' }')},\n"
volume: { label: 'Volume', icon: '🧪' }, out += "};\n"
fluids: { label: 'Fluids', icon: '💧' },
area: { label: 'Area', icon: '🔳' },
speed: { label: 'Speed / Velocity', icon: '💨' },
pressure: { label: 'Pressure', icon: '🔽' },
energy: { label: 'Energy', icon: '' },
magnetism: { label: 'Magnetism', icon: '🧲' },
power: { label: 'Power', icon: '🔌' },
data: { label: 'Data Storage', icon: '💾' },
time: { label: 'Time', icon: '⏱️' },
angle: { label: 'Angle', icon: '📐' },
'number-systems':{ label: 'Number Systems', icon: '🔢' },
radiation: { label: 'Radiation', icon: '☢️' },
electrical: { label: 'Electrical', icon: '🔋' },
force: { label: 'Force / Torque', icon: '💪' },
light: { label: 'Light', icon: '💡' },
other: { label: 'Other', icon: '🔄' },
};
out += """
export const calculators: CalculatorDef[] = [ export const calculators: CalculatorDef[] = [
""" """
for e in calculators_ts_entries: for e in calculators_ts_entries:
@@ -454,8 +464,13 @@ export const calculators: CalculatorDef[] = [
out += """ out += """
]; ];
const slugIndex = new Map(calculators.map(c => [c.slug, c])); const slugIndex: Map<string, CalculatorDef> = new Map(
calculators.map(calc => [calc.slug, calc])
);
"""
out += """
export function getCalculatorBySlug(slug: string): CalculatorDef | undefined { export function getCalculatorBySlug(slug: string): CalculatorDef | undefined {
return slugIndex.get(slug); return slugIndex.get(slug);
} }
@@ -487,5 +502,22 @@ export function searchCalculators(query: string): CalculatorDef[] {
print(f"Generated {len(calculators_ts_entries)} calculators into calculators.ts") print(f"Generated {len(calculators_ts_entries)} calculators into calculators.ts")
# Generate stats.ts
total_count = len(calculators_ts_entries)
stats_content = f"""// THIS FILE IS AUTO-GENERATED BY migrate.py
export const categories: Record<string, {{ label: string; icon: string }}> = {json.dumps(CATEGORIES, indent=2, ensure_ascii=False)};
export const totalCalculators = {total_count};
"""
with open(STATS_FILE, 'w', encoding='utf-8') as f:
f.write(stats_content)
print(f"Generated stats.ts with {total_count} total calculators")
# Generate calculators.json for true lazy loading
os.makedirs(os.path.dirname(CALCULATORS_JSON), exist_ok=True)
with open(CALCULATORS_JSON, 'w', encoding='utf-8') as f:
json.dump(calculators_ts_entries, f, ensure_ascii=False, indent=2)
print(f"Generated calculators.json (Size: {os.path.getsize(CALCULATORS_JSON) // 1024}KB)")
if __name__ == '__main__': if __name__ == '__main__':
process() process()

View File

@@ -0,0 +1,27 @@
import urllib.error
import urllib.request
import unittest
URL = 'https://howdoyouconvert.com/apothecary-ounces-to-amu'
class ApothecaryPageTests(unittest.TestCase):
def test_apothecary_page_returns_success(self) -> None:
"""The published URL should return a 200 so the calculator page stays healthy."""
request = urllib.request.Request(
URL,
headers={
'User-Agent': 'Mozilla/5.0',
'Accept': 'text/html,application/xhtml+xml',
},
)
try:
with urllib.request.urlopen(request, timeout=15) as response:
status = response.getcode()
except urllib.error.HTTPError as exc:
self.fail(f'{URL} returned HTTP {exc.code} ({exc.reason})')
except urllib.error.URLError as exc:
self.fail(f'{URL} could not be fetched: {exc}')
self.assertEqual(status, 200, f'{URL} returned {status}')

106
tests/test_consistency.py Normal file
View File

@@ -0,0 +1,106 @@
import math
import re
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CALCULATORS_TS = ROOT / "hdyc-svelte" / "src" / "lib" / "data" / "calculators.ts"
def _js_fmt(n: float) -> str:
"""Mimics the fmt() function in engine.ts"""
if not math.isfinite(n):
return str(n)
if n == 0:
return "0"
if abs(n) < 1e-6:
return f"{n:.6e}".replace("e-0", "e-").replace("e+0", "e+")
# engine.ts uses parseFloat(n.toFixed(6)).toString()
rounded = round(n, 6)
if rounded == 0: # Handle -0.0
return "0"
if rounded == int(rounded):
return str(int(rounded))
return str(rounded)
def _js_fmt_precise(n: float) -> str:
"""Mimics formatExampleValue in QuickConversionExample.svelte"""
if n is None or math.isnan(n):
return ""
if not math.isfinite(n):
return str(n)
if n == 0:
return "0"
rounded = round(n, 6)
if rounded != 0:
if rounded == int(rounded):
return str(int(rounded))
return str(rounded)
# Precise version for very small numbers
precise = f"{n:.12f}".rstrip('0').rstrip('.')
return precise if precise else "0"
class TestCalculatorsConsistency(unittest.TestCase):
def test_standard_calculators_consistency(self):
text = CALCULATORS_TS.read_text(encoding="utf-8")
# Extract the calculators array content
match = re.search(r"export const calculators: CalculatorDef\[\] = \[(.*?)\];", text, re.S)
self.assertTrue(match, "Could not find calculators array in calculators.ts")
body = match.group(1)
# Split by '{"slug":' to avoid splitting on nested braces
raw_entries = body.split('{"slug":')
errors = []
for raw_entry in raw_entries:
if not raw_entry.strip():
continue
entry = '{"slug":' + raw_entry
slug_match = re.search(r'"slug": "(.*?)"', entry)
if not slug_match:
continue
slug = slug_match.group(1)
type_match = re.search(r'"type": "(.*?)"', entry)
if not type_match or type_match.group(1) != "standard":
continue
# Use non-greedy search for factor/offset and handle potential whitespace
factor_match = re.search(r'"factor":\s*([0-9.eE+-]+)', entry)
offset_match = re.search(r'"offset":\s*([0-9.eE+-]+)', entry)
factor = float(factor_match.group(1)) if factor_match else 1.0
offset = float(offset_match.group(1)) if offset_match else 0.0
# 1. Formula Hint vs Chart Row 1 Consistency
# Logic: solve(config, 1, "1") -> val2 = fmt(1 * factor + offset)
row_one_output = _js_fmt(1.0 * factor + offset)
# 2. How to convert Examples Consistency (Inverse)
if factor != 0:
reverse_val = (1.0 - offset) / factor
formatted_reverse = _js_fmt_precise(reverse_val)
# Specific check for Réaumur to Kelvin (User request)
if slug == "reaumur-to-kelvin":
if formatted_reverse != "-217.72":
errors.append(f"[{slug}] Reverse example mismatch: expected -217.72, got {formatted_reverse}")
if row_one_output != "274.4":
errors.append(f"[{slug}] Chart row 1 mismatch: expected 274.4, got {row_one_output}")
# Specific check for Feet per minute to Knots (Previous bug fix)
if slug == "feet-per-minute-to-knots":
if row_one_output != "0.009875":
errors.append(f"[{slug}] Chart row 1 mismatch: expected 0.009875, got {row_one_output}")
if formatted_reverse != "101.268504":
errors.append(f"[{slug}] Reverse example mismatch: expected 101.268504, got {formatted_reverse}")
if errors:
self.fail("\n" + "\n".join(errors))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,27 @@
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CONVERSION_RATE = (
ROOT / "hdyc-svelte" / "src" / "lib" / "utils" / "conversionRate.ts"
)
class ConversionRateTooltipFormattingTests(unittest.TestCase):
def test_conversion_rate_text_uses_formatter(self) -> None:
text = CONVERSION_RATE.read_text(encoding="utf-8")
normalized = " ".join(text.split())
self.assertIn(
"formatConversionValue(config.factor)",
normalized,
"Conversion rate helper must format the factor before inserting it into the tooltip text",
)
self.assertIn(
"formatConversionValue(config.offset ?? 0)",
normalized,
"Conversion rate tooltip must format any offset before rendering",
)
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,68 @@
import re
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
CALCULATORS_TS = ROOT / "hdyc-svelte" / "src" / "lib" / "data" / "calculators.ts"
CONVERSION_RATE_UTIL = ROOT / "hdyc-svelte" / "src" / "lib" / "utils" / "conversionRate.ts"
CATEGORY_PAGE = ROOT / "hdyc-svelte" / "src" / "routes" / "category" / "[category]" / "+page.svelte"
CALCULATOR_COMPONENT = ROOT / "hdyc-svelte" / "src" / "lib" / "components" / "Calculator.svelte"
TARGET_SLUG = "gauss-to-oersted"
NON_APPLICABLE_TWO_INPUT_STANDARD_SLUGS = {
"grams-per-liter-to-molarity",
}
def _extract_calculator_block(slug: str) -> str:
for line in CALCULATORS_TS.read_text(encoding="utf-8").splitlines():
if f'"slug": "{slug}"' in line:
return line
raise AssertionError(f"Could not find calculator definition for '{slug}'")
class GaussToOerstedConversionRateRegressionTests(unittest.TestCase):
def test_gauss_to_oersted_includes_factor_for_conversion_rate_and_tooltip(self) -> None:
block = _extract_calculator_block(TARGET_SLUG)
self.assertRegex(
block,
r'"factor":\s*[0-9.eE+-]+',
"Missing factor on gauss-to-oersted prevents conversion rate text from rendering in both calculator footer and category tooltip.",
)
def test_conversion_rate_is_wired_to_both_surfaces(self) -> None:
util_text = CONVERSION_RATE_UTIL.read_text(encoding="utf-8")
category_page_text = CATEGORY_PAGE.read_text(encoding="utf-8")
calculator_component_text = CALCULATOR_COMPONENT.read_text(encoding="utf-8")
self.assertIn("getConversionRateText", util_text)
self.assertIn("conversionRateText = getConversionRateText(calc)", category_page_text)
self.assertIn('role="tooltip"', category_page_text)
self.assertIn("conversionRateText = getConversionRateText(config)", calculator_component_text)
self.assertIn('<span class="formula-hint">', calculator_component_text)
def test_all_applicable_two_input_standard_calculators_have_factors(self) -> None:
missing_factors: list[str] = []
for line in CALCULATORS_TS.read_text(encoding="utf-8").splitlines():
if '"type": "standard"' not in line:
continue
if '"labels": {"in1":' not in line or '"in2":' not in line or '"in3":' in line:
continue
slug_match = re.search(r'"slug": "([^"]+)"', line)
if not slug_match:
continue
slug = slug_match.group(1)
if slug in NON_APPLICABLE_TWO_INPUT_STANDARD_SLUGS:
continue
if '"factor":' not in line:
missing_factors.append(slug)
self.assertEqual(
missing_factors,
[],
f"Two-input standard calculators missing factors (and therefore conversion-rate text): {missing_factors}",
)
if __name__ == "__main__":
unittest.main()

View File

@@ -72,7 +72,7 @@ class HomepageCategoryRegressionTests(unittest.TestCase):
def test_homepage_uses_canonical_categories_map(self) -> None: def test_homepage_uses_canonical_categories_map(self) -> None:
text = HOMEPAGE_SVELTE.read_text(encoding="utf-8") text = HOMEPAGE_SVELTE.read_text(encoding="utf-8")
self.assertIn("import { categories, calculators } from '$lib/data/calculators';", text) self.assertIn("import { categories, totalCalculators } from '$lib/data/stats';", text)
self.assertIn("requiredCategoryFallbacks", text) self.assertIn("requiredCategoryFallbacks", text)
self.assertIn("fluids: { label: 'Fluids', icon: '💧' }", text) self.assertIn("fluids: { label: 'Fluids', icon: '💧' }", text)
self.assertIn("magnetism: { label: 'Magnetism', icon: '🧲' }", text) self.assertIn("magnetism: { label: 'Magnetism', icon: '🧲' }", text)

View File

@@ -0,0 +1,26 @@
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
QUICK_CONVERSION_EXAMPLE = (
ROOT / "hdyc-svelte" / "src" / "lib" / "components" / "QuickConversionExample.svelte"
)
class QuickConversionExampleFormattingTests(unittest.TestCase):
def test_formula_uses_formatted_factor_and_offset(self) -> None:
text = QUICK_CONVERSION_EXAMPLE.read_text(encoding="utf-8")
normalized = " ".join(text.split())
snippet = (
"1 {config.labels.in1} = {formattedFactorValue}{hasOffset ? ` + ${formattedOffsetValue}` : ''} "
"{config.labels.in2}"
)
self.assertIn(
snippet,
normalized,
"The formula snippet should render formatted factor/offset values instead of raw floats",
)
if __name__ == "__main__":
unittest.main()