Keep units styled and prevent zero reverse examples
This commit is contained in:
@@ -113,8 +113,8 @@ a:hover {
|
||||
.theme-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.35rem 0.9rem;
|
||||
justify-content: center;
|
||||
padding: 0.35rem 0.7rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--input-bg);
|
||||
@@ -125,6 +125,7 @@ a:hover {
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, background 0.2s, color 0.2s, box-shadow 0.2s;
|
||||
min-width: 42px;
|
||||
}
|
||||
.theme-toggle:hover {
|
||||
border-color: var(--accent);
|
||||
@@ -133,17 +134,59 @@ a:hover {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px var(--accent-glow);
|
||||
}
|
||||
.theme-toggle__label {
|
||||
font-size: 0.78rem;
|
||||
letter-spacing: 0.02em;
|
||||
|
||||
.floating-palette-controls {
|
||||
position: fixed;
|
||||
bottom: clamp(1rem, 2vw, 1.5rem);
|
||||
right: clamp(1rem, 2vw, 1.5rem);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.35rem 0.65rem;
|
||||
border-radius: 999px;
|
||||
background: var(--bg-elevated);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.35);
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
z-index: 200;
|
||||
}
|
||||
.palette-dots {
|
||||
display: flex;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.palette-dot {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid transparent;
|
||||
padding: 0;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
.palette-dot:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
.palette-dot.active,
|
||||
.palette-dot:focus-visible {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-glow);
|
||||
}
|
||||
.palette-dot:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.theme-toggle__label {
|
||||
display: none;
|
||||
.floating-palette-controls {
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0.55rem;
|
||||
right: 0.75rem;
|
||||
}
|
||||
.theme-toggle {
|
||||
padding-inline: 0.75rem;
|
||||
.palette-dot {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,13 +15,34 @@
|
||||
$: result = supportsExample
|
||||
? solve(config, 1, exampleInput.toString(), '', '')
|
||||
: null;
|
||||
$: reverseResult = supportsExample
|
||||
? solve(config, 2, '', '1', '')
|
||||
: null;
|
||||
$: offset = config.offset ?? 0;
|
||||
$: formulaExpression = supportsExample
|
||||
? `${exampleInput} × ${config.factor}${offset ? ` + ${offset}` : ''}`
|
||||
: '';
|
||||
|
||||
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);
|
||||
</script>
|
||||
|
||||
{#if supportsExample && result}
|
||||
@@ -31,7 +52,7 @@
|
||||
1 {config.labels.in1} = {config.factor}{config.offset ? ` + ${config.offset}` : ''} {config.labels.in2}
|
||||
</p>
|
||||
<p class="example-note">
|
||||
1 {config.labels.in2} = {reverseResult?.val1 ?? '—'} {config.labels.in1}
|
||||
1 {config.labels.in2} = {formattedReverseValue} {config.labels.in1}
|
||||
</p>
|
||||
<p class="example-line">
|
||||
Example: convert {exampleInput} {config.labels.in1} to {config.labels.in2}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
{#each rows as row}
|
||||
<div class="chart-row">
|
||||
<p class="chart-statement">
|
||||
Converting {row.input} {inputLabel} into {outputLabel} equals
|
||||
Converting {row.input} <span class="chart-unit">{inputLabel}</span> into <span class="chart-unit">{outputLabel}</span> equals
|
||||
<span class="chart-output-value">{row.output}</span>
|
||||
<span class="chart-output-unit">{outputLabel}</span>.
|
||||
</p>
|
||||
@@ -94,6 +94,7 @@
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.chart-unit,
|
||||
.chart-output-unit {
|
||||
font-variant: petite-caps;
|
||||
}
|
||||
|
||||
@@ -6,31 +6,283 @@
|
||||
import Sidebar from '$lib/components/Sidebar.svelte';
|
||||
import SearchBar from '$lib/components/SearchBar.svelte';
|
||||
|
||||
let sidebarOpen = false;
|
||||
let theme: 'light' | 'dark' = 'dark';
|
||||
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';
|
||||
|
||||
const applyTheme = (value: 'light' | 'dark') => {
|
||||
type PaletteTheme = Record<PaletteVar, string>;
|
||||
|
||||
type Palette = {
|
||||
slug: string;
|
||||
label: string;
|
||||
light: PaletteTheme;
|
||||
dark: PaletteTheme;
|
||||
};
|
||||
|
||||
const paletteVariableKeys: 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',
|
||||
];
|
||||
|
||||
const palettes: Palette[] = [
|
||||
{
|
||||
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': '#2b2509',
|
||||
'text-muted': '#6d5f2a',
|
||||
'accent': '#facc15',
|
||||
'accent-dark': '#b45309',
|
||||
'accent-glow': 'rgba(250, 204, 21, 0.2)',
|
||||
'accent-gradient': 'linear-gradient(135deg, #facc15, #f97316)',
|
||||
'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)',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
let sidebarOpen = false;
|
||||
let theme: ThemeMode = 'dark';
|
||||
let selectedPaletteIndex = 0;
|
||||
|
||||
const updatePaletteStyles = (index: number, mode: ThemeMode) => {
|
||||
if (!browser) return;
|
||||
const palette = palettes[index];
|
||||
const colors = palette[mode];
|
||||
paletteVariableKeys.forEach(key => {
|
||||
document.documentElement.style.setProperty(`--${key}`, colors[key]);
|
||||
});
|
||||
document.documentElement.dataset.palette = palette.slug;
|
||||
};
|
||||
|
||||
const updateTheme = (value: ThemeMode, persist = false) => {
|
||||
theme = value;
|
||||
if (!browser) return;
|
||||
document.documentElement.dataset.theme = value;
|
||||
window.localStorage.setItem('theme', value);
|
||||
updatePaletteStyles(selectedPaletteIndex, value);
|
||||
if (persist) {
|
||||
window.localStorage.setItem('theme', value);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleTheme = () => {
|
||||
applyTheme(theme === 'dark' ? 'light' : 'dark');
|
||||
const nextTheme: ThemeMode = theme === 'dark' ? 'light' : 'dark';
|
||||
updateTheme(nextTheme, true);
|
||||
};
|
||||
|
||||
const setPalette = (index: number) => {
|
||||
selectedPaletteIndex = index;
|
||||
if (!browser) return;
|
||||
window.localStorage.setItem('palette', palettes[index].slug);
|
||||
updatePaletteStyles(index, theme);
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
if (!browser) return;
|
||||
|
||||
const savedTheme = window.localStorage.getItem('theme') as 'light' | 'dark' | null;
|
||||
const initialTheme = savedTheme ?? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
|
||||
applyTheme(initialTheme);
|
||||
const savedTheme = window.localStorage.getItem('theme') as ThemeMode | null;
|
||||
const savedPalette = window.localStorage.getItem('palette');
|
||||
const paletteIndex = palettes.findIndex(palette => palette.slug === savedPalette);
|
||||
selectedPaletteIndex = paletteIndex >= 0 ? paletteIndex : 0;
|
||||
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const initialTheme: ThemeMode = savedTheme ?? (mediaQuery.matches ? 'dark' : 'light');
|
||||
updateTheme(initialTheme, Boolean(savedTheme));
|
||||
|
||||
const handlePreferenceChange = (event: MediaQueryListEvent) => {
|
||||
if (window.localStorage.getItem('theme')) return;
|
||||
applyTheme(event.matches ? 'dark' : 'light');
|
||||
updateTheme(event.matches ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
const cleanup = () => {
|
||||
@@ -67,15 +319,6 @@
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<SearchBar />
|
||||
<button
|
||||
type="button"
|
||||
class="theme-toggle"
|
||||
on:click={toggleTheme}
|
||||
aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
|
||||
>
|
||||
<span aria-hidden="true">{theme === 'dark' ? '☀️' : '🌙'}</span>
|
||||
<span class="theme-toggle__label">{theme === 'dark' ? 'Light' : 'Dark'} mode</span>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -86,6 +329,30 @@
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div class="floating-palette-controls" role="group" aria-label="Theme and palette controls">
|
||||
<div class="palette-dots">
|
||||
{#each palettes as palette, index}
|
||||
<button
|
||||
type="button"
|
||||
class="palette-dot"
|
||||
class:active={index === selectedPaletteIndex}
|
||||
aria-pressed={index === selectedPaletteIndex}
|
||||
aria-label={`Switch to ${palette.label} palette`}
|
||||
style={`background: ${palette[theme].accentGradient};`}
|
||||
on:click={() => setPalette(index)}
|
||||
></button>
|
||||
{/each}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="theme-toggle"
|
||||
on:click={toggleTheme}
|
||||
aria-label={`Switch to ${theme === 'dark' ? 'light' : 'dark'} mode`}
|
||||
>
|
||||
<span aria-hidden="true">{theme === 'dark' ? '☀️' : '🌙'}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<footer class="site-footer">
|
||||
© {new Date().getFullYear()} HowDoYouConvert.com — Free unit conversion calculators. All rights reserved.
|
||||
</footer>
|
||||
|
||||
Reference in New Issue
Block a user