diff --git a/projects/stopwatch/index.html b/projects/stopwatch/index.html
index f786147..8371e51 100644
--- a/projects/stopwatch/index.html
+++ b/projects/stopwatch/index.html
@@ -1,18 +1,41 @@
-
+
-
-
- Stopwatch | Vanilla Verse
-
+
+
+ Stopwatch
+
-
-
-
-
-
-
-
+
+
⏱️ Stopwatch
+
+
+ 00:00.000
+
+
+
+
+
+
+
+
+
+
+
+ Space Start/Pause • L Lap • R Reset
+
+
+
+
diff --git a/projects/stopwatch/main.js b/projects/stopwatch/main.js
index 0ee5d09..6f84a90 100644
--- a/projects/stopwatch/main.js
+++ b/projects/stopwatch/main.js
@@ -7,9 +7,4 @@
* Implementation notes:
* - Use performance.now() for precise deltas
* - requestAnimationFrame for smooth display updates
- */
-
-document.addEventListener('DOMContentLoaded', () => {
- console.log('Stopwatch app ready');
- // TODO: Wire up buttons and timing loop
-});
+ */
\ No newline at end of file
diff --git a/projects/stopwatch/styles.css b/projects/stopwatch/styles.css
index 0885a41..f5af81f 100644
--- a/projects/stopwatch/styles.css
+++ b/projects/stopwatch/styles.css
@@ -1,3 +1,244 @@
+<<<<<<< unit
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 20px;
+}
+
+.container {
+ background: rgba(255, 255, 255, 0.95);
+ border-radius: 24px;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+ padding: 40px;
+ max-width: 500px;
+ width: 100%;
+}
+
+h1 {
+ text-align: center;
+ color: #333;
+ font-size: 28px;
+ margin-bottom: 30px;
+ font-weight: 600;
+}
+
+.display {
+ background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
+ color: #fff;
+ font-size: 56px;
+ font-weight: 300;
+ text-align: center;
+ padding: 40px 20px;
+ border-radius: 16px;
+ margin-bottom: 30px;
+ font-variant-numeric: tabular-nums;
+ letter-spacing: 2px;
+ box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.2);
+}
+
+.controls {
+ display: flex;
+ gap: 12px;
+ margin-bottom: 30px;
+ justify-content: center;
+}
+
+button {
+ flex: 1;
+ padding: 16px 24px;
+ font-size: 16px;
+ font-weight: 600;
+ border: none;
+ border-radius: 12px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+ position: relative;
+ overflow: hidden;
+}
+
+button:focus {
+ outline: 3px solid rgba(102, 126, 234, 0.5);
+ outline-offset: 2px;
+}
+
+button:active {
+ transform: translateY(1px);
+}
+
+button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+#startPauseBtn {
+ background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
+ color: white;
+}
+
+#startPauseBtn:hover:not(:disabled) {
+ box-shadow: 0 8px 20px rgba(56, 239, 125, 0.4);
+ transform: translateY(-2px);
+}
+
+#startPauseBtn.paused {
+ background: linear-gradient(135deg, #ee0979 0%, #ff6a00 100%);
+}
+
+#startPauseBtn.paused:hover:not(:disabled) {
+ box-shadow: 0 8px 20px rgba(238, 9, 121, 0.4);
+}
+
+#lapBtn {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+}
+
+#lapBtn:hover:not(:disabled) {
+ box-shadow: 0 8px 20px rgba(118, 75, 162, 0.4);
+ transform: translateY(-2px);
+}
+
+#resetBtn {
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
+ color: white;
+}
+
+#resetBtn:hover:not(:disabled) {
+ box-shadow: 0 8px 20px rgba(245, 87, 108, 0.4);
+ transform: translateY(-2px);
+}
+
+.laps-container {
+ max-height: 300px;
+ overflow-y: auto;
+ margin-top: 20px;
+}
+
+.laps-container::-webkit-scrollbar {
+ width: 8px;
+}
+
+.laps-container::-webkit-scrollbar-track {
+ background: #f1f1f1;
+ border-radius: 10px;
+}
+
+.laps-container::-webkit-scrollbar-thumb {
+ background: #888;
+ border-radius: 10px;
+}
+
+.laps-container::-webkit-scrollbar-thumb:hover {
+ background: #555;
+}
+
+.laps-header {
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 12px;
+ font-size: 18px;
+}
+
+.lap-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 14px 16px;
+ background: #f8f9fa;
+ margin-bottom: 8px;
+ border-radius: 10px;
+ border-left: 4px solid #667eea;
+ transition: all 0.2s ease;
+}
+
+.lap-item:hover {
+ background: #e9ecef;
+ transform: translateX(4px);
+}
+
+.lap-number {
+ font-weight: 600;
+ color: #667eea;
+ font-size: 16px;
+}
+
+.lap-times {
+ display: flex;
+ gap: 20px;
+ align-items: center;
+}
+
+.lap-time {
+ font-variant-numeric: tabular-nums;
+ color: #333;
+ font-size: 15px;
+}
+
+.lap-delta {
+ font-variant-numeric: tabular-nums;
+ color: #666;
+ font-size: 14px;
+ font-style: italic;
+}
+
+.keyboard-hint {
+ text-align: center;
+ color: #666;
+ font-size: 13px;
+ margin-top: 20px;
+ padding-top: 20px;
+ border-top: 1px solid #e0e0e0;
+}
+
+.keyboard-hint kbd {
+ background: #f5f5f5;
+ padding: 3px 8px;
+ border-radius: 4px;
+ border: 1px solid #ccc;
+ font-family: monospace;
+ font-size: 12px;
+}
+
+@media (max-width: 500px) {
+ .container {
+ padding: 24px;
+ }
+
+ .display {
+ font-size: 42px;
+ padding: 30px 15px;
+ }
+
+ button {
+ padding: 14px 18px;
+ font-size: 14px;
+ }
+
+ .controls {
+ flex-wrap: wrap;
+ }
+
+ button {
+ flex-basis: calc(50% - 6px);
+ }
+
+ #resetBtn {
+ flex-basis: 100%;
+ }
+}
+=======
/* TODO: Style stopwatch display and controls */
:root {
--bg: #0b1220;
@@ -15,3 +256,4 @@ body {
place-items: center;
}
#app { width: min(92vw, 600px); }
+>>>>>>> main
diff --git a/projects/unit-converter/index.html b/projects/unit-converter/index.html
index cfb4f47..6154d99 100644
--- a/projects/unit-converter/index.html
+++ b/projects/unit-converter/index.html
@@ -1,18 +1,50 @@
-
+
-
-
- Unit Converter | Vanilla Verse
-
+
+
+ Unit Converter
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/projects/unit-converter/main.js b/projects/unit-converter/main.js
index 6a883ab..4365ad5 100644
--- a/projects/unit-converter/main.js
+++ b/projects/unit-converter/main.js
@@ -1,16 +1,201 @@
-/**
- * TODO: Unit converter logic
- * Categories:
- * - Length: m, km, cm, mm, mi, yd, ft, in
- * - Temperature: C, F, K
- * - Weight: g, kg, lb, oz
- * Implementation:
- * - Define conversion maps/functions
- * - Convert on input/change events; show precise and rounded values
- * Optional: Persist last category/units to localStorage
- */
-
-document.addEventListener('DOMContentLoaded', () => {
- console.log('Unit converter ready');
- // TODO: Build conversion functions and wire to form
+// Unit conversion definitions
+const units = {
+ length: {
+ m: { name: 'Meters', toBase: (v) => v, fromBase: (v) => v },
+ km: { name: 'Kilometers', toBase: (v) => v * 1000, fromBase: (v) => v / 1000 },
+ mi: { name: 'Miles', toBase: (v) => v * 1609.344, fromBase: (v) => v / 1609.344 },
+ ft: { name: 'Feet', toBase: (v) => v * 0.3048, fromBase: (v) => v / 0.3048 }
+ },
+ temperature: {
+ C: {
+ name: 'Celsius',
+ toBase: (v) => v,
+ fromBase: (v) => v
+ },
+ F: {
+ name: 'Fahrenheit',
+ toBase: (v) => (v - 32) * 5/9,
+ fromBase: (v) => (v * 9/5) + 32
+ },
+ K: {
+ name: 'Kelvin',
+ toBase: (v) => v - 273.15,
+ fromBase: (v) => v + 273.15
+ }
+ },
+ weight: {
+ g: { name: 'Grams', toBase: (v) => v, fromBase: (v) => v },
+ kg: { name: 'Kilograms', toBase: (v) => v * 1000, fromBase: (v) => v / 1000 },
+ lb: { name: 'Pounds', toBase: (v) => v * 453.592, fromBase: (v) => v / 453.592 },
+ oz: { name: 'Ounces', toBase: (v) => v * 28.3495, fromBase: (v) => v / 28.3495 }
+ }
+};
+
+// State management
+let currentCategory = 'length';
+let isConverting = false;
+
+// DOM elements
+const input1 = document.getElementById('input1');
+const input2 = document.getElementById('input2');
+const unit1 = document.getElementById('unit1');
+const unit2 = document.getElementById('unit2');
+const swapBtn = document.getElementById('swapBtn');
+const categoryBtns = document.querySelectorAll('.category-btn');
+const themeToggle = document.getElementById('themeToggle');
+
+// Load state from localStorage
+function loadState() {
+ const saved = localStorage.getItem('unitConverterState');
+ if (saved) {
+ try {
+ const state = JSON.parse(saved);
+ currentCategory = state.category || 'length';
+ return state;
+ } catch (e) {
+ return null;
+ }
+ }
+ return null;
+}
+
+// Save state to localStorage
+function saveState() {
+ const state = {
+ category: currentCategory,
+ unit1: unit1.value,
+ unit2: unit2.value
+ };
+ localStorage.setItem('unitConverterState', JSON.stringify(state));
+}
+
+// Load theme from localStorage
+function loadTheme() {
+ const theme = localStorage.getItem('unitConverterTheme') || 'light';
+ document.documentElement.setAttribute('data-theme', theme);
+ themeToggle.textContent = theme === 'dark' ? '☀️' : '🌙';
+}
+
+// Toggle between light and dark theme
+function toggleTheme() {
+ const currentTheme = document.documentElement.getAttribute('data-theme');
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
+ document.documentElement.setAttribute('data-theme', newTheme);
+ localStorage.setItem('unitConverterTheme', newTheme);
+ themeToggle.textContent = newTheme === 'dark' ? '☀️' : '🌙';
+}
+
+// Populate unit dropdowns based on current category
+function populateUnits() {
+ const categoryUnits = units[currentCategory];
+ const unitKeys = Object.keys(categoryUnits);
+
+ unit1.innerHTML = '';
+ unit2.innerHTML = '';
+
+ unitKeys.forEach(key => {
+ const option1 = document.createElement('option');
+ option1.value = key;
+ option1.textContent = categoryUnits[key].name;
+ unit1.appendChild(option1);
+
+ const option2 = document.createElement('option');
+ option2.value = key;
+ option2.textContent = categoryUnits[key].name;
+ unit2.appendChild(option2);
+ });
+
+ const saved = loadState();
+ if (saved && saved.category === currentCategory) {
+ if (unitKeys.includes(saved.unit1)) unit1.value = saved.unit1;
+ if (unitKeys.includes(saved.unit2)) unit2.value = saved.unit2;
+ } else {
+ unit1.value = unitKeys[0];
+ unit2.value = unitKeys[1] || unitKeys[0];
+ }
+}
+
+// Convert units bi-directionally
+function convert(fromInput, toInput) {
+ if (isConverting) return;
+
+ const value = parseFloat(fromInput.value);
+ if (isNaN(value) || fromInput.value === '') {
+ toInput.value = '';
+ return;
+ }
+
+ const fromUnit = fromInput === input1 ? unit1.value : unit2.value;
+ const toUnit = fromInput === input1 ? unit2.value : unit1.value;
+
+ const categoryUnits = units[currentCategory];
+
+ // Convert to base unit, then to target unit
+ const baseValue = categoryUnits[fromUnit].toBase(value);
+ const result = categoryUnits[toUnit].fromBase(baseValue);
+
+ isConverting = true;
+ toInput.value = parseFloat(result.toFixed(6)).toString();
+ isConverting = false;
+}
+
+// Switch between categories (Length, Temperature, Weight)
+function switchCategory(category) {
+ currentCategory = category;
+
+ categoryBtns.forEach(btn => {
+ btn.classList.toggle('active', btn.dataset.category === category);
+ });
+
+ input1.value = '';
+ input2.value = '';
+
+ populateUnits();
+ saveState();
+}
+
+// Swap units and values
+function swapUnits() {
+ const tempValue = input1.value;
+ const tempUnit = unit1.value;
+
+ input1.value = input2.value;
+ unit1.value = unit2.value;
+
+ input2.value = tempValue;
+ unit2.value = tempUnit;
+
+ saveState();
+}
+
+// Event listeners
+categoryBtns.forEach(btn => {
+ btn.addEventListener('click', () => {
+ switchCategory(btn.dataset.category);
+ });
});
+
+input1.addEventListener('input', () => convert(input1, input2));
+input2.addEventListener('input', () => convert(input2, input1));
+
+unit1.addEventListener('change', () => {
+ convert(input1, input2);
+ saveState();
+});
+
+unit2.addEventListener('change', () => {
+ convert(input1, input2);
+ saveState();
+});
+
+swapBtn.addEventListener('click', swapUnits);
+themeToggle.addEventListener('click', toggleTheme);
+
+// Initialize app
+loadTheme();
+const saved = loadState();
+if (saved) {
+ switchCategory(saved.category);
+} else {
+ populateUnits();
+}
diff --git a/projects/unit-converter/styles.css b/projects/unit-converter/styles.css
index af845c5..2c90834 100644
--- a/projects/unit-converter/styles.css
+++ b/projects/unit-converter/styles.css
@@ -1,17 +1,260 @@
-/* TODO: Style the unit converter form and result area */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
:root {
- --bg: #0a0f1d;
- --fg: #e2e8f0;
- --accent: #60a5fa;
+ --bg-gradient-1: #667eea;
+ --bg-gradient-2: #764ba2;
+ --card-bg: #ffffff;
+ --text-primary: #1a1a2e;
+ --text-secondary: #6b7280;
+ --input-bg: #f3f4f6;
+ --input-border: #e5e7eb;
+ --input-focus: #667eea;
+ --accent-primary: #667eea;
+ --accent-hover: #5568d3;
+ --shadow: rgba(0, 0, 0, 0.1);
+ --shadow-lg: rgba(0, 0, 0, 0.2);
+}
+
+[data-theme="dark"] {
+ --bg-gradient-1: #1a1a2e;
+ --bg-gradient-2: #16213e;
+ --card-bg: #0f172a;
+ --text-primary: #f1f5f9;
+ --text-secondary: #94a3b8;
+ --input-bg: #1e293b;
+ --input-border: #334155;
+ --input-focus: #818cf8;
+ --accent-primary: #818cf8;
+ --accent-hover: #6366f1;
+ --shadow: rgba(0, 0, 0, 0.3);
+ --shadow-lg: rgba(0, 0, 0, 0.5);
}
-* { box-sizing: border-box; }
+
body {
- margin: 0;
- font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
- background: var(--bg);
- color: var(--fg);
- min-height: 100vh;
- display: grid;
- place-items: center;
-}
-#app { width: min(92vw, 720px); }
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background: linear-gradient(135deg, var(--bg-gradient-1) 0%, var(--bg-gradient-2) 100%);
+ min-height: 100vh;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 20px;
+ transition: background 0.3s ease;
+}
+
+.container {
+ background: var(--card-bg);
+ border-radius: 24px;
+ box-shadow: 0 20px 60px var(--shadow-lg);
+ padding: 40px;
+ max-width: 520px;
+ width: 100%;
+ transition: all 0.3s ease;
+}
+
+.header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 30px;
+}
+
+h1 {
+ color: var(--text-primary);
+ font-size: 32px;
+ font-weight: 700;
+ letter-spacing: -0.5px;
+}
+
+.theme-toggle {
+ background: var(--input-bg);
+ border: 2px solid var(--input-border);
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 22px;
+ transition: all 0.3s ease;
+}
+
+.theme-toggle:hover {
+ background: var(--accent-primary);
+ border-color: var(--accent-primary);
+ transform: rotate(180deg);
+}
+
+.category-selector {
+ display: flex;
+ gap: 12px;
+ margin-bottom: 32px;
+ justify-content: center;
+ flex-wrap: wrap;
+}
+
+.category-btn {
+ padding: 12px 24px;
+ border: 2px solid var(--accent-primary);
+ background: transparent;
+ color: var(--accent-primary);
+ border-radius: 30px;
+ cursor: pointer;
+ font-size: 15px;
+ font-weight: 600;
+ transition: all 0.3s ease;
+ font-family: 'Inter', sans-serif;
+}
+
+.category-btn:hover {
+ background: var(--accent-primary);
+ color: white;
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px var(--shadow);
+}
+
+.category-btn.active {
+ background: var(--accent-primary);
+ color: white;
+ box-shadow: 0 4px 12px var(--shadow);
+}
+
+.converter-box {
+ background: var(--input-bg);
+ border-radius: 20px;
+ padding: 30px;
+ margin-bottom: 20px;
+ border: 1px solid var(--input-border);
+}
+
+.input-group {
+ margin-bottom: 16px;
+}
+
+.input-group label {
+ display: block;
+ margin-bottom: 10px;
+ color: var(--text-secondary);
+ font-weight: 600;
+ font-size: 14px;
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+.input-wrapper {
+ display: flex;
+ gap: 12px;
+}
+
+input[type="number"] {
+ flex: 1;
+ padding: 14px 16px;
+ border: 2px solid var(--input-border);
+ border-radius: 12px;
+ font-size: 16px;
+ font-family: 'Inter', sans-serif;
+ background: var(--card-bg);
+ color: var(--text-primary);
+ transition: all 0.3s ease;
+}
+
+input[type="number"]:focus {
+ outline: none;
+ border-color: var(--input-focus);
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+}
+
+select {
+ padding: 14px 16px;
+ border: 2px solid var(--input-border);
+ border-radius: 12px;
+ font-size: 16px;
+ font-family: 'Inter', sans-serif;
+ background: var(--card-bg);
+ color: var(--text-primary);
+ cursor: pointer;
+ min-width: 110px;
+ transition: all 0.3s ease;
+ font-weight: 500;
+}
+
+select:focus {
+ outline: none;
+ border-color: var(--input-focus);
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+}
+
+.swap-container {
+ display: flex;
+ justify-content: center;
+ margin: 24px 0;
+}
+
+.swap-btn {
+ background: linear-gradient(135deg, var(--accent-primary), var(--accent-hover));
+ color: white;
+ border: none;
+ width: 56px;
+ height: 56px;
+ border-radius: 50%;
+ cursor: pointer;
+ font-size: 26px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
+ box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4);
+}
+
+.swap-btn:hover {
+ transform: rotate(180deg) scale(1.1);
+ box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5);
+}
+
+.swap-btn:active {
+ transform: rotate(180deg) scale(0.95);
+}
+
+@media (max-width: 600px) {
+ .container {
+ padding: 28px;
+ }
+
+ h1 {
+ font-size: 26px;
+ }
+
+ .theme-toggle {
+ width: 44px;
+ height: 44px;
+ font-size: 20px;
+ }
+
+ .category-btn {
+ padding: 10px 20px;
+ font-size: 14px;
+ }
+
+ .input-wrapper {
+ flex-direction: column;
+ }
+
+ select {
+ width: 100%;
+ }
+}
+
+/* Remove spinner from number input */
+input[type="number"]::-webkit-inner-spin-button,
+input[type="number"]::-webkit-outer-spin-button {
+ -webkit-appearance: none;
+ margin: 0;
+}
+
+input[type="number"] {
+ -moz-appearance : textfield;
+}