initial: bootstrap from BukidBountyApp base
This commit is contained in:
184
resources/js/Components/Core/Forms/InputGroup.vue
Normal file
184
resources/js/Components/Core/Forms/InputGroup.vue
Normal file
@@ -0,0 +1,184 @@
|
||||
<template>
|
||||
<div :class="['form-group mb-3', { 'has-error': error }]">
|
||||
<label v-if="label" :for="id" class="form-label d-flex align-items-center">
|
||||
{{ label }}
|
||||
<span v-if="required" class="text-danger ms-1">*</span>
|
||||
</label>
|
||||
|
||||
<div
|
||||
class="input-group-container"
|
||||
:class="[
|
||||
`variant-${variant}`,
|
||||
{ 'is-premium': isPremium, 'is-invalid': error, 'is-disabled': disabled }
|
||||
]"
|
||||
>
|
||||
<div class="input-wrapper">
|
||||
<input
|
||||
:type="type"
|
||||
:id="id"
|
||||
:name="id"
|
||||
:class="['form-control-custom', inputClass]"
|
||||
:placeholder="placeholder"
|
||||
:value="modelValue"
|
||||
:required="required"
|
||||
:disabled="disabled"
|
||||
:list="datalistId"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
/>
|
||||
|
||||
<div class="input-addons">
|
||||
<!-- Icon/span append -->
|
||||
<IconImage
|
||||
v-if="icon"
|
||||
:src="icon"
|
||||
:width="iconWidth"
|
||||
:height="iconHeight"
|
||||
:id="`imgspan${id}`"
|
||||
/>
|
||||
<span v-else-if="spanClass" :class="spanClass" :id="`${id}-span`"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hint && !error" class="form-hint mt-1">{{ hint }}</div>
|
||||
<div v-if="error" class="form-error mt-1">{{ error }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import IconImage from '../IconImage.vue'
|
||||
|
||||
const props = defineProps({
|
||||
label: { type: String, default: '' },
|
||||
type: { type: String, default: 'text' },
|
||||
id: { type: String, required: true },
|
||||
placeholder: { type: String, default: '' },
|
||||
modelValue: { type: [String, Number], default: '' },
|
||||
required: { type: Boolean, default: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
error: { type: String, default: '' },
|
||||
hint: { type: String, default: '' },
|
||||
isPremium: { type: Boolean, default: true },
|
||||
variant: { type: String, default: 'default', validator: (v) => ['default', 'glass', 'soft'].includes(v) },
|
||||
inputClass: { type: String, default: '' },
|
||||
spanClass: { type: String, default: '' },
|
||||
icon: { type: String, default: '' },
|
||||
iconWidth: { type: [String, Number], default: '20px' },
|
||||
iconHeight: { type: [String, Number], default: '20px' },
|
||||
datalistId: { type: String, default: '' },
|
||||
})
|
||||
|
||||
defineEmits(['update:modelValue'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-primary, #1e1e1e);
|
||||
}
|
||||
|
||||
.input-group-container {
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-control-custom {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-card, #ffffff);
|
||||
border: 1px solid var(--border-color, rgba(0, 0, 0, 0.08));
|
||||
border-radius: 12px;
|
||||
color: var(--text-primary, #1e1e1e);
|
||||
font-size: 1rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
:global(.dark-mode) .form-control-custom {
|
||||
background: var(--bg-secondary, #1a1c22);
|
||||
}
|
||||
|
||||
.form-control-custom:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color, #533dea);
|
||||
box-shadow: 0 0 0 3px var(--accent-soft, rgba(83, 61, 234, 0.2));
|
||||
}
|
||||
|
||||
/* Glass Variant */
|
||||
.variant-glass .form-control-custom {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
:global(.dark-mode) .variant-glass .form-control-custom {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.variant-glass .form-control-custom:focus {
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
border-color: var(--accent-color, #533dea);
|
||||
}
|
||||
|
||||
:global(.dark-mode) .variant-glass .form-control-custom:focus {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Soft Variant */
|
||||
.variant-soft .form-control-custom {
|
||||
background: var(--bg-tertiary, #f0f2f5);
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Premium Styles */
|
||||
.is-premium .form-control-custom {
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
:global(.dark-mode) .is-premium .form-control-custom {
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* States */
|
||||
.is-invalid .form-control-custom {
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.is-disabled .form-control-custom {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.input-addons {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted, #a0a0a0);
|
||||
}
|
||||
|
||||
.form-error {
|
||||
font-size: 0.75rem;
|
||||
color: #ef4444;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
162
resources/js/Components/Core/Forms/InputGroupButton.vue
Normal file
162
resources/js/Components/Core/Forms/InputGroupButton.vue
Normal file
@@ -0,0 +1,162 @@
|
||||
<template>
|
||||
<button
|
||||
:class="[
|
||||
'btn-custom',
|
||||
`btn-${variant}`,
|
||||
`btn-${size}`,
|
||||
{ 'is-loading': loading, 'is-disabled': disabled || loading }
|
||||
]"
|
||||
:id="id"
|
||||
:style="buttonStyle"
|
||||
:disabled="disabled || loading"
|
||||
@click="$emit('click', $event)"
|
||||
>
|
||||
<div class="btn-content">
|
||||
<LoadingSpinner v-if="loading" size="sm" color="currentColor" class="me-2" />
|
||||
<slot>
|
||||
{{ text }}
|
||||
</slot>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import LoadingSpinner from '@/Components/LoadingSpinner.vue'
|
||||
|
||||
defineProps({
|
||||
text: { type: String, default: '' },
|
||||
variant: { type: String, default: 'primary', validator: (v) => ['primary', 'secondary', 'outline', 'danger', 'glass', 'text'].includes(v) },
|
||||
id: { type: String, default: '' },
|
||||
buttonStyle: { type: String, default: '' },
|
||||
loading: { type: Boolean, default: false },
|
||||
size: { type: String, default: 'md', validator: (v) => ['sm', 'md', 'lg'].includes(v) },
|
||||
disabled: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
defineEmits(['click'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.btn-custom {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 600;
|
||||
border-radius: 12px;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Sizes */
|
||||
.btn-sm { padding: 0.5rem 1rem; font-size: 0.875rem; }
|
||||
.btn-md { padding: 0.75rem 1.5rem; font-size: 1rem; }
|
||||
.btn-lg { padding: 1rem 2rem; font-size: 1.125rem; }
|
||||
|
||||
/* Variants */
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, var(--accent-color, #4f46e5) 0%, #3730a3 100%);
|
||||
color: white;
|
||||
box-shadow: 0 4px 6px -1px var(--accent-soft, rgba(79, 70, 229, 0.4));
|
||||
}
|
||||
.btn-primary:hover:not(.is-disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 15px -3px var(--accent-soft, rgba(79, 70, 229, 0.5));
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--bg-tertiary, #f0f2f5);
|
||||
color: var(--text-primary, #1e1e1e);
|
||||
}
|
||||
:global(.dark-mode) .btn-secondary {
|
||||
background: var(--bg-tertiary, #2d3748);
|
||||
color: white;
|
||||
}
|
||||
.btn-secondary:hover:not(.is-disabled) {
|
||||
background: var(--border-color, #e2e8f0);
|
||||
}
|
||||
:global(.dark-mode) .btn-secondary:hover:not(.is-disabled) {
|
||||
background: #3d4a5d;
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border-color: var(--border-color, rgba(0, 0, 0, 0.1));
|
||||
color: var(--text-primary, #1e1e1e);
|
||||
}
|
||||
:global(.dark-mode) .btn-outline {
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
}
|
||||
.btn-outline:hover:not(.is-disabled) {
|
||||
background: var(--accent-soft, rgba(0, 0, 0, 0.05));
|
||||
border-color: var(--accent-color, rgba(0, 0, 0, 0.4));
|
||||
}
|
||||
:global(.dark-mode) .btn-outline:hover:not(.is-disabled) {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
}
|
||||
.btn-danger:hover:not(.is-disabled) {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.btn-glass {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: var(--text-primary, #1e1e1e);
|
||||
}
|
||||
:global(.dark-mode) .btn-glass {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
color: white;
|
||||
}
|
||||
.btn-glass:hover:not(.is-disabled) {
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
:global(.dark-mode) .btn-glass:hover:not(.is-disabled) {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.btn-text {
|
||||
background: transparent;
|
||||
color: var(--text-secondary, #717171);
|
||||
}
|
||||
.btn-text:hover:not(.is-disabled) {
|
||||
color: var(--text-primary, #1e1e1e);
|
||||
}
|
||||
:global(.dark-mode) .btn-text:hover:not(.is-disabled) {
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* States */
|
||||
.is-disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.is-loading {
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.btn-custom:active:not(.is-disabled) {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
</style>
|
||||
168
resources/js/Components/Core/Forms/InputGroupCheckbox.vue
Normal file
168
resources/js/Components/Core/Forms/InputGroupCheckbox.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<div :class="['form-check-container mb-3', { 'has-error': error, 'is-switch': isSwitch }]">
|
||||
<label :for="id" class="checkbox-wrapper">
|
||||
<input
|
||||
type="checkbox"
|
||||
:id="id"
|
||||
class="checkbox-input"
|
||||
:checked="isChecked"
|
||||
:disabled="disabled"
|
||||
:value="value"
|
||||
@change="handleChange"
|
||||
/>
|
||||
<div class="checkbox-custom" :class="{ 'is-premium': isPremium }">
|
||||
<div v-if="isSwitch" class="switch-handle"></div>
|
||||
<svg v-else xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" class="check-icon"><path d="M20 6 9 17l-5-5"/></svg>
|
||||
</div>
|
||||
<span class="checkbox-label" v-if="label">{{ label }}</span>
|
||||
</label>
|
||||
<div v-if="error" class="form-error mt-1">{{ error }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
label: { type: String, default: '' },
|
||||
id: { type: String, required: true },
|
||||
modelValue: { type: [Boolean, Array], default: false },
|
||||
value: { type: [String, Number, Boolean], default: null },
|
||||
disabled: { type: Boolean, default: false },
|
||||
isSwitch: { type: Boolean, default: false },
|
||||
isPremium: { type: Boolean, default: true },
|
||||
error: { type: String, default: '' },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const isChecked = computed(() => {
|
||||
if (Array.isArray(props.modelValue)) {
|
||||
return props.modelValue.includes(props.value)
|
||||
}
|
||||
return props.modelValue
|
||||
})
|
||||
|
||||
const handleChange = (event) => {
|
||||
const checked = event.target.checked
|
||||
if (Array.isArray(props.modelValue)) {
|
||||
const newValue = [...props.modelValue]
|
||||
if (checked) {
|
||||
newValue.push(props.value)
|
||||
} else {
|
||||
const index = newValue.indexOf(props.value)
|
||||
if (index > -1) newValue.splice(index, 1)
|
||||
}
|
||||
emit('update:modelValue', newValue)
|
||||
} else {
|
||||
emit('update:modelValue', checked)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-check-container {
|
||||
display: block;
|
||||
min-height: 1.5rem;
|
||||
}
|
||||
|
||||
.checkbox-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.checkbox-input {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
height: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
/* Custom Checkbox Design */
|
||||
.checkbox-custom {
|
||||
position: relative;
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
background: var(--bg-secondary, #f8f9fa);
|
||||
border: 1px solid var(--border-color, rgba(0, 0, 0, 0.1));
|
||||
border-radius: 6px;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
:global(.dark-mode) .checkbox-custom {
|
||||
background: var(--bg-tertiary, #24272d);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.is-switch .checkbox-custom {
|
||||
width: 44px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.checkbox-input:checked ~ .checkbox-custom {
|
||||
background: var(--accent-color, #533dea);
|
||||
border-color: var(--accent-color, #533dea);
|
||||
box-shadow: 0 4px 6px -1px var(--accent-soft, rgba(83, 61, 234, 0.4));
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: white;
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.checkbox-input:checked ~ .checkbox-custom .check-icon {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
/* Switch Handle */
|
||||
.switch-handle {
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 3px;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.checkbox-input:checked ~ .checkbox-custom .switch-handle {
|
||||
left: 25px;
|
||||
}
|
||||
|
||||
/* Premium Effects */
|
||||
.is-premium {
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
|
||||
.checkbox-wrapper:hover .checkbox-custom {
|
||||
border-color: var(--accent-color, rgba(83, 61, 234, 0.3));
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-primary, #1e1e1e);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.form-error {
|
||||
font-size: 0.75rem;
|
||||
color: #ef4444;
|
||||
margin-left: 2rem;
|
||||
}
|
||||
|
||||
.checkbox-input:disabled ~ .checkbox-custom {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
51
resources/js/Components/Core/Forms/InputGroupFileUpload.vue
Normal file
51
resources/js/Components/Core/Forms/InputGroupFileUpload.vue
Normal file
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div>
|
||||
<label v-if="label" :for="id">
|
||||
<span v-html="label"></span>
|
||||
<IconImage
|
||||
v-if="!disabled"
|
||||
src="https://cdn.jsdelivr.net/gh/telemagnadon/obj-vault-3a@v2026.05.14-vendor-2/a/b6dc254166b4.bin"
|
||||
:id="`ClearUploadButton-${id}`"
|
||||
class="clear-upload-btn"
|
||||
@click="$emit('clear')"
|
||||
/>
|
||||
</label>
|
||||
<div class="input-group mb-3">
|
||||
<form
|
||||
:id="id"
|
||||
:action="uploadUrl"
|
||||
:style="{ width: computedWidth, pointerEvents: disabled ? 'none' : 'auto' }"
|
||||
class="dropzone"
|
||||
:class="{ disabled: disabled }"
|
||||
>
|
||||
<slot>
|
||||
<div class="dz-message">Drop files here or click to upload</div>
|
||||
</slot>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import IconImage from '../IconImage.vue'
|
||||
|
||||
const props = defineProps({
|
||||
label: { type: String, default: '' },
|
||||
id: { type: String, required: true },
|
||||
uploadUrl: { type: String, default: '/File/Upload/Unknown' },
|
||||
width: { type: String, default: '100%' },
|
||||
disabled: { type: Boolean, default: false },
|
||||
})
|
||||
|
||||
defineEmits(['files-changed', 'clear'])
|
||||
|
||||
const computedWidth = computed(() => props.width || '100%')
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.clear-upload-btn {
|
||||
cursor: pointer;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
48
resources/js/Components/Core/Forms/InputGroupNumber.vue
Normal file
48
resources/js/Components/Core/Forms/InputGroupNumber.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<InputGroup
|
||||
:label="label"
|
||||
type="number"
|
||||
:id="id"
|
||||
:placeholder="placeholder"
|
||||
:model-value="modelValue"
|
||||
:required="required"
|
||||
:disabled="disabled"
|
||||
:input-class="inputClass"
|
||||
:icon="icon"
|
||||
:icon-width="iconWidth"
|
||||
:icon-height="iconHeight"
|
||||
:datalist-id="datalistId"
|
||||
v-bind="numberAttrs"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import InputGroup from './InputGroup.vue'
|
||||
|
||||
const props = defineProps({
|
||||
label: { type: String, default: '' },
|
||||
id: { type: String, required: true },
|
||||
placeholder: { type: String, default: '' },
|
||||
modelValue: { type: [String, Number], default: 0 },
|
||||
min: { type: [String, Number], default: '' },
|
||||
max: { type: [String, Number], default: '' },
|
||||
required: { type: Boolean, default: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
inputClass: { type: String, default: '' },
|
||||
icon: { type: String, default: '' },
|
||||
iconWidth: { type: [String, Number], default: '40px' },
|
||||
iconHeight: { type: [String, Number], default: '40px' },
|
||||
datalistId: { type: String, default: '' },
|
||||
})
|
||||
|
||||
defineEmits(['update:modelValue'])
|
||||
|
||||
const numberAttrs = computed(() => {
|
||||
const attrs = {}
|
||||
if (props.min !== '') attrs.min = props.min
|
||||
if (props.max !== '') attrs.max = props.max
|
||||
return attrs
|
||||
})
|
||||
</script>
|
||||
191
resources/js/Components/Core/Forms/InputGroupSelect.vue
Normal file
191
resources/js/Components/Core/Forms/InputGroupSelect.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<div :class="['form-group mb-3', { 'has-error': error }]">
|
||||
<label v-if="label" :for="id" class="form-label d-flex align-items-center">
|
||||
{{ label }}
|
||||
<span v-if="required" class="text-danger ms-1">*</span>
|
||||
</label>
|
||||
|
||||
<div
|
||||
class="input-group-container"
|
||||
:class="[
|
||||
`variant-${variant}`,
|
||||
{ 'is-premium': isPremium, 'is-invalid': error, 'is-disabled': disabled }
|
||||
]"
|
||||
>
|
||||
<div class="input-wrapper">
|
||||
<select
|
||||
:id="id"
|
||||
:class="['form-control-custom', selectClass]"
|
||||
:disabled="disabled"
|
||||
:value="modelValue"
|
||||
:required="required"
|
||||
@change="$emit('update:modelValue', $event.target.value)"
|
||||
>
|
||||
<option v-if="placeholder" value="" disabled selected>{{ placeholder }}</option>
|
||||
<option
|
||||
v-for="(opt, index) in formattedOptions"
|
||||
:key="index"
|
||||
:value="opt.value"
|
||||
>
|
||||
{{ opt.text }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
<div class="select-arrow">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
|
||||
</div>
|
||||
|
||||
<div v-if="icon || spanClass" class="input-addons">
|
||||
<IconImage v-if="icon" :src="icon" :width="iconWidth" :height="iconHeight" />
|
||||
<span v-else-if="spanClass" :class="spanClass"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="hint && !error" class="form-hint mt-1">{{ hint }}</div>
|
||||
<div v-if="error" class="form-error mt-1">{{ error }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import IconImage from '../IconImage.vue'
|
||||
|
||||
const props = defineProps({
|
||||
label: { type: String, default: '' },
|
||||
id: { type: String, required: true },
|
||||
modelValue: { type: [String, Number], default: '' },
|
||||
options: { type: Array, default: () => [] },
|
||||
placeholder: { type: String, default: '' },
|
||||
required: { type: Boolean, default: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
error: { type: String, default: '' },
|
||||
hint: { type: String, default: '' },
|
||||
isPremium: { type: Boolean, default: true },
|
||||
variant: { type: String, default: 'default' },
|
||||
selectClass: { type: String, default: '' },
|
||||
spanClass: { type: String, default: '' },
|
||||
icon: { type: String, default: '' },
|
||||
iconWidth: { type: [String, Number], default: '20px' },
|
||||
iconHeight: { type: [String, Number], default: '20px' },
|
||||
})
|
||||
|
||||
defineEmits(['update:modelValue'])
|
||||
|
||||
const formattedOptions = computed(() => {
|
||||
return props.options.map(opt => {
|
||||
if (typeof opt === 'string' || typeof opt === 'number') {
|
||||
return { value: opt, text: opt }
|
||||
}
|
||||
return opt
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-primary, #1e1e1e);
|
||||
}
|
||||
|
||||
.input-group-container {
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.form-control-custom {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-card, #ffffff);
|
||||
border: 1px solid var(--border-color, rgba(0, 0, 0, 0.08));
|
||||
border-radius: 12px;
|
||||
color: var(--text-primary, #1e1e1e);
|
||||
font-size: 1rem;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:global(.dark-mode) .form-control-custom {
|
||||
background: var(--bg-secondary, #1a1c22);
|
||||
}
|
||||
|
||||
.form-control-custom:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color, #533dea);
|
||||
box-shadow: 0 0 0 3px var(--accent-soft, rgba(83, 61, 234, 0.2));
|
||||
}
|
||||
|
||||
.select-arrow {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
pointer-events: none;
|
||||
color: var(--text-muted, #a0a0a0);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Glass Variant */
|
||||
.variant-glass .form-control-custom {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
:global(.dark-mode) .variant-glass .form-control-custom {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Premium Styles */
|
||||
.is-premium .form-control-custom {
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
:global(.dark-mode) .is-premium .form-control-custom {
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* States */
|
||||
.is-invalid .form-control-custom {
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.is-disabled .form-control-custom {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.input-addons {
|
||||
position: absolute;
|
||||
right: 2.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted, #a0a0a0);
|
||||
}
|
||||
|
||||
.form-error {
|
||||
font-size: 0.75rem;
|
||||
color: #ef4444;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
133
resources/js/Components/Core/Forms/InputGroupTextarea.vue
Normal file
133
resources/js/Components/Core/Forms/InputGroupTextarea.vue
Normal file
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<div :class="['form-group mb-3', { 'has-error': error }]">
|
||||
<label v-if="label" :for="id" class="form-label d-flex align-items-center">
|
||||
{{ label }}
|
||||
<span v-if="required" class="text-danger ms-1">*</span>
|
||||
</label>
|
||||
|
||||
<div
|
||||
class="input-group-container"
|
||||
:class="[
|
||||
`variant-${variant}`,
|
||||
{ 'is-premium': isPremium, 'is-invalid': error, 'is-disabled': disabled }
|
||||
]"
|
||||
>
|
||||
<textarea
|
||||
:id="id"
|
||||
:name="id"
|
||||
:rows="rows"
|
||||
:class="['form-control-custom', textareaClass]"
|
||||
:placeholder="placeholder"
|
||||
:required="required"
|
||||
:disabled="disabled"
|
||||
:value="modelValue"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div v-if="hint && !error" class="form-hint mt-1">{{ hint }}</div>
|
||||
<div v-if="error" class="form-error mt-1">{{ error }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
label: { type: String, default: '' },
|
||||
id: { type: String, required: true },
|
||||
modelValue: { type: String, default: '' },
|
||||
placeholder: { type: String, default: '' },
|
||||
required: { type: Boolean, default: false },
|
||||
disabled: { type: Boolean, default: false },
|
||||
rows: { type: Number, default: 3 },
|
||||
error: { type: String, default: '' },
|
||||
hint: { type: String, default: '' },
|
||||
isPremium: { type: Boolean, default: true },
|
||||
variant: { type: String, default: 'default' },
|
||||
textareaClass: { type: String, default: '' },
|
||||
})
|
||||
|
||||
defineEmits(['update:modelValue'])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-group {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
color: var(--text-primary, #1e1e1e);
|
||||
}
|
||||
|
||||
.input-group-container {
|
||||
position: relative;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.form-control-custom {
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--bg-card, #ffffff);
|
||||
border: 1px solid var(--border-color, rgba(0, 0, 0, 0.08));
|
||||
border-radius: 12px;
|
||||
color: var(--text-primary, #1e1e1e);
|
||||
font-size: 1rem;
|
||||
transition: all 0.2s ease;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
:global(.dark-mode) .form-control-custom {
|
||||
background: var(--bg-secondary, #1a1c22);
|
||||
}
|
||||
|
||||
.form-control-custom:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color, #533dea);
|
||||
box-shadow: 0 0 0 3px var(--accent-soft, rgba(83, 61, 234, 0.2));
|
||||
}
|
||||
|
||||
/* Glass Variant */
|
||||
.variant-glass .form-control-custom {
|
||||
background: rgba(255, 255, 255, 0.4);
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
:global(.dark-mode) .variant-glass .form-control-custom {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Premium Styles */
|
||||
.is-premium .form-control-custom {
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
:global(.dark-mode) .is-premium .form-control-custom {
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* States */
|
||||
.is-invalid .form-control-custom {
|
||||
border-color: #ef4444;
|
||||
}
|
||||
|
||||
.is-disabled .form-control-custom {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted, #a0a0a0);
|
||||
}
|
||||
|
||||
.form-error {
|
||||
font-size: 0.75rem;
|
||||
color: #ef4444;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user