commit 7dbdab4ee81b808a32a2ca88feb3c18cf17c15a2 Author: dev Date: Sat Nov 15 18:45:58 2025 +0100 Initial commit diff --git a/index.html b/index.html new file mode 100644 index 0000000..8b32341 --- /dev/null +++ b/index.html @@ -0,0 +1,81 @@ + + + + + Stitches and Rows + + + + + + + + +

+ Stiches and Rows! +

+ + +
+

Settings

+
+ + +
+
+ + + +
+ +
+ +
+
+
+ + +
+ +
+ + +
+
+
+ +
+

+ About this Site +

+

+ This is Stiches and Rows, made by Honey. +

+

+ You can support this projects by buying me a Starbucks and giving luvs +

+ +

+ | + +

+
+ + + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..5b2d140 --- /dev/null +++ b/script.js @@ -0,0 +1,167 @@ +const MAX_HISTORY_LENGTH = 32; +const PIP_WINDOW_SIZE = { width: 280, height: 140 }; + +class Counter { + constructor(elementId, storageKey) { + this.el = document.getElementById(elementId); + this.key = storageKey; + this.count = parseInt(localStorage.getItem(this.key)) || 0; + this.history = []; + this.render(); + } + + render = () => { + this.el.textContent = this.count; + } + + set = (val) => { + this.history.push(this.count); + if (this.history.length > MAX_HISTORY_LENGTH) this.history.shift(); + this.count = val; + localStorage.setItem(this.key, this.count); + this.render(); + } + + increase = () => { + this.set(this.count + 1); + } + + undo = () => { + if (this.history.length > 0) { + this.count = this.history.pop(); + localStorage.setItem(this.key, this.count); + this.render(); + } + } +} + +const stitches = new Counter("stitches-counter", "stitches"); +const rows = new Counter("rows-counter", "rows"); + +const inputAuto = document.getElementById("input-autoincrease"); +const inputStitchMax = document.getElementById("input-stitches"); + +inputAuto.checked = localStorage.getItem("input_autoincrease") === "true"; +inputStitchMax.value = localStorage.getItem("input_stitches") || "10"; + +inputAuto.addEventListener("change", () => { + localStorage.setItem("input_autoincrease", inputAuto.checked); +}); + +inputStitchMax.addEventListener("change", () => { + localStorage.setItem("input_stitches", inputStitchMax.value); +}); + +document.getElementById("button-stitch").addEventListener("click", () => { + const max = parseInt(inputStitchMax.value); + if (inputAuto.checked && stitches.count + 1 >= max) { + rows.increase(); + stitches.set(0); + } else { + stitches.increase(); + } +}); + +document.getElementById("button-row").addEventListener("click", () => { + rows.increase(); + stitches.set(0); +}); + +document.getElementById("button-undo").addEventListener("click", () => { + stitches.undo(); + rows.undo(); +}); + +document.getElementById("button-reset").addEventListener("click", () => { + stitches.set(0); + rows.set(0); +}); + +class PipManager { + constructor(pipElement, pipContainer, pipButton) { + this.el = pipElement; + this.container = pipContainer; + this.button = pipButton; + this.pipWindow = null; + + this.button.addEventListener("click", this.start); + } + + start = async () => { + if (this.pipWindow && !this.pipWindow.closed) return; + + this.pipWindow = await documentPictureInPicture.requestWindow(PIP_WINDOW_SIZE); + + for (const styleSheet of document.styleSheets) { + try { + const style = document.createElement("style"); + const rules = Array.from(styleSheet.cssRules).map(r => r.cssText).join(""); + style.textContent = rules; + this.pipWindow.document.head.appendChild(style); + } catch { + if (styleSheet.href) { + const link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = styleSheet.href; + this.pipWindow.document.head.appendChild(link); + } + } + } + + this.pipWindow.document.body.appendChild(this.el); + + // Add keydown listener inside PiP window + this.pipWindow.addEventListener("keydown", (event) => { + // Ignore if focused on input or textarea in PiP window + const active = this.pipWindow.document.activeElement; + if (active && (active.tagName === "INPUT" || active.tagName === "TEXTAREA")) return; + + if (event.code === "Space") { + event.preventDefault(); + const max = parseInt(inputStitchMax.value); + if (inputAuto.checked && stitches.count + 1 >= max) { + rows.increase(); + stitches.set(0); + } else { + stitches.increase(); + } + } + }); + + this.pipWindow.addEventListener("pagehide", () => this.stop()); + } + + stop = () => { + this.container.appendChild(this.el); + this.pipWindow = null; + } +} + +new PipManager( + document.getElementById("div-pip-element"), + document.getElementById("div-pip-container"), + document.getElementById("button-start-pip") +); + +const notesField = document.getElementById("notes"); +notesField.value = localStorage.getItem("notes") || ""; +notesField.addEventListener("input", () => { + localStorage.setItem("notes", notesField.value); +}); + +window.addEventListener("keydown", (event) => { + // Ignore if focused on input or textarea, so typing space doesn't increment stitches accidentally + const active = document.activeElement; + if (active && (active.tagName === "INPUT" || active.tagName === "TEXTAREA")) return; + + if (event.code === "Space") { + event.preventDefault(); // prevent page scroll on space key + const max = parseInt(inputStitchMax.value); + if (inputAuto.checked && stitches.count + 1 >= max) { + rows.increase(); + stitches.set(0); + } else { + stitches.increase(); + } + } +}); \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..626a4fa --- /dev/null +++ b/style.css @@ -0,0 +1,307 @@ +:root { + --color-primary: rgb(230, 134, 78); + --color-secondary: rgb(236, 233, 61); /* banana yellow */ + --card-bg: rgba(255, 245, 230, 0.8); + --text-dark: #333; + --border-soft: rgba(0, 0, 0, 0.05); + --radius: 1.25rem; + --shadow: 0 6px 24px rgba(0, 0, 0, 0.1); +} + +body { + margin: 0; + padding: 2rem 1rem; + font-family: 'Roboto', sans-serif; + background: linear-gradient(135deg, var(--color-primary), var(--color-secondary)); + color: var(--text-dark); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 1.5rem; + min-height: 100vh; + box-sizing: border-box; +} + +#settings-card, #div-pip-element { + width: 100%; + max-width: 360px; + background: rgba(255, 255, 255, 0.15); + border-radius: var(--radius); + box-shadow: var(--shadow); + padding: 1.5rem; + border: 1px solid rgba(255, 255, 255, 0.2); + backdrop-filter: blur(12px); + box-sizing: border-box; +} + +#notes { + width: 100%; + max-width: 360px; + background: var(--card-bg); + border-radius: var(--radius); + box-shadow: var(--shadow); + padding: 1.5rem; + border: 1px solid var(--border-soft); + backdrop-filter: blur(8px); + box-sizing: border-box; +} + +#notes { + min-height: 100px; + font-size: 1rem; + resize: vertical; + outline: none; + border: none; + font-family: inherit; + background: var(--card-bg); + color: var(--text-dark); +} + +#notes::placeholder { + color: rgba(51, 51, 51, 0.7); +} + +#settings-card h2 { + font-size: 1.2rem; + margin-bottom: 1rem; + text-align: center; + font-weight: 600; + color: var(--text-dark); +} + +.settings-group { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.settings-group label { + font-size: 0.95rem; + font-weight: 500; + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.settings-group input[type="number"] { + padding: 0.5rem; + border-radius: 0.5rem; + border: 1px solid #aaa; + text-align: center; + font-size: 1rem; +} + +.toggle { + display: flex; + justify-content: space-between; + align-items: center; + flex-direction: row !important; +} + +.toggle input { + display: none; +} + +.toggle .slider { + width: 42px; + height: 24px; + background-color: #ccc; + border-radius: 999px; + position: relative; + cursor: pointer; + transition: background-color 0.2s; +} + +.toggle .slider::before { + content: ""; + position: absolute; + left: 2px; + top: 2px; + width: 20px; + height: 20px; + background: white; + border-radius: 50%; + transition: transform 0.2s; +} + +.toggle input:checked + .slider { + background-color: var(--color-primary); +} + +.toggle input:checked + .slider::before { + transform: translateX(18px); +} + +#pip-button-container { + position: absolute; + top: 1rem; + right: 1rem; +} + +#button-start-pip { + background: none; + border: none; + font-size: 2rem; + cursor: pointer; +} + +#div-pip-container { + width: 100%; + max-width: 360px; +} + +#div-pip-element { + display: flex; + flex-direction: column; + align-items: center; + gap: 1.5rem; + min-width: 200px; /* smaller min width */ + min-height: 100px; /* smaller min height */ +} + +.counter-buttons { + display: flex; + gap: 1.5rem; + width: 100%; + justify-content: center; +} + +.counter-buttons button { + background-color: var(--color-primary); + color: black; + border: none; + padding: 1.5rem; + border-radius: 1rem; + font-size: 1.2rem; + cursor: pointer; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + box-shadow: var(--shadow); + width: 120px; + height: 80px; + flex-shrink: 0; +} + +.control-buttons { + display: flex; + gap: 1.5rem; + width: 100%; + justify-content: center; +} + +.control-buttons button { + background-color: var(--color-primary); + color: black; + border: none; + padding: 0.75rem; + border-radius: 0.75rem; + font-size: 1rem; + cursor: pointer; + box-shadow: var(--shadow); + width: 120px; + height: 45px; + flex-shrink: 0; +} + +@media (display-mode: picture-in-picture) { + body { + background: linear-gradient(135deg, var(--color-primary), var(--color-secondary)); + justify-content: center; + padding: 1rem; + margin: 0; + } + + #pip-button-container, + #settings-card, + #notes { + display: none; + } + + #div-pip-element { + background: rgba(255, 255, 255, 0.2); + border-radius: 1rem; + padding: 1rem; + backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.3); + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); + width: 100%; + max-width: none; + } + + .counter-buttons { + gap: 1rem; + } + + .counter-buttons button { + width: 80px; + height: 60px; + font-size: 1rem; + padding: 0.5rem; + } + + .counter-buttons button div:first-child { + font-size: 1.4rem; + margin-bottom: 0.2rem; + } + + .counter-buttons button div:last-child { + font-size: 1rem; + opacity: 0.8; + } + + .control-buttons { + gap: 1rem; + } + + .control-buttons button { + width: 80px; + height: 30px; + font-size: 0.85rem; + padding: 0.5rem; + } +} + + +#div-button-help { + position: fixed; + bottom: 2vh; + right: 2vw; +} + +#button-help { + color: var(--color-primary); + font-size: 4vh; + text-shadow: 0px 0px 2px black; + background: transparent; + border: none; + padding: 1vh 1vw; +} + +.icon-link { + font-size: 2vh; + color: black; + text-decoration: none; +} + +#about { + text-align: center; + max-width: 400px; + margin: 0 auto; +} + + +/* Subtle hover effect */ +button:hover { + filter: brightness(1.03); /* Slightly lighter */ + transform: scale(1.01); /* Barely grows */ + transition: transform 0.12s ease, filter 0.12s ease; +} + +/* Subtle press (active) effect */ +button:active { + transform: scale(0.985); /* Slight shrink */ + filter: brightness(0.98); /* Slightly darker */ +}