diff --git a/fe/src/assets/valerie-ui.scss b/fe/src/assets/valerie-ui.scss index 79ba0d8..b13bafd 100644 --- a/fe/src/assets/valerie-ui.scss +++ b/fe/src/assets/valerie-ui.scss @@ -37,13 +37,11 @@ /* 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"); - --progress-texture: repeating-linear-gradient( - 45deg, - rgba(0, 0, 0, 0.05), - rgba(0, 0, 0, 0.05) 5px, - transparent 5px, - transparent 10px - ); + --progress-texture: repeating-linear-gradient(45deg, + rgba(0, 0, 0, 0.05), + rgba(0, 0, 0, 0.05) 5px, + transparent 5px, + transparent 10px); } /* Accessibility Helpers */ @@ -61,6 +59,7 @@ /* Reduced Motion Preference */ @media (prefers-reduced-motion: reduce) { + *, *::before, *::after { @@ -133,8 +132,8 @@ body { /* Higher than non-focused tabs */ } -.checkbox-label input:focus-visible ~ .checkmark, -.radio-label input:focus-visible ~ .checkmark { +.checkbox-label input:focus-visible~.checkmark, +.radio-label input:focus-visible~.checkmark { outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); box-shadow: @@ -142,7 +141,7 @@ body { 0 0 0 var(--focus-outline-width) var(--focus-outline-color); } -.switch-container input:focus-visible + .switch { +.switch-container input:focus-visible+.switch { outline: var(--focus-outline); outline-offset: var(--focus-outline-offset); } @@ -211,7 +210,7 @@ h3 { margin-right: 0.5em; } -button > .icon:last-child { +button>.icon:last-child { margin-right: 0; } @@ -300,6 +299,7 @@ button > .icon:last-child { } @keyframes jiggle-subtle { + 0%, 100% { transform: translate(2px, 2px) scale(0.98) rotate(0deg); @@ -340,7 +340,7 @@ button > .icon:last-child { /* Prevent texture from interfering */ } -.card > * { +.card>* { position: relative; z-index: 1; /* Ensure content is above texture */ @@ -513,8 +513,8 @@ select.form-input { border-radius: 50%; } -.checkbox-label input:checked ~ .checkmark:after, -.radio-label input:checked ~ .checkmark:after { +.checkbox-label input:checked~.checkmark:after, +.radio-label input:checked~.checkmark:after { content: ''; position: absolute; display: block; @@ -723,12 +723,12 @@ select.form-input { /* Push to the right on larger screens */ } -.list-item-details > * { +.list-item-details>* { margin-left: 0.5rem; } /* Spacing between items in details */ -.list-item-details > :first-child { +.list-item-details> :first-child { margin-left: 0; } @@ -908,7 +908,7 @@ select.form-input { pointer-events: none; } -.tab-content > * { +.tab-content>* { position: relative; z-index: 1; } @@ -1073,7 +1073,7 @@ select.form-input { pointer-events: none; } -.alert > .alert-content { +.alert>.alert-content { /* Wrap main content */ display: flex; align-items: center; @@ -1241,11 +1241,11 @@ select.form-input { /* Vertical centering */ } -.switch-container input:checked + .switch { +.switch-container input:checked+.switch { background-color: var(--secondary-accent); } -.switch-container input:checked + .switch:before { +.switch-container input:checked+.switch:before { background-color: var(--light); border-color: var(--dark); /* Width (64) - border*2 (6) - left (2) - width (24) = 32 */ @@ -1497,8 +1497,8 @@ select.form-input { border-color: var(--dark) transparent transparent transparent; } -.tooltip .tooltip-trigger:hover + .tooltip-text, -.tooltip .tooltip-trigger:focus-visible + .tooltip-text, +.tooltip .tooltip-trigger:hover+.tooltip-text, +.tooltip .tooltip-trigger:focus-visible+.tooltip-text, .tooltip-text.visible { visibility: visible; opacity: 1; @@ -1544,6 +1544,7 @@ select.form-input { } @keyframes pulse-dot { + 0%, 80%, 100% { @@ -1678,7 +1679,7 @@ select.form-input { /* Allow badges/avatar wrap on mobile */ } - .list-item-details > * { + .list-item-details>* { margin-left: 0; margin-right: 0.5rem; margin-bottom: 0.25rem; @@ -1723,7 +1724,7 @@ select.form-input { flex-direction: column; } - .flex-layout-stack-mobile > .card { + .flex-layout-stack-mobile>.card { width: 100% !important; margin: 0 0 1.5rem 0 !important; flex-basis: auto !important; @@ -1733,12 +1734,12 @@ select.form-input { flex-direction: column; } - .form-row-wrap-mobile > .form-group { + .form-row-wrap-mobile>.form-group { margin-right: 0 !important; width: 100%; } - .form-row-wrap-mobile > .form-group:not(:last-child) { + .form-row-wrap-mobile>.form-group:not(:last-child) { margin-bottom: 1.5rem; } -} +} \ No newline at end of file diff --git a/fe/src/components/CreateListModal.vue b/fe/src/components/CreateListModal.vue index c33d4dd..ab040ed 100644 --- a/fe/src/components/CreateListModal.vue +++ b/fe/src/components/CreateListModal.vue @@ -7,7 +7,7 @@ </VFormField> <VFormField label="Description"> - <VTextarea v-model="description" rows="3" /> + <VTextarea v-model="description" :rows="3" /> </VFormField> <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 listName = 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 formErrors = ref<{ listName?: string }>({}); const notificationStore = useNotificationStore(); @@ -61,14 +62,8 @@ const listNameInput = ref<InstanceType<typeof VInput> | null>(null); // const modalContainerRef = ref<HTMLElement | null>(null); // Removed const groupOptionsForSelect = computed(() => { - const options = props.groups ? props.groups.map(g => ({ label: g.label, value: g.value })) : []; - // VSelect expects placeholder to be passed as a prop, not as an option for empty value usually - // 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]; + // VSelect's placeholder should work if selectedGroupId is the sentinel value + return props.groups ? props.groups.map(g => ({ label: g.label, value: g.value })) : []; }); @@ -77,10 +72,15 @@ watch(isOpen, (newVal) => { // Reset form when opening listName.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 = {}; 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; try { - const response = await apiClient.post(API_ENDPOINTS.LISTS.BASE, { + const payload = { name: listName.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' }); @@ -125,7 +126,7 @@ const onSubmit = async () => { }; </script> -<style scoped> +<style> .form-error-text { color: var(--danger); font-size: 0.85rem; diff --git a/fe/src/components/valerie/VModal.vue b/fe/src/components/valerie/VModal.vue index 4d2568b..ce85599 100644 --- a/fe/src/components/valerie/VModal.vue +++ b/fe/src/components/valerie/VModal.vue @@ -1,34 +1,14 @@ <template> <Teleport to="body"> - <Transition - name="modal-fade" - @after-enter="onOpened" - @after-leave="onClosed" - > - <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 - > + <Transition name="modal-fade" @after-enter="onOpened" @after-leave="onClosed"> + <div v-if="modelValue" class="modal-backdrop open" @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"> <slot name="header"> <h3 v-if="title" :id="titleId" class="modal-title">{{ title }}</h3> - <button - v-if="!hideCloseButton" - type="button" - class="close-button" - @click="closeModal" - aria-label="Close modal" - > + <button v-if="!hideCloseButton" type="button" class="close-button" @click="closeModal" + aria-label="Close modal"> <VIcon name="close" /> </button> </slot> @@ -131,115 +111,10 @@ const onClosed = () => { </script> -<style lang="scss" scoped> -.modal-backdrop { - position: fixed; - top: 0; - left: 0; - 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 +<style lang="scss"> +// Global style for body when modal is open - this is the only style needed +// All other modal styles are in valerie-ui.scss +body.modal-open { + overflow: hidden; } - -.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> diff --git a/fe/src/components/valerie/VTextarea.vue b/fe/src/components/valerie/VTextarea.vue index daa795f..ef1113c 100644 --- a/fe/src/components/valerie/VTextarea.vue +++ b/fe/src/components/valerie/VTextarea.vue @@ -1,19 +1,10 @@ <template> - <textarea - :id="id" - :value="modelValue" - :placeholder="placeholder" - :disabled="disabled" - :required="required" - :rows="rows" - :class="textareaClasses" - :aria-invalid="error ? 'true' : null" - @input="onInput" - ></textarea> + <textarea :id="id" :value="modelValue" :placeholder="placeholder" :disabled="disabled" :required="required" + :rows="rows" :class="textareaClasses" :aria-invalid="error ? 'true' : 'false'" @input="onInput"></textarea> </template> <script lang="ts"> -import { defineComponent, computed, PropType } from 'vue'; +import { defineComponent, computed, type PropType } from 'vue'; export default defineComponent({ name: 'VTextarea', @@ -108,7 +99,8 @@ export default defineComponent({ } &[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; opacity: 1; cursor: not-allowed; @@ -116,6 +108,7 @@ export default defineComponent({ &.error { border-color: #dc3545; + &:focus { border-color: #dc3545; 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 // 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. -// :style="{ '--v-textarea-rows': rows }" could be bound to the textarea element. -</style> +// :style="{ '--v-textarea-rows': rows }" could be bound to the textarea element.</style> diff --git a/fe/src/config/api-config.ts b/fe/src/config/api-config.ts index 052aade..19a3f4f 100644 --- a/fe/src/config/api-config.ts +++ b/fe/src/config/api-config.ts @@ -2,7 +2,7 @@ export const API_VERSION = 'v1' // 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 export const API_ENDPOINTS = {