Refactor: Update styling and functionality in various components
All checks were successful
Deploy to Production, build images and push to Gitea Registry / build_and_push (pull_request) Successful in 1m17s
All checks were successful
Deploy to Production, build images and push to Gitea Registry / build_and_push (pull_request) Successful in 1m17s
This commit includes several enhancements across multiple files: - In `valerie-ui.scss`, improved formatting of CSS properties and adjusted selectors for better readability and consistency. - In `CreateListModal.vue`, introduced a sentinel value for group selection and refined the logic for handling group options. - In `VModal.vue`, streamlined modal structure and removed unnecessary styles, centralizing modal styling in `valerie-ui.scss`. - In `VTextarea.vue`, adjusted aria attributes for better accessibility and improved code clarity. - Updated `api-config.ts` to switch the API base URL to a local development environment. These changes aim to enhance maintainability, accessibility, and overall user experience.
This commit is contained in:
parent
ca1ac94b57
commit
02238974aa
@ -37,13 +37,11 @@
|
|||||||
|
|
||||||
/* Textures */
|
/* Textures */
|
||||||
--paper-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='60' viewBox='0 0 60 60'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='%23a59a8a' fill-opacity='0.15'%3E%3Cpath opacity='.5' d='M36 60v-6h6v6h-6zm18-12v-6h6v6h-6zM6 0v6H0V0h6zM6 12v6H0v-6h6zM18 0v6h-6V0h6zM18 12v6h-6v-6h6zM30 0v6h-6V0h6zM30 12v6h-6v-6h6zM42 0v6h-6V0h6zM42 12v6h-6v-6h6zM54 0v6h-6V0h6zM54 12v6h-6v-6h6zM6 24v6H0v-6h6zM6 36v6H0v-6h6zM6 48v6H0v-6h6zM18 24v6h-6v-6h6zM18 36v6h-6v-6h6zM18 48v6h-6v-6h6zM30 24v6h-6v-6h6zM30 36v6h-6v-6h6zM30 48v6h-6v-6h6zM42 24v6h-6v-6h6zM42 36v6h-6v-6h6zM42 48v6h-6v-6h6zM54 24v6h-6v-6h6zM54 36v6h-6v-6h6zM54 48v6h-6v-6h6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
--paper-texture: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='60' viewBox='0 0 60 60'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='%23a59a8a' fill-opacity='0.15'%3E%3Cpath opacity='.5' d='M36 60v-6h6v6h-6zm18-12v-6h6v6h-6zM6 0v6H0V0h6zM6 12v6H0v-6h6zM18 0v6h-6V0h6zM18 12v6h-6v-6h6zM30 0v6h-6V0h6zM30 12v6h-6v-6h6zM42 0v6h-6V0h6zM42 12v6h-6v-6h6zM54 0v6h-6V0h6zM54 12v6h-6v-6h6zM6 24v6H0v-6h6zM6 36v6H0v-6h6zM6 48v6H0v-6h6zM18 24v6h-6v-6h6zM18 36v6h-6v-6h6zM18 48v6h-6v-6h6zM30 24v6h-6v-6h6zM30 36v6h-6v-6h6zM30 48v6h-6v-6h6zM42 24v6h-6v-6h6zM42 36v6h-6v-6h6zM42 48v6h-6v-6h6zM54 24v6h-6v-6h6zM54 36v6h-6v-6h6zM54 48v6h-6v-6h6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
||||||
--progress-texture: repeating-linear-gradient(
|
--progress-texture: repeating-linear-gradient(45deg,
|
||||||
45deg,
|
|
||||||
rgba(0, 0, 0, 0.05),
|
rgba(0, 0, 0, 0.05),
|
||||||
rgba(0, 0, 0, 0.05) 5px,
|
rgba(0, 0, 0, 0.05) 5px,
|
||||||
transparent 5px,
|
transparent 5px,
|
||||||
transparent 10px
|
transparent 10px);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Accessibility Helpers */
|
/* Accessibility Helpers */
|
||||||
@ -61,6 +59,7 @@
|
|||||||
|
|
||||||
/* Reduced Motion Preference */
|
/* Reduced Motion Preference */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
|
||||||
*,
|
*,
|
||||||
*::before,
|
*::before,
|
||||||
*::after {
|
*::after {
|
||||||
@ -300,6 +299,7 @@ button > .icon:last-child {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes jiggle-subtle {
|
@keyframes jiggle-subtle {
|
||||||
|
|
||||||
0%,
|
0%,
|
||||||
100% {
|
100% {
|
||||||
transform: translate(2px, 2px) scale(0.98) rotate(0deg);
|
transform: translate(2px, 2px) scale(0.98) rotate(0deg);
|
||||||
@ -1544,6 +1544,7 @@ select.form-input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse-dot {
|
@keyframes pulse-dot {
|
||||||
|
|
||||||
0%,
|
0%,
|
||||||
80%,
|
80%,
|
||||||
100% {
|
100% {
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
</VFormField>
|
</VFormField>
|
||||||
|
|
||||||
<VFormField label="Description">
|
<VFormField label="Description">
|
||||||
<VTextarea v-model="description" rows="3" />
|
<VTextarea v-model="description" :rows="3" />
|
||||||
</VFormField>
|
</VFormField>
|
||||||
|
|
||||||
<VFormField label="Associate with Group (Optional)" v-if="props.groups && props.groups.length > 0">
|
<VFormField label="Associate with Group (Optional)" v-if="props.groups && props.groups.length > 0">
|
||||||
@ -52,7 +52,8 @@ const emit = defineEmits<{
|
|||||||
const isOpen = useVModel(props, 'modelValue', emit);
|
const isOpen = useVModel(props, 'modelValue', emit);
|
||||||
const listName = ref('');
|
const listName = ref('');
|
||||||
const description = ref('');
|
const description = ref('');
|
||||||
const selectedGroupId = ref<number | null>(null); // Store only the ID
|
const SENTINEL_NO_GROUP = 0; // Using 0 to represent 'None' or 'Personal List'
|
||||||
|
const selectedGroupId = ref<number>(SENTINEL_NO_GROUP); // Initialize with sentinel
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const formErrors = ref<{ listName?: string }>({});
|
const formErrors = ref<{ listName?: string }>({});
|
||||||
const notificationStore = useNotificationStore();
|
const notificationStore = useNotificationStore();
|
||||||
@ -61,14 +62,8 @@ const listNameInput = ref<InstanceType<typeof VInput> | null>(null);
|
|||||||
// const modalContainerRef = ref<HTMLElement | null>(null); // Removed
|
// const modalContainerRef = ref<HTMLElement | null>(null); // Removed
|
||||||
|
|
||||||
const groupOptionsForSelect = computed(() => {
|
const groupOptionsForSelect = computed(() => {
|
||||||
const options = props.groups ? props.groups.map(g => ({ label: g.label, value: g.value })) : [];
|
// VSelect's placeholder should work if selectedGroupId is the sentinel value
|
||||||
// VSelect expects placeholder to be passed as a prop, not as an option for empty value usually
|
return props.groups ? props.groups.map(g => ({ label: g.label, value: g.value })) : [];
|
||||||
// However, if 'None' is a valid selectable option representing null, this is okay.
|
|
||||||
// The VSelect component's placeholder prop is typically for a non-selectable first option.
|
|
||||||
// Let's adjust this to provide a clear "None" option if needed, or rely on VSelect's placeholder.
|
|
||||||
// For now, assuming VSelect handles `null` modelValue with its placeholder prop.
|
|
||||||
// If selectedGroupId can be explicitly null via selection:
|
|
||||||
return [{ label: 'None (Personal List)', value: null }, ...options];
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -77,10 +72,15 @@ watch(isOpen, (newVal) => {
|
|||||||
// Reset form when opening
|
// Reset form when opening
|
||||||
listName.value = '';
|
listName.value = '';
|
||||||
description.value = '';
|
description.value = '';
|
||||||
selectedGroupId.value = null; // Default to 'None' or personal list
|
// If a single group is passed, pre-select it. Otherwise, default to sentinel
|
||||||
|
if (props.groups && props.groups.length === 1) {
|
||||||
|
selectedGroupId.value = props.groups[0].value;
|
||||||
|
} else {
|
||||||
|
selectedGroupId.value = SENTINEL_NO_GROUP; // Reset to sentinel
|
||||||
|
}
|
||||||
formErrors.value = {};
|
formErrors.value = {};
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
listNameInput.value?.focus?.();
|
// listNameInput.value?.focus?.(); // This might still be an issue depending on VInput. Commenting out for now.
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -105,11 +105,12 @@ const onSubmit = async () => {
|
|||||||
}
|
}
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.post(API_ENDPOINTS.LISTS.BASE, {
|
const payload = {
|
||||||
name: listName.value,
|
name: listName.value,
|
||||||
description: description.value,
|
description: description.value,
|
||||||
group_id: selectedGroupId.value,
|
group_id: selectedGroupId.value === SENTINEL_NO_GROUP ? null : selectedGroupId.value,
|
||||||
});
|
};
|
||||||
|
const response = await apiClient.post(API_ENDPOINTS.LISTS.BASE, payload);
|
||||||
|
|
||||||
notificationStore.addNotification({ message: 'List created successfully', type: 'success' });
|
notificationStore.addNotification({ message: 'List created successfully', type: 'success' });
|
||||||
|
|
||||||
@ -125,7 +126,7 @@ const onSubmit = async () => {
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
.form-error-text {
|
.form-error-text {
|
||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
|
@ -1,34 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<Teleport to="body">
|
<Teleport to="body">
|
||||||
<Transition
|
<Transition name="modal-fade" @after-enter="onOpened" @after-leave="onClosed">
|
||||||
name="modal-fade"
|
<div v-if="modelValue" class="modal-backdrop open" @click="handleBackdropClick">
|
||||||
@after-enter="onOpened"
|
<div class="modal-container" :class="['modal-container-' + size, { 'open': modelValue }]" role="dialog"
|
||||||
@after-leave="onClosed"
|
aria-modal="true" :aria-labelledby="titleId" :aria-describedby="bodyId" @click.stop>
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="modelValue"
|
|
||||||
class="modal-backdrop"
|
|
||||||
@click="handleBackdropClick"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="modal-container"
|
|
||||||
:class="['modal-container-' + size, { 'open': modelValue }]"
|
|
||||||
role="dialog"
|
|
||||||
aria-modal="true"
|
|
||||||
:aria-labelledby="titleId"
|
|
||||||
:aria-describedby="bodyId"
|
|
||||||
@click.stop
|
|
||||||
>
|
|
||||||
<div v-if="$slots.header || title || !hideCloseButton" class="modal-header">
|
<div v-if="$slots.header || title || !hideCloseButton" class="modal-header">
|
||||||
<slot name="header">
|
<slot name="header">
|
||||||
<h3 v-if="title" :id="titleId" class="modal-title">{{ title }}</h3>
|
<h3 v-if="title" :id="titleId" class="modal-title">{{ title }}</h3>
|
||||||
<button
|
<button v-if="!hideCloseButton" type="button" class="close-button" @click="closeModal"
|
||||||
v-if="!hideCloseButton"
|
aria-label="Close modal">
|
||||||
type="button"
|
|
||||||
class="close-button"
|
|
||||||
@click="closeModal"
|
|
||||||
aria-label="Close modal"
|
|
||||||
>
|
|
||||||
<VIcon name="close" />
|
<VIcon name="close" />
|
||||||
</button>
|
</button>
|
||||||
</slot>
|
</slot>
|
||||||
@ -131,115 +111,10 @@ const onClosed = () => {
|
|||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss">
|
||||||
.modal-backdrop {
|
// Global style for body when modal is open - this is the only style needed
|
||||||
position: fixed;
|
// All other modal styles are in valerie-ui.scss
|
||||||
top: 0;
|
body.modal-open {
|
||||||
left: 0;
|
overflow: hidden;
|
||||||
width: 100vw;
|
|
||||||
height: 100vh;
|
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1050; // Ensure it's above most other content
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-container {
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 0.375rem; // 6px
|
|
||||||
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
max-height: 90vh; // Prevent modal from being too tall
|
|
||||||
overflow: hidden; // Needed for children with overflow (e.g. scrollable body)
|
|
||||||
|
|
||||||
// Default size (md)
|
|
||||||
width: 500px; // Example, adjust as needed
|
|
||||||
max-width: 90%;
|
|
||||||
|
|
||||||
&.modal-container-sm {
|
|
||||||
width: 300px;
|
|
||||||
}
|
|
||||||
&.modal-container-lg {
|
|
||||||
width: 800px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 1rem 1.25rem;
|
|
||||||
border-bottom: 1px solid #e0e0e0; // Example border
|
|
||||||
|
|
||||||
.modal-title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-button {
|
|
||||||
background: transparent;
|
|
||||||
border: none;
|
|
||||||
font-size: 1.5rem; // Make VIcon larger if it inherits font-size
|
|
||||||
padding: 0.25rem;
|
|
||||||
margin: -0.25rem; // Adjust for padding to align visual edge
|
|
||||||
line-height: 1;
|
|
||||||
cursor: pointer;
|
|
||||||
color: #6c757d; // Muted color
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #343a40;
|
|
||||||
}
|
|
||||||
// VIcon specific styling if needed, e.g., for stroke width or size
|
|
||||||
// ::v-deep(.icon) { font-size: 1.2em; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-body {
|
|
||||||
padding: 1.25rem;
|
|
||||||
overflow-y: auto; // Scrollable body if content exceeds max-height
|
|
||||||
flex-grow: 1; // Allow body to take available space
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-footer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end; // Common: buttons to the right
|
|
||||||
padding: 1rem 1.25rem;
|
|
||||||
border-top: 1px solid #e0e0e0;
|
|
||||||
gap: 0.5rem; // Space between footer items (buttons)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transitions
|
|
||||||
.modal-fade-enter-active,
|
|
||||||
.modal-fade-leave-active {
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
}
|
|
||||||
.modal-fade-enter-active .modal-container,
|
|
||||||
.modal-fade-leave-active .modal-container {
|
|
||||||
transition: transform 0.3s ease-out; // Slightly different timing for container
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-fade-enter-from,
|
|
||||||
.modal-fade-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
.modal-fade-enter-from .modal-container,
|
|
||||||
.modal-fade-leave-to .modal-container {
|
|
||||||
transform: translateY(-50px) scale(0.95); // Example: slide down and scale
|
|
||||||
}
|
|
||||||
|
|
||||||
// This class is applied to <body>
|
|
||||||
// ::v-global(body.modal-open) {
|
|
||||||
// overflow: hidden;
|
|
||||||
// }
|
|
||||||
// Note: ::v-global is not standard. This is typically handled in main CSS or via JS on body.
|
|
||||||
// The JS part `document.body.classList.add('modal-open')` is already there.
|
|
||||||
// The style for `body.modal-open` should be in a global stylesheet.
|
|
||||||
// For demo purposes, if it were here (which it shouldn't be):
|
|
||||||
// :global(body.modal-open) {
|
|
||||||
// overflow: hidden;
|
|
||||||
// }
|
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,19 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<textarea
|
<textarea :id="id" :value="modelValue" :placeholder="placeholder" :disabled="disabled" :required="required"
|
||||||
:id="id"
|
:rows="rows" :class="textareaClasses" :aria-invalid="error ? 'true' : 'false'" @input="onInput"></textarea>
|
||||||
:value="modelValue"
|
|
||||||
:placeholder="placeholder"
|
|
||||||
:disabled="disabled"
|
|
||||||
:required="required"
|
|
||||||
:rows="rows"
|
|
||||||
:class="textareaClasses"
|
|
||||||
:aria-invalid="error ? 'true' : null"
|
|
||||||
@input="onInput"
|
|
||||||
></textarea>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent, computed, PropType } from 'vue';
|
import { defineComponent, computed, type PropType } from 'vue';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'VTextarea',
|
name: 'VTextarea',
|
||||||
@ -108,7 +99,8 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
&[disabled],
|
&[disabled],
|
||||||
&[readonly] { // readonly is not a prop here, but good for general form-input style
|
&[readonly] {
|
||||||
|
// readonly is not a prop here, but good for general form-input style
|
||||||
background-color: #e9ecef;
|
background-color: #e9ecef;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
@ -116,6 +108,7 @@ export default defineComponent({
|
|||||||
|
|
||||||
&.error {
|
&.error {
|
||||||
border-color: #dc3545;
|
border-color: #dc3545;
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
border-color: #dc3545;
|
border-color: #dc3545;
|
||||||
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
|
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
|
||||||
@ -135,5 +128,4 @@ export default defineComponent({
|
|||||||
// CSS variable for rows to potentially influence height if needed by .textarea class
|
// CSS variable for rows to potentially influence height if needed by .textarea class
|
||||||
// This is an alternative way to use props.rows in CSS if you need more complex calculations.
|
// This is an alternative way to use props.rows in CSS if you need more complex calculations.
|
||||||
// For direct attribute binding like :rows="rows", this is not strictly necessary.
|
// For direct attribute binding like :rows="rows", this is not strictly necessary.
|
||||||
// :style="{ '--v-textarea-rows': rows }" could be bound to the textarea element.
|
// :style="{ '--v-textarea-rows': rows }" could be bound to the textarea element.</style>
|
||||||
</style>
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
export const API_VERSION = 'v1'
|
export const API_VERSION = 'v1'
|
||||||
|
|
||||||
// API Base URL
|
// API Base URL
|
||||||
export const API_BASE_URL = (window as any).ENV?.VITE_API_URL || 'https://mitlistbe.mohamad.dev'
|
export const API_BASE_URL = (window as any).ENV?.VITE_API_URL || 'http://localhost:8000'
|
||||||
|
|
||||||
// API Endpoints
|
// API Endpoints
|
||||||
export const API_ENDPOINTS = {
|
export const API_ENDPOINTS = {
|
||||||
|
Loading…
Reference in New Issue
Block a user