From c36ca506df430fc2bc8c16f45f12b6d81583bc06 Mon Sep 17 00:00:00 2001 From: zoe2276 Date: Wed, 10 Dec 2025 08:02:28 -0600 Subject: [PATCH 01/11] trying to get the login page to work. having input binding issues --- .env | 1 + .env.development | 1 + environment.d.ts | 9 +++ package-lock.json | 34 +++++++++ package.json | 2 + src/components/CRTContainer.vue | 11 ++- src/components/CursorGlow.vue | 1 + src/components/LoginField.vue | 124 ++++++++++++++++++++++++++++++++ src/components/Navigation.vue | 40 +++++++---- src/composables/localApi.ts | 35 +++++++++ src/composables/typewriter.ts | 23 ++++++ src/pages/Home.vue | 15 ++-- src/pages/Login.vue | 45 ++++++++++++ src/router.js | 5 +- tsconfig.app.json | 17 +++++ tsconfig.json | 12 ++++ tsconfig.node.json | 27 +++++++ 17 files changed, 379 insertions(+), 23 deletions(-) create mode 100644 .env create mode 100644 .env.development create mode 100644 environment.d.ts create mode 100644 src/components/LoginField.vue create mode 100644 src/composables/localApi.ts create mode 100644 src/composables/typewriter.ts create mode 100644 src/pages/Login.vue create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json diff --git a/.env b/.env new file mode 100644 index 0000000..12006df --- /dev/null +++ b/.env @@ -0,0 +1 @@ +VITE_ROOT = "https://auth.zoe.rip" \ No newline at end of file diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..196cf4c --- /dev/null +++ b/.env.development @@ -0,0 +1 @@ +VITE_ROOT = "127.0.0.1" \ No newline at end of file diff --git a/environment.d.ts b/environment.d.ts new file mode 100644 index 0000000..28bbc81 --- /dev/null +++ b/environment.d.ts @@ -0,0 +1,9 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + VITE_ROOT : "127.0.0.1:8099" | "https://auth.zoe.rip" + } + } +} + +export {} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3c36ed5..c1c04d5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,11 +8,13 @@ "name": "zoe-rip", "version": "0.0.0", "dependencies": { + "typescript": "^5.9.3", "vue": "^3.5.18", "vue-router": "^4.6.3" }, "devDependencies": { "@vitejs/plugin-vue": "^6.0.1", + "@vue/tsconfig": "^0.8.1", "vite": "^7.1.2" } }, @@ -941,6 +943,25 @@ "integrity": "sha512-+2k1EQpnYuVuu3N7atWyG3/xoFWIVJZq4Mz8XNOdScFI0etES75fbny/oU4lKWk/577P1zmg0ioYvpGEDZ3DLw==", "license": "MIT" }, + "node_modules/@vue/tsconfig": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@vue/tsconfig/-/tsconfig-0.8.1.tgz", + "integrity": "sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "5.x", + "vue": "^3.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -1181,6 +1202,19 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/vite": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz", diff --git a/package.json b/package.json index 6075e92..39614a4 100644 --- a/package.json +++ b/package.json @@ -9,11 +9,13 @@ "preview": "vite preview" }, "dependencies": { + "typescript": "^5.9.3", "vue": "^3.5.18", "vue-router": "^4.6.3" }, "devDependencies": { "@vitejs/plugin-vue": "^6.0.1", + "@vue/tsconfig": "^0.8.1", "vite": "^7.1.2" } } diff --git a/src/components/CRTContainer.vue b/src/components/CRTContainer.vue index c1208d6..f30c827 100644 --- a/src/components/CRTContainer.vue +++ b/src/components/CRTContainer.vue @@ -9,17 +9,24 @@ + + \ No newline at end of file diff --git a/src/components/Navigation.vue b/src/components/Navigation.vue index 1d65799..62966b0 100644 --- a/src/components/Navigation.vue +++ b/src/components/Navigation.vue @@ -1,5 +1,5 @@ \ No newline at end of file diff --git a/src/composables/localApi.ts b/src/composables/localApi.ts new file mode 100644 index 0000000..fe58895 --- /dev/null +++ b/src/composables/localApi.ts @@ -0,0 +1,35 @@ +export interface ApiParameters { + headers?: Headers + body?: string +} + +async function callApi(endpoint: string, params: ApiParameters): Promise { + const res = await fetch(`${import.meta.env.VITE_BASE_URL}${endpoint}`, params) + + if (!res.ok) throw new Error(res.statusText) + return await res.json() as T +} + +export const register = async (username: string, password: string, email: string, role: string = "user") => { + const registrationObj = { + username: username, + password: password, + email: email, + role: role + } + + return await callApi<{ message: string }>("/api/register", { + body: JSON.stringify(registrationObj) + }) +} + +export const login = async (username: string, password: string) => { + const credsObj = { + username: username, + password: password + } + + return await callApi<{ token: string, expiration: string}>("/api/token", { + body: JSON.stringify(credsObj) + }) +} \ No newline at end of file diff --git a/src/composables/typewriter.ts b/src/composables/typewriter.ts new file mode 100644 index 0000000..d218e4f --- /dev/null +++ b/src/composables/typewriter.ts @@ -0,0 +1,23 @@ +export const typeEffect = (selector: string, charsPerSecond: number = 20, delay: number = 6) => { + const targets = document.querySelectorAll(`${selector}:not(.shown)`) + Array.from(targets).forEach(el => { + const charCount = el.innerHTML.length + const typeDuration = charCount / charsPerSecond + const styleString = `animation: type ${typeDuration}s steps(${charCount}, end) ${delay}s, grow 1ms steps(1, end) ${delay}s;` + el.setAttribute("style", styleString) + setTimeout(() => el.classList.add("shown"), delay * 1001) + delay += typeDuration + }) +} + +export const backspaceEffect = (selector: string, charsPerSecond: number = 20, delay: number = 6) => { + const targets = document.querySelectorAll(`${selector}.shown`) + Array.from(targets).forEach(el => { + const charCount = el.innerHTML.length + const typeDuration = charCount / charsPerSecond + const styleString = `animation: backspace ${typeDuration}s steps(${charCount}, end) ${delay}s, shrink 1ms steps(1, end) ${delay}s;` + el.setAttribute("style", styleString) + setTimeout(() => el.classList.remove("shown"), delay * 1001) + delay += typeDuration + }) +} \ No newline at end of file diff --git a/src/pages/Home.vue b/src/pages/Home.vue index f721109..417a89b 100644 --- a/src/pages/Home.vue +++ b/src/pages/Home.vue @@ -35,10 +35,6 @@ const asciiTitle = ` ` const title = ref({ bin: binaryTitle, ascii: asciiTitle }) -const appendClass = (selector, className, delay = 0, first) => { - (first ? [document.querySelector(`${selector}:not(.${className})`)].filter(Boolean) : document.querySelectorAll(selector)).forEach(e => setTimeout(() => e.classList.add(className), delay)) -} - const initSiteTitle = () => { const e = document.querySelector(".asciiTitle") setTimeout(() => e.innerHTML = title.value.bin, 2000) @@ -48,7 +44,6 @@ const initSiteTitle = () => { // set up "initializing..." effect onMounted(() => { initSiteTitle() - appendClass(".crtContainer", "vignette") }) @@ -58,11 +53,21 @@ onMounted(() => { to { width: 100% } } +@keyframes backspace { + from { width: 100% } + to { width: 0; } +} + @keyframes grow { from { height: 0 } to { height: auto } } +@keyframes grow { + from { height: auto } + to { height: 0 } +} + @keyframes slideUp { from { max-height: 100vh } to { max-height: 0 } diff --git a/src/pages/Login.vue b/src/pages/Login.vue new file mode 100644 index 0000000..49ba155 --- /dev/null +++ b/src/pages/Login.vue @@ -0,0 +1,45 @@ + + + + + \ No newline at end of file diff --git a/src/router.js b/src/router.js index aefba7a..87da64b 100644 --- a/src/router.js +++ b/src/router.js @@ -3,12 +3,13 @@ import { createWebHistory, createRouter } from "vue-router"; import Home from "./pages/Home.vue"; import About from "./pages/About.vue"; import Projects from "./pages/Projects.vue"; +import Login from "./pages/Login.vue"; const routes = [ { path: "/", component: Home }, { path: "/about", component: About }, - { path: "/projects", component: Projects} // , - // { path: "/login", component: Login} + { path: "/projects", component: Projects}, + { path: "/login", component: Login} ] const router = createRouter({ diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..ad07a8b --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,17 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "types": ["vite/client"], + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "composite": true + }, + "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ca0fbf9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "types": ["node"], + "typeRoots": ["./node_modules/@types", "./types"] + }, + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" }, + ], + "include": [ "./environment.d.ts" ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..2b65708 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "composite": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} From 1d3d12fc90620b319585119d2ce3de7de0db9a6e Mon Sep 17 00:00:00 2001 From: zoe2276 Date: Wed, 10 Dec 2025 18:53:47 -0600 Subject: [PATCH 02/11] added cursor blink --- src/components/LoginField.vue | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/LoginField.vue b/src/components/LoginField.vue index bfae678..504b54c 100644 --- a/src/components/LoginField.vue +++ b/src/components/LoginField.vue @@ -72,6 +72,13 @@ const updateCaretPos = () => { } + + + - \ No newline at end of file diff --git a/src/components/Counter.vue b/src/components/Counter.vue deleted file mode 100644 index 7b48658..0000000 --- a/src/components/Counter.vue +++ /dev/null @@ -1,27 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/composables/typewriter.ts b/src/composables/typewriter.ts index d218e4f..689312f 100644 --- a/src/composables/typewriter.ts +++ b/src/composables/typewriter.ts @@ -1,6 +1,6 @@ export const typeEffect = (selector: string, charsPerSecond: number = 20, delay: number = 6) => { const targets = document.querySelectorAll(`${selector}:not(.shown)`) - Array.from(targets).forEach(el => { + Array.from(targets).forEach((el: Element) => { const charCount = el.innerHTML.length const typeDuration = charCount / charsPerSecond const styleString = `animation: type ${typeDuration}s steps(${charCount}, end) ${delay}s, grow 1ms steps(1, end) ${delay}s;` @@ -12,7 +12,7 @@ export const typeEffect = (selector: string, charsPerSecond: number = 20, delay: export const backspaceEffect = (selector: string, charsPerSecond: number = 20, delay: number = 6) => { const targets = document.querySelectorAll(`${selector}.shown`) - Array.from(targets).forEach(el => { + Array.from(targets).forEach((el: Element) => { const charCount = el.innerHTML.length const typeDuration = charCount / charsPerSecond const styleString = `animation: backspace ${typeDuration}s steps(${charCount}, end) ${delay}s, shrink 1ms steps(1, end) ${delay}s;` diff --git a/src/pages/About.vue b/src/pages/About.vue index 101ad24..1cff855 100644 --- a/src/pages/About.vue +++ b/src/pages/About.vue @@ -1,12 +1,12 @@ \ No newline at end of file From 984623d69954f7f4ec875c411f16053dd5e26c11 Mon Sep 17 00:00:00 2001 From: zoe2276 Date: Sat, 13 Dec 2025 14:13:48 -0600 Subject: [PATCH 07/11] caret redefined as composable. need to adjust for login forms still --- src/components/Console.vue | 164 +++++------------ src/components/LoginField.vue | 92 +++------- src/components/Navigation.vue | 13 +- src/composables/caret.ts | 324 ++++++++++++++++++++++++++++++++++ src/pages/Login.vue | 4 +- 5 files changed, 403 insertions(+), 194 deletions(-) create mode 100644 src/composables/caret.ts diff --git a/src/components/Console.vue b/src/components/Console.vue index 296e4a8..198837a 100644 --- a/src/components/Console.vue +++ b/src/components/Console.vue @@ -1,49 +1,49 @@ - border-right: 0.66rem solid #42b983ee; - pointer-events: none; - height: 1.25rem; - /* background-color: #42b983ee; */ + \ No newline at end of file diff --git a/src/components/LoginField.vue b/src/components/LoginField.vue index 504b54c..2585834 100644 --- a/src/components/LoginField.vue +++ b/src/components/LoginField.vue @@ -3,15 +3,21 @@ username: - -
+ +
- password: + - -
+ +
@@ -19,8 +25,19 @@ - - \ No newline at end of file diff --git a/src/components/Navigation.vue b/src/components/Navigation.vue index 62966b0..a84bd0d 100644 --- a/src/components/Navigation.vue +++ b/src/components/Navigation.vue @@ -1,17 +1,19 @@ @@ -22,6 +24,7 @@ import { RouterLink, useRoute } from 'vue-router' import { typeEffect } from '../composables/typewriter' const props = defineProps(["position"]) +const emit = defineEmits(['handleClick']) const isLoggedIn = window.localStorage.getItem("jwtAuth") const setToTop = (positionProp) => { diff --git a/src/composables/caret.ts b/src/composables/caret.ts new file mode 100644 index 0000000..9392529 --- /dev/null +++ b/src/composables/caret.ts @@ -0,0 +1,324 @@ +import { ref, computed, nextTick, onMounted, onUnmounted, type Ref, watch } from "vue" + +export interface UseCaretOptions { + target: Ref + caret: Ref + thickness?: number + color?: string + blinkInterval?: number + hideNativeCaret?: boolean + useCanvasMeasure?: boolean +} + +export interface UseCaretResult { + caretVisible: Ref + caretStyle: ReturnType + update: () => void + scheduleUpdate: () => void + startBlink: () => void + stopBlink: () => void + dispose: () => void +} + +export const useCaret = (options: UseCaretOptions): UseCaretResult => { + const { + target, caret, + thickness = 10, + color = "#42b983aa", + blinkInterval = 500, + hideNativeCaret = true, + useCanvasMeasure = false + } = options + + const caretVisible = ref(true) + const x = ref(0) + const y = ref(0) + const height = ref(16) + + let mirrorEl: HTMLDivElement | null = null + let blinkTimer: number | null = null + let listenersBound = false + let savedCaretColor: string | null = null + let isComposing = false + + const caretStyle = computed({ + get: () => ({ + position: "absolute", + width: "0px", + borderRight: `${thickness}px solid ${color}`, + left: `${x.value}px`, + top: `${y.value}px`, + height: `${height.value}px`, + opacity: caretVisible ? 1 : 0, + pointerEvents: "none" + }), set: () => {}}) + + const assertMirror = (): HTMLDivElement => { + if (mirrorEl) return mirrorEl + mirrorEl = document.createElement("div") + mirrorEl.style.position = "absolute" + mirrorEl.style.left = "-99999px" + mirrorEl.style.top = "-99999px" + mirrorEl.style.whiteSpace = "pre" + mirrorEl.style.visibility = "hidden" + mirrorEl.style.pointerEvents = "none" + document.body.appendChild(mirrorEl) + return mirrorEl + } + + const canvasMeasureWidth = (text: string, computedStyle: CSSStyleDeclaration): number => { + const canvas = document.createElement("canvas") + const cx = canvas.getContext("2d") + if (!cx) return 0 + const weight = computedStyle.fontWeight || "normal" + const size = computedStyle.fontSize || "1rem" + const family = computedStyle.fontFamily || "'Courier New', Courier, monospace" + const style = computedStyle.fontStyle || "normal" + cx.font = `${style} ${weight} ${size} ${family}` + return cx.measureText(text).width + } + + const measureInputCaret = (input: HTMLInputElement) => { + const selStart = input.selectionStart ?? 0 + const computedStyle = getComputedStyle(input) + + const borderTop = parseFloat(computedStyle.borderTopWidth || "0") || 0 + const paddingTop = parseFloat(computedStyle.paddingTop || "0") || 0 + const paddingBottom = parseFloat(computedStyle.paddingBottom || "0") || 0 + let lineH = parseFloat(computedStyle.lineHeight || "0") + if (!Number.isFinite(lineH) || !lineH) { + lineH = input.clientHeight - paddingTop - paddingBottom + } + height.value = Math.max(1, Math.round(lineH)) + y.value = Math.round(borderTop + paddingTop) + + const borderLeft = parseFloat(computedStyle.borderLeftWidth || "0") || 0 + const paddingLeft = parseFloat(computedStyle.paddingLeft || "0") || 0 + const paddingRight = parseFloat(computedStyle.paddingRight || "0") || 0 + const scrollLeft = input.scrollLeft || 0 // helps us stay in visual bounds + + const textUpToCaret = input.value.slice(0, selStart) + let textWidth = 0 + + if (useCanvasMeasure) { // less precise but i may be able to use it over the old mirror el method + textWidth = canvasMeasureWidth(textUpToCaret, computedStyle) + } else { + const mr = assertMirror() + mr.style.fontFamily = computedStyle.fontFamily + mr.style.fontSize = computedStyle.fontSize + mr.style.fontWeight = computedStyle.fontWeight + mr.style.fontStyle = computedStyle.fontStyle + mr.style.letterSpacing = computedStyle.letterSpacing + mr.style.textTransform = computedStyle.textTransform + mr.style.textRendering = "optimizeLegibility" + mr.textContent = textUpToCaret + textWidth = mr.offsetWidth + } + + let caretX = borderLeft + paddingLeft + textWidth - scrollLeft + + // clamp to visible area + const maxX = input.clientWidth - (parseFloat(computedStyle.borderRightWidth || "0") || 0) - paddingRight - 1 + const minX = borderLeft + paddingLeft + caretX = Math.max(minX, Math.min(maxX, caretX)) + + x.value = Math.round(caretX) + } + + const update = () => { + const el = target.value + const cr = caret.value + if (!el || !cr) return + if (isComposing) return // kinda debounce + + measureInputCaret(el) + } + + const scheduleUpdate = () => requestAnimationFrame(update) + + const startBlink = () => { + console.log("startBlink") + // caretVisible.value = true + // if (blinkTimer) { + // clearInterval(blinkTimer) + // blinkTimer = null + // } + // blinkTimer = window.setInterval(() => caretVisible.value = !caretVisible.value, blinkInterval) + caret.value?.classList.add("blinking") + } + + const stopBlink = () => { + console.log("stopBlink") + // if (blinkTimer) { + // clearInterval(blinkTimer) + // blinkTimer = null + // } + // caretVisible.value = false + caret.value?.classList.remove("blinking") + + } + + const bindListeners = (el: HTMLInputElement) => { + if (listenersBound) return + if (hideNativeCaret) { + const computedStyle = getComputedStyle(el) + savedCaretColor = computedStyle.caretColor || null; + (el.style as any).caretColor = "transparent" + } + + el.addEventListener("input", update) + el.addEventListener("keydown", scheduleUpdate) + el.addEventListener("click", update) + el.addEventListener("focus", () => { + startBlink() + scheduleUpdate() + }) + el.addEventListener("blur", () => stopBlink()) + + + // helps for IME input - otherwise it jitters + el.addEventListener('compositionstart', () => isComposing = true) + el.addEventListener('compositionend', () => { + isComposing = false + scheduleUpdate() + }) + + window.addEventListener("resize", update) + listenersBound = true + } + + const unbindListeners = (el: HTMLInputElement) => { + if (!listenersBound) return + + el.removeEventListener("input", update) + el.removeEventListener("keydown", scheduleUpdate) + el.removeEventListener("click", update) + el.removeEventListener("focus", startBlink as EventListener) + el.removeEventListener("blur", stopBlink as EventListener) + el.removeEventListener("compositionstart", () => {}) + el.removeEventListener("compositionend", () => {}) + window.removeEventListener("resize", update) + + // reset the native caret + if (hideNativeCaret) { + if (savedCaretColor !== null) { + (el.style as any).caretColor = savedCaretColor + } else (el.style as any).caretColor = "" + savedCaretColor = null + } + listenersBound = false + } + + const dispose = () => { + const el = target.value + if (el) unbindListeners(el) + if (blinkTimer) { + clearInterval(blinkTimer) + blinkTimer = null + } + if (mirrorEl && mirrorEl.parentNode) { + mirrorEl.parentNode.removeChild(mirrorEl) + mirrorEl = null + } + } + + // handle lifecycle + onMounted(() => { + nextTick(() => { + const el = target.value + if (!el) return + bindListeners(el) + scheduleUpdate() + }) + }) + onUnmounted(() => dispose()) + + // if the target ref changes, we need to rebind + watch(target, (newEl, oldEl) => { + if (oldEl) unbindListeners(oldEl) + if (newEl) { + bindListeners(newEl) + scheduleUpdate() + } + }) + + return { + caretVisible, + caretStyle, + update, + scheduleUpdate, + startBlink, + stopBlink, + dispose + } +} + + +/* old stuff +export const updateCaret = (inputEl: Ref, caretEl: Ref, caretX: Ref): FrameRequestCallback => { + const input = inputEl.value + const caret = caretEl.value + if (!input) console.log("input empty, returning") + if (!caret) console.log("caret empty, returning") + if (!input || !caret) return () => {} + + const selStart = input.selectionStart ?? 0 + + const computedStyles = getComputedStyle(input) + + // copy all styles that affect text width + const mirror = assertMirror(input) + mirror.style.fontFamily = computedStyles.fontFamily + mirror.style.fontSize = computedStyles.fontSize + mirror.style.fontWeight = computedStyles.fontWeight + mirror.style.fontStyle = computedStyles.fontStyle + mirror.style.letterSpacing = computedStyles.letterSpacing + mirror.style.textTransform = computedStyles.textTransform + mirror.style.textRendering = "optimizeLegibility" + mirror.textContent = input.value.slice(0, selStart) + + const textWidth = mirror.offsetWidth + + const paddingLeft = parseFloat(computedStyles.paddingLeft) || 0 + const borderLeft = parseFloat(computedStyles.borderLeftWidth) || 0 + const scrollLeft = input.scrollLeft || 0 + + let x = borderLeft + paddingLeft + textWidth - scrollLeft // calc native caret x position + + // keep caret inbounds + const paddingRight = parseFloat(computedStyles.paddingRight || "0") + const minX = borderLeft + paddingLeft + const maxX = input.clientWidth - paddingRight - 1 + x = Math.max(minX, Math.min(maxX, x)) + + caretX.value = x + + const caretTop = parseFloat(computedStyles.borderTopWidth || "0") + parseFloat(computedStyles.paddingTop || "0") // lol + const lineHeight = parseFloat(computedStyles.lineHeight) + const height = Number.isFinite(lineHeight) ? lineHeight : input.clientHeight - (parseFloat(computedStyles.paddingTop || "0") + parseFloat(computedStyles.paddingBottom || "0")) + + caretEl.value.style.top = `${caretTop}px` // still funny + caretEl.value.style.height = `${height}px` + caretEl.value.style.left = `${caretX.value}px` + return () => {} +} + +export const scheduleUpdate = (inputEl: Ref, caretEl: Ref, caretX: Ref) => requestAnimationFrame(updateCaret(inputEl, caretEl, caretX)) + +// blink +export const startBlink = (caretVisible: Ref | boolean, blinkTimer: number) => { + caretVisible = true + if (blinkTimer) clearInterval(blinkTimer) + blinkTimer = setInterval(() => (caretVisible = !caretVisible), 500) +} + +export const stopBlink = (caretVisible: Ref | boolean, blinkTimer: number | null) => { + if (blinkTimer) clearInterval(blinkTimer) + blinkTimer = null + caretVisible = false +} + +export const caretDynamicStyle = (caretVisible: Ref | boolean) => ({ + opacity: caretVisible ? 1 : 0, +}) +*/ \ No newline at end of file diff --git a/src/pages/Login.vue b/src/pages/Login.vue index 49ba155..125406b 100644 --- a/src/pages/Login.vue +++ b/src/pages/Login.vue @@ -2,8 +2,8 @@
- - + +
From eab6c11ef50e7d5a23b0397462b9cc73819d13c2 Mon Sep 17 00:00:00 2001 From: zoe2276 Date: Sun, 14 Dec 2025 01:28:04 -0600 Subject: [PATCH 08/11] increased caret efficacy --- src/components/LoginField.vue | 40 ++++++++++++++++++++++++----------- src/composables/caret.ts | 5 +++-- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/components/LoginField.vue b/src/components/LoginField.vue index 2585834..79507c5 100644 --- a/src/components/LoginField.vue +++ b/src/components/LoginField.vue @@ -1,7 +1,7 @@