Compare commits
34 Commits
f315ff1dc1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 63bafc2feb | |||
| d23e910aa7 | |||
| ae95a66668 | |||
| 0d099e34cd | |||
|
|
b44e9e5702 | ||
|
|
c20f2ebc60 | ||
|
|
56873816bb | ||
|
|
07a299275b | ||
|
|
17ed319fe7 | ||
|
|
de799c3a7b | ||
|
|
b1bf3f40d8 | ||
|
|
e4987d6764 | ||
|
|
6af3b23987 | ||
|
|
66d02c7f14 | ||
|
|
5651ecb6d4 | ||
|
|
3e26376584 | ||
| c006971cbe | |||
| de4fa5ba85 | |||
| 379bb60722 | |||
| 3ae77e02a0 | |||
| 1093208324 | |||
| 0114e00618 | |||
| 2794835590 | |||
| 193affca27 | |||
|
|
4c989ef1b3 | ||
|
|
a1758a9074 | ||
|
|
afe72b9ee1 | ||
|
|
cf1114e8d8 | ||
|
|
02b9c2411f | ||
|
|
2dca3654a8 | ||
|
|
51a26ad120 | ||
|
|
6a0347fd22 | ||
|
|
edb08e3e5c | ||
|
|
700953194a |
15730
hdyc-svelte/sitemap.xml
Normal file
15730
hdyc-svelte/sitemap.xml
Normal file
File diff suppressed because it is too large
Load Diff
116
hdyc-svelte/sitemap.xsd
Normal file
116
hdyc-svelte/sitemap.xsd
Normal 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>
|
||||
@@ -10,7 +10,7 @@
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
font-display: optional;
|
||||
src: url('/fonts/inter/Inter-Medium.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
font-display: optional;
|
||||
src: url('/fonts/inter/Inter-SemiBold.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
font-display: swap;
|
||||
font-display: optional;
|
||||
src: url('/fonts/inter/Inter-Bold.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
font-display: swap;
|
||||
font-display: optional;
|
||||
src: url('/fonts/jetbrains-mono/JetBrainsMono-Regular.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
font-display: swap;
|
||||
font-display: optional;
|
||||
src: url('/fonts/jetbrains-mono/JetBrainsMono-Medium.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
font-family: 'JetBrains Mono';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-display: swap;
|
||||
font-display: optional;
|
||||
src: url('/fonts/jetbrains-mono/JetBrainsMono-SemiBold.woff2') format('woff2');
|
||||
}
|
||||
|
||||
@@ -343,9 +343,14 @@ a:focus-visible {
|
||||
justify-content: space-between;
|
||||
padding: 0 1.5rem;
|
||||
background: var(--header-bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@media (min-width: 1025px) {
|
||||
.site-header {
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
}
|
||||
|
||||
.header-left {
|
||||
@@ -357,7 +362,7 @@ a:focus-visible {
|
||||
.site-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
gap: 0.3rem;
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
font-weight: 800;
|
||||
@@ -664,11 +669,17 @@ a:focus-visible {
|
||||
|
||||
.calc-list {
|
||||
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;
|
||||
}
|
||||
.calc-list-item {
|
||||
display: block;
|
||||
position: relative;
|
||||
--calc-tooltip-left: 50%;
|
||||
--calc-tooltip-translate: -0.35rem;
|
||||
padding: 1rem 1.25rem;
|
||||
background: var(--card-bg);
|
||||
border: 1px solid var(--border);
|
||||
@@ -684,11 +695,40 @@ a:focus-visible {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
color: var(--accent);
|
||||
z-index: 20;
|
||||
}
|
||||
.calc-list-item:focus-visible {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
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 ─────────────────────────────────── */
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
<html lang="en" data-theme="dark" data-palette="classic">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
|
||||
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/inter/Inter-Regular.woff2"
|
||||
@@ -10,27 +12,6 @@
|
||||
type="font/woff2"
|
||||
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
|
||||
rel="preload"
|
||||
href="/fonts/inter/Inter-ExtraBold.woff2"
|
||||
@@ -38,27 +19,25 @@
|
||||
type="font/woff2"
|
||||
crossorigin
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/jetbrains-mono/JetBrainsMono-Regular.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/jetbrains-mono/JetBrainsMono-Medium.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/jetbrains-mono/JetBrainsMono-SemiBold.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin
|
||||
/>
|
||||
<style>
|
||||
/* Critical CSS – inlined to eliminate render-blocking stylesheet for FCP/LCP */
|
||||
: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}
|
||||
: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)}
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
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}
|
||||
.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)}
|
||||
.header-left{display:flex;align-items:center;gap:.75rem}
|
||||
.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}
|
||||
.site-logo .logo-accent{color:var(--accent)}.site-logo .logo-domain{color:var(--text-muted);font-weight:500}
|
||||
.hero{text-align:center;padding:3rem 1rem 2rem;margin-bottom:1rem}
|
||||
.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}
|
||||
.stats-row{display:flex;justify-content:center;gap:2.5rem;margin-bottom:2.5rem;padding:1rem 0}
|
||||
.stat-num{font-size:1.8rem;font-weight:800;color:var(--accent)}
|
||||
.stat-label{font-size:.78rem;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em}
|
||||
.main-content{flex:1;width:100%;max-width:900px;margin:0 auto;padding:2rem 1.25rem 3rem}
|
||||
</style>
|
||||
<script>
|
||||
(function () {
|
||||
const doc = document.documentElement;
|
||||
@@ -77,8 +56,9 @@
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
%sveltekit.head%
|
||||
<!-- SvelteKit head tags moved to top of <head> -->
|
||||
</head>
|
||||
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
|
||||
@@ -13,7 +13,7 @@ const MIME_TYPES: Record<string, string> = {
|
||||
'.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 ASSET_404_CACHE_CONTROL = 'no-store';
|
||||
const LONG_CACHE_EXTENSIONS = new Set([
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
import { solve } from '$lib/engine';
|
||||
import type { CalculatorDef } from '$lib/data/calculators';
|
||||
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 QuickConversionExample from '$lib/components/QuickConversionExample.svelte';
|
||||
import QuickConversionTable from '$lib/components/QuickConversionTable.svelte';
|
||||
@@ -14,17 +16,33 @@
|
||||
let val2 = '';
|
||||
let val3 = '';
|
||||
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;
|
||||
$: isTextInput = ['base', 'text-bin', 'bin-text', 'dec-frac', 'dms-dd', 'dd-dms'].includes(config.type);
|
||||
$: conversionRateText = getConversionRateText(config);
|
||||
|
||||
// Clear inputs on config (route) change
|
||||
$: if (config) {
|
||||
if (!paramsInitializing) clear();
|
||||
// Clear inputs only when navigating to a different calculator slug.
|
||||
$: if (config?.slug) {
|
||||
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 }) {
|
||||
if (!options?.preserveSwap) {
|
||||
@@ -64,12 +82,147 @@
|
||||
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(() => {
|
||||
const params = new URLSearchParams($page.url.search);
|
||||
if (params.has('v1')) { val1 = params.get('v1')!; handleInput(1); }
|
||||
else if (params.has('v2')) { val2 = params.get('v2')!; handleInput(2); }
|
||||
else if (params.has('v3') && has3) { val3 = params.get('v3')!; handleInput(3); }
|
||||
setTimeout(() => { paramsInitializing = false; }, 0);
|
||||
const hasV1 = params.has('v1');
|
||||
const hasV2 = params.has('v2');
|
||||
const hasV3 = has3 && params.has('v3');
|
||||
|
||||
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>
|
||||
|
||||
@@ -142,12 +295,54 @@
|
||||
</div>
|
||||
|
||||
<div class="calc-footer">
|
||||
<div class="footer-controls" bind:this={footerControlsEl}>
|
||||
<button type="button" class="clear-btn" on:click={clear} aria-label="Clear calculator inputs">
|
||||
Clear
|
||||
</button>
|
||||
{#if config.factor && config.type === 'standard'}
|
||||
<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">
|
||||
1 {config.labels.in1} = {config.factor}{config.offset ? ` + ${config.offset}` : ''} {config.labels.in2}
|
||||
{conversionRateText}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -284,6 +479,13 @@
|
||||
padding: 1rem 2rem 1.25rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.footer-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
padding: 0.5rem 1.25rem;
|
||||
border: 1px solid var(--border);
|
||||
@@ -303,6 +505,69 @@
|
||||
outline: none;
|
||||
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 {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
|
||||
@@ -24,8 +24,6 @@
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s;
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
cursor: pointer;
|
||||
}
|
||||
.category-card:hover {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { solve } from '$lib/engine';
|
||||
import type { CalculatorDef } from '$lib/data/calculators';
|
||||
import { formatConversionValue } from '$lib/utils/formatConversionValue';
|
||||
|
||||
export let config: CalculatorDef;
|
||||
|
||||
@@ -17,40 +18,29 @@
|
||||
? solve(config, 1, exampleInput.toString(), '', '')
|
||||
: null;
|
||||
$: offset = config.offset ?? 0;
|
||||
$: formulaExpression = supportsExample
|
||||
? `${exampleInput} × ${config.factor}${offset ? ` + ${offset}` : ''}`
|
||||
$: hasOffset = Boolean(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 =
|
||||
supportsExample && config.factor !== 0
|
||||
? (1 - offset) / config.factor
|
||||
: null;
|
||||
$: formattedReverseValue = formatExampleValue(reverseExampleValue);
|
||||
$: formattedReverseValue = formatConversionValue(reverseExampleValue);
|
||||
</script>
|
||||
|
||||
{#if supportsExample && result}
|
||||
<section class="example-card">
|
||||
<h3>How to convert {config.labels.in1} to {config.labels.in2}</h3>
|
||||
<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 class="example-note">
|
||||
1 {config.labels.in2} = {formattedReverseValue} {config.labels.in1}
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { solve } from '$lib/engine';
|
||||
import { parseScientificNotation, type ScientificNotationParts } from '$lib/utils/formatScientific';
|
||||
import type { CalculatorDef } from '$lib/data/calculators';
|
||||
|
||||
export let config: CalculatorDef;
|
||||
|
||||
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 formatted = solve(config, 1, value.toString(), '', '');
|
||||
const buildRow = (value: number, c: CalculatorDef): Row => {
|
||||
const formatted = solve(c, 1, value.toString(), '', '');
|
||||
return {
|
||||
input: value,
|
||||
output: formatted.val2 || '—',
|
||||
scientific: formatted.val2 ? parseScientificNotation(formatted.val2) ?? undefined : undefined,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -24,8 +22,8 @@
|
||||
let outputLabel = 'target units';
|
||||
|
||||
$: supportsTable = ['standard', 'inverse'].includes(config.type);
|
||||
$: rows = supportsTable
|
||||
? numericSamples.map(buildRow)
|
||||
$: rows = (config && supportsTable)
|
||||
? numericSamples.map(v => buildRow(v, config))
|
||||
: [];
|
||||
$: inputLabel = config.labels?.in1 ?? 'source units';
|
||||
$: outputLabel = config.labels?.in2 ?? 'target units';
|
||||
@@ -44,16 +42,7 @@
|
||||
<div class="chart-row">
|
||||
<p class="chart-statement">
|
||||
Converting {row.input} <span class="chart-unit">{inputLabel}</span> into <span class="chart-unit">{outputLabel}</span> equals
|
||||
<span class="chart-output-value">
|
||||
{#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-value">{row.output}</span>
|
||||
<span class="chart-output-unit">{outputLabel}</span>.
|
||||
</p>
|
||||
</div>
|
||||
@@ -105,22 +94,6 @@
|
||||
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-output-unit {
|
||||
font-variant: petite-caps;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { getDefinition } from '$lib/data/unitDefinitions';
|
||||
import type { CalculatorDef } from '$lib/data/calculators';
|
||||
import type { CalculatorDef } from '$lib/data/calculatorLoader';
|
||||
|
||||
export let config: CalculatorDef;
|
||||
|
||||
@@ -11,8 +11,10 @@
|
||||
|
||||
$: label1 = config.labels.in1 || 'Unit 1';
|
||||
$: 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>
|
||||
|
||||
<section class="definition-card">
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { searchCalculators } from '$lib/data/calculators';
|
||||
import { goto } from '$app/navigation';
|
||||
import { loadCalculators, searchCalculators, type CalculatorDef } from '$lib/data/calculatorLoader';
|
||||
|
||||
export let idPrefix = 'search';
|
||||
|
||||
let query = '';
|
||||
let focused = false;
|
||||
let selectedIndex = -1;
|
||||
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`;
|
||||
$: inputId = `${idPrefix}-input`;
|
||||
$: isOpen = focused && results.length > 0;
|
||||
|
||||
@@ -2,7 +2,18 @@
|
||||
import { browser } from '$app/environment';
|
||||
import { onMount } from 'svelte';
|
||||
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 expandedUnits: Record<string, string> = {};
|
||||
@@ -23,50 +34,85 @@
|
||||
|
||||
type UnitGroup = {
|
||||
label: string;
|
||||
conversions: CalculatorDef[];
|
||||
conversions: UnitConversionLink[];
|
||||
};
|
||||
|
||||
type UnitBucket = {
|
||||
label: string;
|
||||
conversions: CalculatorDef[];
|
||||
conversions: UnitConversionLink[];
|
||||
};
|
||||
|
||||
const sortConversionsForUnit = (conversions: CalculatorDef[], unitLabel: string) => {
|
||||
const normalizedUnit = unitLabel.toLowerCase();
|
||||
return conversions.slice().sort((a, b) => {
|
||||
const aIsSource = a.labels.in1?.toLowerCase() === normalizedUnit;
|
||||
const bIsSource = b.labels.in1?.toLowerCase() === normalizedUnit;
|
||||
if (aIsSource !== bIsSource) {
|
||||
return aIsSource ? -1 : 1;
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
type UnitConversionLink = {
|
||||
name: string;
|
||||
slug: string;
|
||||
sortKey: string;
|
||||
};
|
||||
|
||||
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]) => {
|
||||
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 => {
|
||||
[calc.labels.in1, calc.labels.in2].forEach(unit => {
|
||||
const key = unit.toLowerCase();
|
||||
const existing = buckets.get(key);
|
||||
if (existing) {
|
||||
existing.conversions.push(calc);
|
||||
} else {
|
||||
buckets.set(key, {
|
||||
label: unit,
|
||||
conversions: [calc],
|
||||
});
|
||||
const pairKey = toPairKey(calc.labels.in1, calc.labels.in2);
|
||||
const existing = canonicalByPair.get(pairKey);
|
||||
if (!existing || calc.slug.localeCompare(existing.slug) < 0) {
|
||||
canonicalByPair.set(pairKey, calc);
|
||||
}
|
||||
});
|
||||
|
||||
canonicalByPair.forEach(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()]
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([, bucket]) => ({
|
||||
label: bucket.label,
|
||||
conversions: sortConversionsForUnit(bucket.conversions, bucket.label),
|
||||
conversions: sortConversionsForUnit(bucket.conversions),
|
||||
}));
|
||||
|
||||
return { key, meta, units };
|
||||
@@ -131,6 +177,11 @@
|
||||
}
|
||||
|
||||
export let open = false;
|
||||
|
||||
$: if (browser && (isDesktop || open)) {
|
||||
loadData();
|
||||
}
|
||||
|
||||
$: isSidebarHidden = !isDesktop && !open;
|
||||
|
||||
function closeSidebar() {
|
||||
@@ -188,14 +239,14 @@
|
||||
</button>
|
||||
{#if expandedUnits[group.key] === unit.label}
|
||||
<ul class="unit-list">
|
||||
{#each unit.conversions as calc}
|
||||
{#each unit.conversions as conversion}
|
||||
<li>
|
||||
<a
|
||||
href="/{calc.slug}"
|
||||
class:current={currentPath === `/${calc.slug}`}
|
||||
aria-current={currentPath === `/${calc.slug}` ? 'page' : undefined}
|
||||
href="/{conversion.slug}"
|
||||
class:current={currentPath === `/${conversion.slug}`}
|
||||
aria-current={currentPath === `/${conversion.slug}` ? 'page' : undefined}
|
||||
>
|
||||
{calc.name}
|
||||
{conversion.name}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
|
||||
53
hdyc-svelte/src/lib/data/calculatorLoader.ts
Normal file
53
hdyc-svelte/src/lib/data/calculatorLoader.ts
Normal 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
|
||||
);
|
||||
}
|
||||
@@ -17,26 +17,26 @@ export interface CalculatorDef {
|
||||
}
|
||||
|
||||
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: '🔄' },
|
||||
'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 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": "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": "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": "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": "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"}, "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": "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": "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": "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"}, "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": "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},
|
||||
@@ -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-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": "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": "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-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-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},
|
||||
@@ -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": "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-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": "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},
|
||||
@@ -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-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-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-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},
|
||||
@@ -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": "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-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-decimal", "name": "Hex to Decimal", "category": "number-systems", "type": "standard", "labels": {"in1": "Hex", "in2": "Decimal"}, "hidden": true},
|
||||
{"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-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-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": "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": "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": "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": "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-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},
|
||||
@@ -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-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-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-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},
|
||||
@@ -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-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": "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-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},
|
||||
@@ -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": "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": "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-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},
|
||||
@@ -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-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-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-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},
|
||||
@@ -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-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": "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-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},
|
||||
@@ -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-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": "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-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-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"}},
|
||||
@@ -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-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-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-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-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"}, "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-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},
|
||||
@@ -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-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": "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-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": "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},
|
||||
@@ -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-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": "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-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},
|
||||
@@ -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-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": "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": "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": "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, "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": "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": "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": "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, "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": "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},
|
||||
@@ -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": "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": "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": "lamberts-to-phot", "name": "Lamberts to Phot", "category": "other", "type": "standard", "teaser": "Flip lamberts back into phots.", "labels": {"in1": "Lamberts", "in2": "Phot"}},
|
||||
{"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"}, "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": "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": "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": "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"}, "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": "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},
|
||||
@@ -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-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": "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": "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},
|
||||
@@ -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-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-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": "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},
|
||||
@@ -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": "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": "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": "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": "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"}, "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": "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},
|
||||
@@ -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": "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": "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": "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": "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, "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-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},
|
||||
@@ -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": "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": "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": "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},
|
||||
@@ -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-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-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-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},
|
||||
@@ -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": "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-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-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},
|
||||
@@ -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 {
|
||||
return slugIndex.get(slug);
|
||||
|
||||
85
hdyc-svelte/src/lib/data/stats.ts
Normal file
85
hdyc-svelte/src/lib/data/stats.ts
Normal 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;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { calculators } from './calculators';
|
||||
import { loadCalculators, type CalculatorDef } from './calculatorLoader';
|
||||
|
||||
const domainDefinitions: Record<string, { summary: string; context: string }> = {
|
||||
length: {
|
||||
@@ -97,7 +97,6 @@ const normalizeLabel = (label?: string): string | undefined => {
|
||||
return alias ?? trimmed;
|
||||
};
|
||||
|
||||
const definitions: Record<string, Record<string, string>> = {};
|
||||
const categoryPriority = [...Object.keys(domainDefinitions)];
|
||||
|
||||
const buildDefinition = (label: string, categoryKey: string): string => {
|
||||
@@ -107,17 +106,36 @@ const buildDefinition = (label: string, categoryKey: string): string => {
|
||||
return `${label} ${description}`;
|
||||
};
|
||||
|
||||
// Lazily built definitions cache
|
||||
let definitions: Record<string, Record<string, string>> | null = null;
|
||||
let buildPromise: Promise<void> | null = null;
|
||||
|
||||
async function ensureBuilt(): Promise<Record<string, Record<string, string>>> {
|
||||
if (definitions) return definitions;
|
||||
if (buildPromise) {
|
||||
await buildPromise;
|
||||
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 = definitions[normalized] || {};
|
||||
const bucket = defs[normalized] || {};
|
||||
const text = buildDefinition(normalized, category);
|
||||
bucket[category] = text;
|
||||
definitions[normalized] = bucket;
|
||||
defs[normalized] = bucket;
|
||||
});
|
||||
});
|
||||
definitions = defs;
|
||||
});
|
||||
|
||||
await buildPromise;
|
||||
return definitions!;
|
||||
}
|
||||
|
||||
const findByPriority = (entries: Record<string, string>, preferred?: string): string | undefined => {
|
||||
if (!entries) return undefined;
|
||||
@@ -129,11 +147,10 @@ const findByPriority = (entries: Record<string, string>, preferred?: string): st
|
||||
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);
|
||||
if (!normalized) return undefined;
|
||||
const entries = definitions[normalized];
|
||||
const defs = await ensureBuilt();
|
||||
const entries = defs[normalized];
|
||||
return findByPriority(entries, category);
|
||||
}
|
||||
|
||||
export const unitDefinitions = definitions;
|
||||
|
||||
244
hdyc-svelte/src/lib/palettes.ts
Normal file
244
hdyc-svelte/src/lib/palettes.ts
Normal 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)',
|
||||
},
|
||||
},
|
||||
];
|
||||
21
hdyc-svelte/src/lib/utils/conversionRate.ts
Normal file
21
hdyc-svelte/src/lib/utils/conversionRate.ts
Normal 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}`;
|
||||
};
|
||||
24
hdyc-svelte/src/lib/utils/formatConversionValue.js
Normal file
24
hdyc-svelte/src/lib/utils/formatConversionValue.js
Normal 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 };
|
||||
23
hdyc-svelte/src/lib/utils/formatConversionValue.ts
Normal file
23
hdyc-svelte/src/lib/utils/formatConversionValue.ts
Normal 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');
|
||||
}
|
||||
@@ -7,251 +7,8 @@
|
||||
import '../app.css';
|
||||
import Sidebar from '$lib/components/Sidebar.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';
|
||||
|
||||
type WindowWithAnalytics = Window & {
|
||||
@@ -265,6 +22,7 @@
|
||||
let isMobileHeader = false;
|
||||
let theme: ThemeMode = 'dark';
|
||||
let selectedPaletteIndex = 0;
|
||||
let savedScrollRestoration: ScrollRestoration | null = null;
|
||||
$: isHomepage = $page.url.pathname === '/';
|
||||
$: if (isHomepage && (sidebarOpen || headerSearchOpen)) {
|
||||
sidebarOpen = false;
|
||||
@@ -326,14 +84,30 @@
|
||||
document.head.appendChild(script);
|
||||
};
|
||||
|
||||
afterNavigate(() => {
|
||||
const scrollToTop = () => {
|
||||
if (!browser) return;
|
||||
window.scrollTo({ top: 0, behavior: 'auto' });
|
||||
};
|
||||
|
||||
afterNavigate(({ from, to, type }) => {
|
||||
sidebarOpen = false;
|
||||
headerSearchOpen = false;
|
||||
|
||||
if (!browser) return;
|
||||
if (type === 'popstate') return;
|
||||
if (!from || !to) return;
|
||||
if (from.url.pathname === to.url.pathname) return;
|
||||
|
||||
scrollToTop();
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
if (!browser) return;
|
||||
const appWindow = window as WindowWithAnalytics;
|
||||
if ('scrollRestoration' in window.history) {
|
||||
savedScrollRestoration = window.history.scrollRestoration;
|
||||
window.history.scrollRestoration = 'manual';
|
||||
}
|
||||
let idleCallbackId: number | null = null;
|
||||
let fallbackTimeoutId: number | null = null;
|
||||
|
||||
@@ -411,6 +185,9 @@
|
||||
window.clearTimeout(fallbackTimeoutId);
|
||||
}
|
||||
window.removeEventListener('keydown', handleEscape);
|
||||
if (savedScrollRestoration !== null) {
|
||||
window.history.scrollRestoration = savedScrollRestoration;
|
||||
}
|
||||
};
|
||||
|
||||
if ('addEventListener' in mediaQuery) {
|
||||
@@ -461,7 +238,7 @@
|
||||
</button>
|
||||
{/if}
|
||||
<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>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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 SearchBar from '$lib/components/SearchBar.svelte';
|
||||
import { buildSeoMeta, SITE_NAME, SITE_URL, toJsonLd } from '$lib/seo';
|
||||
@@ -18,8 +18,8 @@
|
||||
key,
|
||||
...meta,
|
||||
}));
|
||||
const totalCalculators = calculators.length;
|
||||
const totalCategories = Object.keys(homepageCategories).length;
|
||||
const totalConversions = totalCalculators;
|
||||
const totalCategoriesCount = Object.keys(homepageCategories).length;
|
||||
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 seo = buildSeoMeta({
|
||||
@@ -49,7 +49,7 @@
|
||||
<meta name="twitter:card" content={seo.twitter.card} />
|
||||
<meta name="twitter:title" content={seo.twitter.title} />
|
||||
<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>
|
||||
|
||||
<section class="hero">
|
||||
@@ -62,11 +62,11 @@
|
||||
|
||||
<div class="stats-row">
|
||||
<div class="stat">
|
||||
<div class="stat-num">{totalCalculators}</div>
|
||||
<div class="stat-num">{totalConversions}</div>
|
||||
<div class="stat-label">Converters</div>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<div class="stat-num">{totalCategories}</div>
|
||||
<div class="stat-num">{totalCategoriesCount}</div>
|
||||
<div class="stat-label">Categories</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
3
hdyc-svelte/src/routes/+page.ts
Normal file
3
hdyc-svelte/src/routes/+page.ts
Normal 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;
|
||||
@@ -1,8 +1,5 @@
|
||||
<script lang="ts">
|
||||
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 { 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>
|
||||
|
||||
<svelte:head>
|
||||
@@ -80,8 +68,8 @@
|
||||
<meta name="twitter:card" content={seo.twitter.card} />
|
||||
<meta name="twitter:title" content={seo.twitter.title} />
|
||||
<meta name="twitter:description" content={seo.twitter.description} />
|
||||
<script type="application/ld+json">{breadcrumbJsonLd}</script>
|
||||
<script type="application/ld+json">{webPageJsonLd}</script>
|
||||
{@html `<script type="application/ld+json">${breadcrumbJsonLd}</script>`}
|
||||
{@html `<script type="application/ld+json">${webPageJsonLd}</script>`}
|
||||
</svelte:head>
|
||||
|
||||
<nav class="breadcrumbs" aria-label="Breadcrumb">
|
||||
@@ -94,7 +82,9 @@
|
||||
|
||||
<h1 class="page-title calculator-page-title">{calc.name}</h1>
|
||||
|
||||
{#key calc.slug}
|
||||
<Calculator config={calc} showTitle={false} />
|
||||
{/key}
|
||||
|
||||
<div class="seo-content">
|
||||
{#if calc.descriptionHTML}
|
||||
|
||||
@@ -1,6 +1,28 @@
|
||||
<script lang="ts">
|
||||
import type { PageData } from './$types';
|
||||
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;
|
||||
|
||||
@@ -66,8 +88,8 @@
|
||||
<meta name="twitter:card" content={seo.twitter.card} />
|
||||
<meta name="twitter:title" content={seo.twitter.title} />
|
||||
<meta name="twitter:description" content={seo.twitter.description} />
|
||||
<script type="application/ld+json">{breadcrumbJsonLd}</script>
|
||||
<script type="application/ld+json">{collectionJsonLd}</script>
|
||||
{@html `<script type="application/ld+json">${breadcrumbJsonLd}</script>`}
|
||||
{@html `<script type="application/ld+json">${collectionJsonLd}</script>`}
|
||||
</svelte:head>
|
||||
|
||||
<nav class="breadcrumbs" aria-label="Breadcrumb">
|
||||
@@ -85,8 +107,18 @@
|
||||
|
||||
<div class="calc-list">
|
||||
{#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}
|
||||
{#if conversionRateText}
|
||||
<span class="calc-list-tooltip" role="tooltip">{conversionRateText}</span>
|
||||
{/if}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,7 @@ import { calculators, categories } from '$lib/data/calculators';
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
const calculatorUrls = calculators.map(
|
||||
(calc) => `
|
||||
<url>
|
||||
(calc) => ` <url>
|
||||
<loc>https://howdoyouconvert.com/${calc.slug}</loc>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
@@ -12,8 +11,7 @@ export const GET: RequestHandler = async () => {
|
||||
);
|
||||
|
||||
const categoryUrls = Object.keys(categories).map(
|
||||
(category) => `
|
||||
<url>
|
||||
(category) => ` <url>
|
||||
<loc>https://howdoyouconvert.com/category/${category}</loc>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.9</priority>
|
||||
@@ -27,8 +25,8 @@ export const GET: RequestHandler = async () => {
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>1.0</priority>
|
||||
</url>
|
||||
${categoryUrls.join('')}
|
||||
${calculatorUrls.join('')}
|
||||
${categoryUrls.join('\n')}
|
||||
${calculatorUrls.join('\n')}
|
||||
</urlset>`;
|
||||
|
||||
return new Response(sitemap, {
|
||||
|
||||
37133
hdyc-svelte/static/data/calculators.json
Normal file
37133
hdyc-svelte/static/data/calculators.json
Normal file
File diff suppressed because it is too large
Load Diff
76
migrate.py
76
migrate.py
@@ -1,10 +1,13 @@
|
||||
import json
|
||||
import re
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
CALCLIST = BASE_DIR / 'calculators_list.md'
|
||||
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 = [
|
||||
'length',
|
||||
@@ -29,6 +32,29 @@ CATEGORY_KEYS = [
|
||||
'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)
|
||||
|
||||
# Lightweight label normalization to catch duplicate/identity conversions
|
||||
@@ -418,28 +444,12 @@ export interface CalculatorDef {
|
||||
}
|
||||
|
||||
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: '🔄' },
|
||||
};
|
||||
"""
|
||||
for k, v in CATEGORIES.items():
|
||||
out += f" '{k}': {json.dumps(v, ensure_ascii=False).replace('{', '{ ').replace('}', ' }')},\n"
|
||||
out += "};\n"
|
||||
|
||||
out += """
|
||||
export const calculators: CalculatorDef[] = [
|
||||
"""
|
||||
for e in calculators_ts_entries:
|
||||
@@ -454,8 +464,13 @@ export const calculators: CalculatorDef[] = [
|
||||
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 {
|
||||
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")
|
||||
|
||||
# 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__':
|
||||
process()
|
||||
|
||||
27
tests/test_apothecary_page.py
Normal file
27
tests/test_apothecary_page.py
Normal 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
106
tests/test_consistency.py
Normal 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()
|
||||
27
tests/test_conversion_rate_tooltip.py
Normal file
27
tests/test_conversion_rate_tooltip.py
Normal 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()
|
||||
68
tests/test_gauss_to_oersted_conversion_rate.py
Normal file
68
tests/test_gauss_to_oersted_conversion_rate.py
Normal 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()
|
||||
@@ -72,7 +72,7 @@ class HomepageCategoryRegressionTests(unittest.TestCase):
|
||||
def test_homepage_uses_canonical_categories_map(self) -> None:
|
||||
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("fluids: { label: 'Fluids', icon: '💧' }", text)
|
||||
self.assertIn("magnetism: { label: 'Magnetism', icon: '🧲' }", text)
|
||||
|
||||
26
tests/test_quick_conversion_formatting.py
Normal file
26
tests/test_quick_conversion_formatting.py
Normal 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()
|
||||
Reference in New Issue
Block a user