Add:Eye icon

This commit is contained in:
supalerk-ar66 2026-01-13 11:24:03 +07:00
parent b1aa414190
commit 959a7e3f85
18 changed files with 118 additions and 43 deletions

File diff suppressed because one or more lines are too long

View file

@ -1 +1 @@
{"id":"dev","timestamp":1768275611480}
{"id":"dev","timestamp":1768276905519}

View file

@ -1 +1 @@
{"id":"dev","timestamp":1768275611480,"matcher":{"static":{},"wildcard":{},"dynamic":{}},"prerendered":[]}
{"id":"dev","timestamp":1768276905519,"matcher":{"static":{},"wildcard":{},"dynamic":{}},"prerendered":[]}

View file

@ -1,5 +1,5 @@
{
"date": "2026-01-13T03:40:17.768Z",
"date": "2026-01-13T04:01:52.330Z",
"preset": "nitro-dev",
"framework": {
"name": "nuxt",
@ -9,9 +9,9 @@
"nitro": "2.12.8"
},
"dev": {
"pid": 14040,
"pid": 17152,
"workerAddress": {
"socketPath": "\\\\.\\pipe\\nitro-worker-14040-1-1-9354.sock"
"socketPath": "\\\\.\\pipe\\nitro-worker-17152-1-1-4801.sock"
}
}
}

View file

@ -1,8 +1,8 @@
/// <reference types="quasar" />
/// <reference types="nuxt-quasar-ui" />
/// <reference types="@nuxt/telemetry" />
/// <reference types="@nuxtjs/tailwindcss" />
/// <reference types="nuxt-quasar-ui" />
/// <reference types="@nuxt/devtools" />
/// <reference types="@nuxt/telemetry" />
/// <reference path="types/builder-env.d.ts" />
/// <reference types="nuxt" />
/// <reference path="types/app-defaults.d.ts" />

View file

@ -1,4 +1,4 @@
// generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 13/1/2569 10:40:15
// generated by the @nuxtjs/tailwindcss <https://github.com/nuxt-modules/tailwindcss> module at 13/1/2569 11:18:03
import "@nuxtjs/tailwindcss/config-ctx"
import configMerger from "@nuxtjs/tailwindcss/merger";

View file

@ -2,9 +2,10 @@
/**
* @file FormInput.vue
* @description Reusable input component with label, error handling, and support for disabled/required states.
* Now supports password visibility toggle.
*/
defineProps<{
const props = defineProps<{
modelValue: string
label: string
type?: string
@ -19,6 +20,22 @@ const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
// Password visibility state
const showPassword = ref(false)
// Toggle function
const togglePassword = () => {
showPassword.value = !showPassword.value
}
// Compute input type based on visibility state
const inputType = computed(() => {
if (props.type === 'password') {
return showPassword.value ? 'text' : 'password'
}
return props.type || 'text'
})
const updateValue = (event: Event) => {
emit('update:modelValue', (event.target as HTMLInputElement).value)
}
@ -30,15 +47,42 @@ const updateValue = (event: Event) => {
{{ label }}
<span v-if="required" class="required-mark">*</span>
</label>
<input
:type="type || 'text'"
:value="modelValue"
class="input-field"
:class="{ 'input-error': error }"
:placeholder="placeholder"
:disabled="disabled"
@input="updateValue"
>
<div class="input-wrapper">
<input
:type="inputType"
:value="modelValue"
class="input-field"
:class="{ 'input-error': error, 'has-password-toggle': type === 'password' }"
:placeholder="placeholder"
:disabled="disabled"
@input="updateValue"
>
<!-- Password Toggle Button -->
<button
v-if="type === 'password'"
type="button"
class="password-toggle-btn"
@click="togglePassword"
tabindex="-1"
>
<!-- Eye Icon (Show) -->
<svg v-if="!showPassword" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<!-- Eye Off Icon (Hide) -->
<svg v-else xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/>
<path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/>
<path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7c.44 0 .87-.03 1.28-.08"/>
<line x1="2" y1="2" x2="22" y2="22"/>
</svg>
</button>
</div>
<span v-if="error" class="error-message">
<span class="error-icon"></span>
{{ error }}
@ -51,6 +95,37 @@ const updateValue = (event: Event) => {
margin-bottom: 16px;
}
.input-wrapper {
position: relative;
width: 100%;
}
.password-toggle-btn {
position: absolute;
right: 12px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
color: #94a3b8;
display: flex;
align-items: center;
justify-content: center;
padding: 4px;
border-radius: 50%;
transition: all 0.2s;
}
.password-toggle-btn:hover {
color: #64748b;
background-color: rgba(0, 0, 0, 0.05);
}
.input-field.has-password-toggle {
padding-right: 42px;
}
.required-mark {
color: var(--error);
margin-left: 2px;

View file

@ -40,4 +40,4 @@ var _default = exports.default = defineNuxtConfig({
}
}
}); /* v9-2bc4be59a7dd6932 */
}); /* v9-1ad4b5e9404ba42a */

View file

@ -32,4 +32,4 @@
}
},
plugins: []
}; /* v9-5906c7bca40137c7 */
}; /* v9-24ec813999eb8913 */

View file

@ -20,7 +20,7 @@ import {
toggleClientConnected,
updateDevToolsClientDetected,
updateTimelineLayersState
} from "./chunk-4EWULRPV.js";
} from "./chunk-KUURTRQ3.js";
// node_modules/@vue/devtools-core/dist/index.js
import { computed, inject, onUnmounted, ref, watch } from "vue";

File diff suppressed because one or more lines are too long

View file

@ -81,7 +81,7 @@ import {
updateDevToolsClientDetected,
updateDevToolsState,
updateTimelineLayersState
} from "./chunk-4EWULRPV.js";
} from "./chunk-KUURTRQ3.js";
export {
DevToolsContextHookKeys,
DevToolsMessagingHookKeys,

View file

@ -1,31 +1,31 @@
{
"hash": "725beae3",
"configHash": "230b07ea",
"lockfileHash": "189bf8ec",
"browserHash": "922f090e",
"hash": "06498dbb",
"configHash": "5974d207",
"lockfileHash": "a84e6c42",
"browserHash": "63f18de1",
"optimized": {
"errx": {
"src": "../../../../errx/dist/index.js",
"file": "errx.js",
"fileHash": "2195cf07",
"fileHash": "222375d5",
"needsInterop": false
},
"@vue/devtools-core": {
"src": "../../../../@vue/devtools-core/dist/index.js",
"file": "@vue_devtools-core.js",
"fileHash": "a62f2c91",
"fileHash": "2c86f7fa",
"needsInterop": false
},
"@vue/devtools-kit": {
"src": "../../../../@vue/devtools-kit/dist/index.js",
"file": "@vue_devtools-kit.js",
"fileHash": "cd5a5dd1",
"fileHash": "cddf14f6",
"needsInterop": false
}
},
"chunks": {
"chunk-4EWULRPV": {
"file": "chunk-4EWULRPV.js"
"chunk-KUURTRQ3": {
"file": "chunk-KUURTRQ3.js"
}
}
}

File diff suppressed because one or more lines are too long

View file

@ -5988,4 +5988,4 @@ export {
parse,
devtools
};
//# sourceMappingURL=chunk-4EWULRPV.js.map
//# sourceMappingURL=chunk-KUURTRQ3.js.map

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,7 @@
{
"version": 3,
"sources": ["../../../../errx/dist/index.js"],
"sourcesContent": ["const IS_ABSOLUTE_RE = /^[/\\\\](?![/\\\\])|^[/\\\\]{2}(?!\\.)|^[a-z]:[/\\\\]/i;\nconst LINE_RE = /^\\s+at (?:(?<function>[^)]+) \\()?(?<source>[^)]+)\\)?$/u;\nconst SOURCE_RE = /^(?<source>.+):(?<line>\\d+):(?<column>\\d+)$/u;\nfunction captureRawStackTrace() {\n if (!Error.captureStackTrace) {\n return;\n }\n const stack = new Error();\n Error.captureStackTrace(stack);\n return stack.stack;\n}\nfunction captureStackTrace() {\n const stack = captureRawStackTrace();\n return stack ? parseRawStackTrace(stack) : [];\n}\nfunction parseRawStackTrace(stacktrace) {\n const trace = [];\n for (const line of stacktrace.split(\"\\n\")) {\n const parsed = LINE_RE.exec(line)?.groups;\n if (!parsed) {\n continue;\n }\n if (!parsed.source) {\n continue;\n }\n const parsedSource = SOURCE_RE.exec(parsed.source)?.groups;\n if (parsedSource) {\n Object.assign(parsed, parsedSource);\n }\n if (IS_ABSOLUTE_RE.test(parsed.source)) {\n parsed.source = `file://${parsed.source}`;\n }\n if (parsed.source === import.meta.url) {\n continue;\n }\n for (const key of [\"line\", \"column\"]) {\n if (parsed[key]) {\n parsed[key] = Number(parsed[key]);\n }\n }\n trace.push(parsed);\n }\n return trace;\n}\n\nexport { captureRawStackTrace, captureStackTrace, parseRawStackTrace };\n"],
"sourcesContent": ["const IS_ABSOLUTE_RE = /^[/\\\\](?![/\\\\])|^[/\\\\]{2}(?!\\.)|^[a-z]:[/\\\\]/i;\r\nconst LINE_RE = /^\\s+at (?:(?<function>[^)]+) \\()?(?<source>[^)]+)\\)?$/u;\r\nconst SOURCE_RE = /^(?<source>.+):(?<line>\\d+):(?<column>\\d+)$/u;\r\nfunction captureRawStackTrace() {\r\n if (!Error.captureStackTrace) {\r\n return;\r\n }\r\n const stack = new Error();\r\n Error.captureStackTrace(stack);\r\n return stack.stack;\r\n}\r\nfunction captureStackTrace() {\r\n const stack = captureRawStackTrace();\r\n return stack ? parseRawStackTrace(stack) : [];\r\n}\r\nfunction parseRawStackTrace(stacktrace) {\r\n const trace = [];\r\n for (const line of stacktrace.split(\"\\n\")) {\r\n const parsed = LINE_RE.exec(line)?.groups;\r\n if (!parsed) {\r\n continue;\r\n }\r\n if (!parsed.source) {\r\n continue;\r\n }\r\n const parsedSource = SOURCE_RE.exec(parsed.source)?.groups;\r\n if (parsedSource) {\r\n Object.assign(parsed, parsedSource);\r\n }\r\n if (IS_ABSOLUTE_RE.test(parsed.source)) {\r\n parsed.source = `file://${parsed.source}`;\r\n }\r\n if (parsed.source === import.meta.url) {\r\n continue;\r\n }\r\n for (const key of [\"line\", \"column\"]) {\r\n if (parsed[key]) {\r\n parsed[key] = Number(parsed[key]);\r\n }\r\n }\r\n trace.push(parsed);\r\n }\r\n return trace;\r\n}\r\n\r\nexport { captureRawStackTrace, captureStackTrace, parseRawStackTrace };\r\n"],
"mappings": ";AAAA,IAAM,iBAAiB;AACvB,IAAM,UAAU;AAChB,IAAM,YAAY;AAClB,SAAS,uBAAuB;AAC9B,MAAI,CAAC,MAAM,mBAAmB;AAC5B;AAAA,EACF;AACA,QAAM,QAAQ,IAAI,MAAM;AACxB,QAAM,kBAAkB,KAAK;AAC7B,SAAO,MAAM;AACf;AACA,SAAS,oBAAoB;AAC3B,QAAM,QAAQ,qBAAqB;AACnC,SAAO,QAAQ,mBAAmB,KAAK,IAAI,CAAC;AAC9C;AACA,SAAS,mBAAmB,YAAY;AACtC,QAAM,QAAQ,CAAC;AACf,aAAW,QAAQ,WAAW,MAAM,IAAI,GAAG;AACzC,UAAM,SAAS,QAAQ,KAAK,IAAI,GAAG;AACnC,QAAI,CAAC,QAAQ;AACX;AAAA,IACF;AACA,QAAI,CAAC,OAAO,QAAQ;AAClB;AAAA,IACF;AACA,UAAM,eAAe,UAAU,KAAK,OAAO,MAAM,GAAG;AACpD,QAAI,cAAc;AAChB,aAAO,OAAO,QAAQ,YAAY;AAAA,IACpC;AACA,QAAI,eAAe,KAAK,OAAO,MAAM,GAAG;AACtC,aAAO,SAAS,UAAU,OAAO,MAAM;AAAA,IACzC;AACA,QAAI,OAAO,WAAW,YAAY,KAAK;AACrC;AAAA,IACF;AACA,eAAW,OAAO,CAAC,QAAQ,QAAQ,GAAG;AACpC,UAAI,OAAO,GAAG,GAAG;AACf,eAAO,GAAG,IAAI,OAAO,OAAO,GAAG,CAAC;AAAA,MAClC;AAAA,IACF;AACA,UAAM,KAAK,MAAM;AAAA,EACnB;AACA,SAAO;AACT;",
"names": []
}

View file

@ -163,7 +163,7 @@ const handleRegister = async () => {
v-model="registerForm.phone"
label="เบอร์โทรศัพท์"
type="tel"
placeholder="0812345678"
placeholder=""
:error="errors.phone"
required
class="dark-form-input"