From 32d04123f682ad514d71c3994ebae1f89f1f91fb Mon Sep 17 00:00:00 2001 From: DhwaniKhandelwal-tech Date: Tue, 7 Jul 2026 20:31:57 +0530 Subject: [PATCH 01/25] fixed new branch bugs --- frontend/src/assets/styles/Composer.css | 37 ++++++++ frontend/src/assets/styles/ContentGrid.css | 80 +++++++++++++++++ frontend/src/assets/styles/FabricChat.css | 61 +++++++++++++ frontend/src/assets/styles/FabricSearch.css | 98 +++++++++++++++++++++ 4 files changed, 276 insertions(+) diff --git a/frontend/src/assets/styles/Composer.css b/frontend/src/assets/styles/Composer.css index bb80954..2cd6f9d 100644 --- a/frontend/src/assets/styles/Composer.css +++ b/frontend/src/assets/styles/Composer.css @@ -1014,3 +1014,40 @@ max-width: 100% ; } } + +/* Senior QA patch: Composer readability guard after global theme changes. */ +.composer .composer-input { + color: var(--tz-ui-text, #0f172a); + background: var(--tz-ui-surface, #ffffff); + border-color: var(--tz-ui-border, rgba(15, 23, 42, 0.14)); + opacity: 1; +} + +.composer .composer-input::placeholder { + color: var(--tz-ui-text-muted, #64748b); + opacity: 1; +} + +.composer .attach-btn.inside, +.composer .record-btn { + color: var(--tz-ui-text, #0f172a); + background: var(--tz-ui-control-bg, #ffffff); + border-color: var(--tz-ui-border, rgba(15, 23, 42, 0.14)); +} + +:root[data-theme="dark"] .composer .composer-input { + color: #f8fafc; + background: rgba(10, 15, 29, 0.92); + border-color: rgba(226, 232, 240, 0.24); +} + +:root[data-theme="dark"] .composer .composer-input::placeholder { + color: #cbd5e1; +} + +:root[data-theme="dark"] .composer .attach-btn.inside, +:root[data-theme="dark"] .composer .record-btn { + color: #f8fafc; + background: rgba(30, 41, 59, 0.96); + border-color: rgba(226, 232, 240, 0.24); +} diff --git a/frontend/src/assets/styles/ContentGrid.css b/frontend/src/assets/styles/ContentGrid.css index 28a4dc5..6b92a78 100644 --- a/frontend/src/assets/styles/ContentGrid.css +++ b/frontend/src/assets/styles/ContentGrid.css @@ -385,3 +385,83 @@ figcaption { bottom: 58px; } } + +/* Senior QA patch: Fabric List contrast fixes without !important. + Keep pagination, totals, card meta, and audio labels readable in both themes. */ +.grid-page .grid-header, +.grid-page .grid-controls.inline, +.grid-page .media-card, +.grid-page .audio-box { + color: var(--tz-ui-text, var(--tz-cotton, #f8fafc)); +} + +.grid-page .grid-title-text, +.grid-page .grid-count-inline, +.grid-page .grid-controls.inline .grid-page { + color: var(--tz-ui-text, #f8fafc); + opacity: 1; + text-shadow: none; +} + +.grid-page .grid-count-inline { + font-weight: 850; +} + +.grid-page .grid-controls.inline .grid-page { + display: inline-flex; + align-items: center; + min-height: 34px; + padding: 0 12px; + border: 1px solid var(--tz-ui-border, rgba(226, 232, 240, 0.24)); + border-radius: 8px; + background: var(--tz-ui-surface-raised, rgba(30, 41, 59, 0.96)); + font-size: 14px; + font-weight: 800; +} + +.grid-page .grid-controls.inline button { + border-color: var(--tz-ui-border, rgba(226, 232, 240, 0.24)); + background: var(--tz-ui-control-bg, rgba(30, 41, 59, 0.94)); + color: var(--tz-ui-text, #f8fafc); +} + +.grid-page .grid-controls.inline button:hover:not(:disabled) { + background: var(--tz-ui-control-hover-bg, rgba(51, 65, 85, 0.98)); +} + +.grid-page .audio-box { + background: var(--tz-ui-surface-raised, rgba(30, 41, 59, 0.96)); + border: 1px solid var(--tz-ui-border, rgba(226, 232, 240, 0.24)); +} + +.grid-page .audio-label { + color: var(--tz-ui-text, #f8fafc); + opacity: 1; + text-shadow: none; +} + +.grid-page .media-meta, +.grid-page .media-meta time, +.grid-page .grid-page-subtitle { + color: var(--tz-ui-text-muted, #cbd5e1); + opacity: 1; +} + +:root[data-theme="light"] .grid-page .grid-title-text, +:root[data-theme="light"] .grid-page .grid-count-inline, +:root[data-theme="light"] .grid-page .grid-controls.inline .grid-page, +:root[data-theme="light"] .grid-page .audio-label { + color: #0f172a; +} + +:root[data-theme="light"] .grid-page .audio-box, +:root[data-theme="light"] .grid-page .grid-controls.inline .grid-page { + background: #ffffff; + border-color: rgba(15, 23, 42, 0.14); +} + +:root[data-theme="light"] .grid-page .media-meta, +:root[data-theme="light"] .grid-page .media-meta time, +:root[data-theme="light"] .grid-page .grid-page-subtitle { + color: #475569; +} diff --git a/frontend/src/assets/styles/FabricChat.css b/frontend/src/assets/styles/FabricChat.css index 8f91696..25bafec 100644 --- a/frontend/src/assets/styles/FabricChat.css +++ b/frontend/src/assets/styles/FabricChat.css @@ -390,3 +390,64 @@ padding: 0 ; } } + +/* Senior QA patch: Chat text contrast after theme toggle. + Keeps empty-state heading/subtitle, topbar labels, composer text and action labels readable. */ +.page-root .chat-card-topbar, +.page-root .chat-body, +.page-root .empty-state-large, +.page-root .composer { + color: var(--tz-ui-text, var(--tz-cotton, #f8fafc)); +} + +.page-root .chat-card-title .main, +.page-root .empty-state-large h3 { + color: var(--tz-ui-text, #f8fafc); + opacity: 1; + text-shadow: none; +} + +.page-root .empty-state-large p, +.page-root .audio-label, +.page-root .stop-text { + color: var(--tz-ui-text-muted, #cbd5e1); + opacity: 1; + text-shadow: none; +} + +.page-root .chat-card-actions .btn, +.page-root .chat-card-actions .btn span, +.page-root .chat-card-actions .chat-action-icon { + color: inherit; + opacity: 1; + text-shadow: none; +} + +.page-root .chat-card-actions .btn-secondary { + background: var(--tz-turmeric, #e0a82e); + border-color: var(--tz-turmeric, #e0a82e); + color: #171101; +} + +.page-root .chat-card-actions .btn-ghost { + background: var(--tz-ui-control-bg, rgba(30, 41, 59, 0.94)); + border-color: var(--tz-ui-border, rgba(226, 232, 240, 0.24)); + color: var(--tz-ui-text, #f8fafc); +} + +:root[data-theme="light"] .page-root .chat-card-title .main, +:root[data-theme="light"] .page-root .empty-state-large h3, +:root[data-theme="light"] .page-root .chat-card-actions .btn-ghost { + color: #0f172a; +} + +:root[data-theme="light"] .page-root .empty-state-large p, +:root[data-theme="light"] .page-root .audio-label, +:root[data-theme="light"] .page-root .stop-text { + color: #475569; +} + +:root[data-theme="light"] .page-root .chat-card-actions .btn-ghost { + background: #ffffff; + border-color: rgba(15, 23, 42, 0.14); +} diff --git a/frontend/src/assets/styles/FabricSearch.css b/frontend/src/assets/styles/FabricSearch.css index 75391f8..1c4bc75 100644 --- a/frontend/src/assets/styles/FabricSearch.css +++ b/frontend/src/assets/styles/FabricSearch.css @@ -2412,3 +2412,101 @@ max-height: calc(100dvh - 126px) ; } } + +/* Senior QA patch: Search page visibility + settings dropdown layout. + Fixes low-contrast category/database labels and prevents the settings menu + from covering original/cropped images in image-search preview mode. */ +.fabric-search .category-picker, +.fabric-search .db-panel, +.fabric-search .settings-panel__toggle, +.fabric-search .settings-panel__content { + color: var(--tz-ui-text, var(--color-text, #f8fafc)); +} + +.fabric-search .category-picker__title, +.fabric-search .db-panel__label { + color: var(--tz-ui-text-muted, #cbd5e1); + opacity: 1; + text-shadow: none; +} + +.fabric-search .category-picker__toggle-all { + color: var(--tz-green, var(--color-green, #22c55e)); + opacity: 1; +} + +.fabric-search .settings-panel__toggle, +.fabric-search .settings-panel__toggle span { + color: var(--tz-ui-text, var(--color-text, #f8fafc)); + opacity: 1; + text-shadow: none; +} + +.fabric-search .settings-panel__content .db-panel { + background: var(--tz-ui-surface-raised, var(--color-surface, rgba(20, 16, 12, 0.94))); + border-color: var(--tz-ui-border, var(--color-border, rgba(255, 255, 255, 0.12))); + box-shadow: 0 18px 42px rgba(0, 0, 0, 0.24); +} + +.fabric-search .db-panel__btn { + opacity: 1; + text-shadow: none; +} + +.fabric-search.fabric-search--previewing .fabric-search__top-bar { + align-items: flex-start; + justify-content: flex-end; + margin-bottom: 18px; + min-height: auto; + position: relative; + z-index: 30; +} + +.fabric-search.fabric-search--previewing .fabric-search__settings { + width: 100%; + display: flex; + justify-content: flex-end; +} + +.fabric-search.fabric-search--previewing .settings-panel { + display: flex; + flex-direction: column; + align-items: flex-end; + max-width: min(360px, 100%); +} + +.fabric-search.fabric-search--previewing .settings-panel__content { + position: static; + margin-top: 10px; + min-width: min(340px, calc(100vw - 48px)); + max-width: 100%; +} + +.fabric-search.fabric-search--previewing .image-preview { + position: relative; + z-index: 1; +} + +:root[data-theme="light"] .fabric-search .category-picker__title, +:root[data-theme="light"] .fabric-search .db-panel__label, +:root[data-theme="light"] .fabric-search .settings-panel__toggle, +:root[data-theme="light"] .fabric-search .settings-panel__toggle span { + color: #0f172a; +} + +:root[data-theme="light"] .fabric-search .settings-panel__content .db-panel { + background: #ffffff; + border-color: rgba(15, 23, 42, 0.14); +} + +@media (max-width: 760px) { + .fabric-search.fabric-search--previewing .fabric-search__settings, + .fabric-search.fabric-search--previewing .settings-panel { + align-items: stretch; + } + + .fabric-search.fabric-search--previewing .settings-panel__toggle, + .fabric-search.fabric-search--previewing .settings-panel__content { + width: 100%; + } +} From 018831ff6dbbe322fd2fee009a05efea8207e83a Mon Sep 17 00:00:00 2001 From: DhwaniKhandelwal-tech Date: Wed, 8 Jul 2026 20:50:40 +0530 Subject: [PATCH 02/25] broke search page into smaller react components --- frontend/src/assets/styles/Composer.css | 37 + frontend/src/assets/styles/ContentGrid.css | 159 +++ frontend/src/assets/styles/EmptyState.css | 69 +- frontend/src/assets/styles/FabricChat.css | 167 +++ frontend/src/assets/styles/FabricSearch.css | 239 +++++ frontend/src/assets/styles/_makeover.css | 32 + .../src/components/search/CategoryPicker.tsx | 70 ++ frontend/src/components/search/CropDrawer.tsx | 74 ++ .../src/components/search/DbControlPanel.tsx | 67 ++ .../src/components/search/ImagePreview.tsx | 76 ++ frontend/src/components/search/Lightbox.tsx | 93 ++ .../src/components/search/LimitSlider.tsx | 27 + frontend/src/components/search/Pagination.tsx | 21 + frontend/src/components/search/ResultCard.tsx | 52 + .../src/components/search/ResultsSection.tsx | 49 + frontend/src/components/search/SearchHero.tsx | 89 ++ .../src/components/search/SettingsPanel.tsx | 27 + .../src/components/search/StickySearchBar.tsx | 114 ++ .../src/components/search/searchConfig.ts | 15 + frontend/src/components/search/searchUtils.ts | 70 ++ frontend/src/components/search/types.ts | 23 + frontend/src/components/search/useSearch.ts | 69 ++ frontend/src/pages/FabricList.tsx | 25 +- frontend/src/pages/FabricSearch.tsx | 973 +----------------- 24 files changed, 1674 insertions(+), 963 deletions(-) create mode 100644 frontend/src/components/search/CategoryPicker.tsx create mode 100644 frontend/src/components/search/CropDrawer.tsx create mode 100644 frontend/src/components/search/DbControlPanel.tsx create mode 100644 frontend/src/components/search/ImagePreview.tsx create mode 100644 frontend/src/components/search/Lightbox.tsx create mode 100644 frontend/src/components/search/LimitSlider.tsx create mode 100644 frontend/src/components/search/Pagination.tsx create mode 100644 frontend/src/components/search/ResultCard.tsx create mode 100644 frontend/src/components/search/ResultsSection.tsx create mode 100644 frontend/src/components/search/SearchHero.tsx create mode 100644 frontend/src/components/search/SettingsPanel.tsx create mode 100644 frontend/src/components/search/StickySearchBar.tsx create mode 100644 frontend/src/components/search/searchConfig.ts create mode 100644 frontend/src/components/search/searchUtils.ts create mode 100644 frontend/src/components/search/types.ts create mode 100644 frontend/src/components/search/useSearch.ts diff --git a/frontend/src/assets/styles/Composer.css b/frontend/src/assets/styles/Composer.css index 2cd6f9d..0428e01 100644 --- a/frontend/src/assets/styles/Composer.css +++ b/frontend/src/assets/styles/Composer.css @@ -1051,3 +1051,40 @@ background: rgba(30, 41, 59, 0.96); border-color: rgba(226, 232, 240, 0.24); } + +/* Senior QA: Composer readability guard after global theme changes. */ +.composer .composer-input { + color: var(--tz-ui-text, #0f172a); + background: var(--tz-ui-surface, #ffffff); + border-color: var(--tz-ui-border, rgba(15, 23, 42, 0.14)); + opacity: 1; +} + +.composer .composer-input::placeholder { + color: var(--tz-ui-text-muted, #64748b); + opacity: 1; +} + +.composer .attach-btn.inside, +.composer .record-btn { + color: var(--tz-ui-text, #0f172a); + background: var(--tz-ui-control-bg, #ffffff); + border-color: var(--tz-ui-border, rgba(15, 23, 42, 0.14)); +} + +:root[data-theme="dark"] .composer .composer-input { + color: #f8fafc; + background: rgba(10, 15, 29, 0.92); + border-color: rgba(226, 232, 240, 0.24); +} + +:root[data-theme="dark"] .composer .composer-input::placeholder { + color: #cbd5e1; +} + +:root[data-theme="dark"] .composer .attach-btn.inside, +:root[data-theme="dark"] .composer .record-btn { + color: #f8fafc; + background: rgba(30, 41, 59, 0.96); + border-color: rgba(226, 232, 240, 0.24); +} diff --git a/frontend/src/assets/styles/ContentGrid.css b/frontend/src/assets/styles/ContentGrid.css index 6b92a78..3d211e3 100644 --- a/frontend/src/assets/styles/ContentGrid.css +++ b/frontend/src/assets/styles/ContentGrid.css @@ -465,3 +465,162 @@ figcaption { :root[data-theme="light"] .grid-page .grid-page-subtitle { color: #475569; } + +/* Senior QA: Fabric List contrast fixes without forced-priority rules. + Keep pagination, totals, card meta, and audio labels readable in both themes. */ +.grid-page .grid-header, +.grid-page .grid-controls.inline, +.grid-page .media-card, +.grid-page .audio-box { + color: var(--tz-ui-text, var(--tz-cotton, #f8fafc)); +} + +.grid-page .grid-title-text, +.grid-page .grid-count-inline, +.grid-page .grid-controls.inline .grid-page-indicator { + color: var(--tz-ui-text, #f8fafc); + opacity: 1; + text-shadow: none; +} + +.grid-page .grid-count-inline { + font-weight: 850; +} + +.grid-page .grid-controls.inline .grid-page-indicator { + display: inline-flex; + align-items: center; + min-height: 34px; + padding: 0 12px; + border: 1px solid var(--tz-ui-border, rgba(226, 232, 240, 0.24)); + border-radius: 8px; + background: var(--tz-ui-surface-raised, rgba(30, 41, 59, 0.96)); + font-size: 14px; + font-weight: 800; +} + +.grid-page .grid-controls.inline button { + border-color: var(--tz-ui-border, rgba(226, 232, 240, 0.24)); + background: var(--tz-ui-control-bg, rgba(30, 41, 59, 0.94)); + color: var(--tz-ui-text, #f8fafc); +} + +.grid-page .grid-controls.inline button:hover:not(:disabled) { + background: var(--tz-ui-control-hover-bg, rgba(51, 65, 85, 0.98)); +} + +.grid-page .audio-box { + background: var(--tz-ui-surface-raised, rgba(30, 41, 59, 0.96)); + border: 1px solid var(--tz-ui-border, rgba(226, 232, 240, 0.24)); +} + +.grid-page .audio-label { + color: var(--tz-ui-text, #f8fafc); + opacity: 1; + text-shadow: none; +} + +.grid-page .media-meta, +.grid-page .media-meta time, +.grid-page .grid-page-subtitle { + color: var(--tz-ui-text-muted, #cbd5e1); + opacity: 1; +} + +:root[data-theme="light"] .grid-page .grid-title-text, +:root[data-theme="light"] .grid-page .grid-count-inline, +:root[data-theme="light"] .grid-page .grid-controls.inline .grid-page-indicator, +:root[data-theme="light"] .grid-page .audio-label { + color: #0f172a; +} + +:root[data-theme="light"] .grid-page .audio-box, +:root[data-theme="light"] .grid-page .grid-controls.inline .grid-page-indicator { + background: #ffffff; + border-color: rgba(15, 23, 42, 0.14); +} + +:root[data-theme="light"] .grid-page .media-meta, +:root[data-theme="light"] .grid-page .media-meta time, +:root[data-theme="light"] .grid-page .grid-page-subtitle { + color: #475569; +} + +/* Senior visual-system pass: list page readable metadata, intentional disabled state, consistent card rhythm. */ +.grid-page { + color: var(--tz-ui-text, var(--tz-cotton, #f8fafc)); + min-height: calc(100dvh - 68px); + padding-block: clamp(18px, 3vh, 32px) 48px; +} + +.grid-page .grid-header { + align-items: center; + margin: 24px 0 18px; +} + +.grid-page .grid-title-text, +.grid-page .grid-count-inline, +.grid-page .grid-controls.inline .grid-page-indicator { + color: var(--tz-ui-text, #f8fafc); +} + +.grid-page .grid-controls.inline button:disabled { + opacity: 1; + color: var(--tz-ui-text-faint, #94a3b8); + background: var(--tz-ui-surface-subtle, rgba(30, 41, 59, 0.58)); + border-color: var(--tz-ui-border, rgba(226, 232, 240, 0.2)); + cursor: not-allowed; +} + +.grid-page .media-grid { + align-items: stretch; +} + +.grid-page .media-card { + background: var(--tz-ui-surface-raised, rgba(23, 31, 51, 0.96)); + border-color: var(--tz-ui-border, rgba(226, 232, 240, 0.22)); + box-shadow: var(--tz-ui-shadow-panel, 0 18px 48px rgba(0, 0, 0, 0.22)); +} + +.grid-page .media-thumb img { + height: 250px; + object-fit: cover; +} + +.grid-page .media-name { + min-height: 34px; + padding: 8px 12px; + color: var(--tz-ui-text, #f8fafc); + background: rgba(15, 23, 42, 0.72); + font-style: normal; + font-weight: 800; + line-height: 1.2; +} + +.grid-page .media-meta { + border-top-color: var(--tz-ui-border, rgba(226, 232, 240, 0.22)); + color: var(--tz-ui-text-muted, #cbd5e1); +} + +.grid-page .audio-label { + color: var(--tz-ui-text, #f8fafc); + font-weight: 800; + letter-spacing: 0.01em; +} + +:root[data-theme="light"] .grid-page .media-card, +:root[data-theme="light"] .grid-page .audio-box { + background: var(--tz-ui-surface-raised, #ffffff); + border-color: var(--tz-ui-border, rgba(15, 23, 42, 0.14)); +} + +:root[data-theme="light"] .grid-page .media-name { + color: #0f172a; + background: rgba(255, 255, 255, 0.92); +} + +:root[data-theme="light"] .grid-page .grid-controls.inline button:disabled { + color: #64748b; + background: #f1f5f9; + border-color: rgba(15, 23, 42, 0.12); +} diff --git a/frontend/src/assets/styles/EmptyState.css b/frontend/src/assets/styles/EmptyState.css index cf26569..247ce25 100644 --- a/frontend/src/assets/styles/EmptyState.css +++ b/frontend/src/assets/styles/EmptyState.css @@ -64,4 +64,71 @@ .empty-state-large .chip:nth-child(even) { background: #aeafb23f; border-color: #e5e7eb; -} \ No newline at end of file +} +/* Senior QA: keep chat landing copy and prompt chips readable after theme changes. */ +.page-root .empty-state-large h3 { + color: var(--tz-ui-text, var(--tz-cotton, #f8fafc)); + opacity: 1; + text-shadow: none; +} + +.page-root .empty-state-large p { + color: var(--tz-ui-text-muted, var(--tz-muted, #cbd5e1)); + opacity: 1; + text-shadow: none; +} + +.page-root .empty-state-large .chip, +.page-root .empty-state-large .chip:nth-child(odd), +.page-root .empty-state-large .chip:nth-child(even) { + color: var(--tz-ui-text, #0f172a); + background: var(--tz-ui-control-bg, rgba(255, 255, 255, 0.94)); + border-color: var(--tz-ui-border, rgba(15, 23, 42, 0.14)); + opacity: 1; + text-shadow: none; +} + +:root[data-theme="dark"] .page-root .empty-state-large .chip, +:root[data-theme="dark"] .page-root .empty-state-large .chip:nth-child(odd), +:root[data-theme="dark"] .page-root .empty-state-large .chip:nth-child(even) { + color: #f8fafc; + background: rgba(30, 41, 59, 0.96); + border-color: rgba(226, 232, 240, 0.24); +} + +:root[data-theme="light"] .page-root .empty-state-large .chip, +:root[data-theme="light"] .page-root .empty-state-large .chip:nth-child(odd), +:root[data-theme="light"] .page-root .empty-state-large .chip:nth-child(even) { + color: #0f172a; + background: rgba(255, 255, 255, 0.96); + border-color: rgba(15, 23, 42, 0.14); +} + +/* Senior visual-system pass: prompt chips should never look blank or disabled by accident. */ +.page-root .empty-state-large .chips { + gap: 12px; +} + +.page-root .empty-state-large .chip, +.page-root .empty-state-large .chip:nth-child(odd), +.page-root .empty-state-large .chip:nth-child(even) { + min-height: 40px; + color: var(--tz-ui-text, #0f172a); + background: var(--tz-ui-control-bg, rgba(255, 255, 255, 0.96)); + border-color: var(--tz-ui-border, rgba(15, 23, 42, 0.14)); + font-weight: 800; + box-shadow: 0 10px 26px rgba(15, 23, 42, 0.08); +} + +.page-root .empty-state-large .chip:disabled { + opacity: 0.72; + color: var(--tz-ui-text-muted, #64748b); +} + +:root[data-theme="dark"] .page-root .empty-state-large .chip, +:root[data-theme="dark"] .page-root .empty-state-large .chip:nth-child(odd), +:root[data-theme="dark"] .page-root .empty-state-large .chip:nth-child(even) { + color: var(--tz-ui-text, #f8fafc); + background: var(--tz-ui-control-bg, rgba(30, 41, 59, 0.96)); + border-color: var(--tz-ui-border, rgba(226, 232, 240, 0.24)); +} diff --git a/frontend/src/assets/styles/FabricChat.css b/frontend/src/assets/styles/FabricChat.css index 25bafec..ff2983c 100644 --- a/frontend/src/assets/styles/FabricChat.css +++ b/frontend/src/assets/styles/FabricChat.css @@ -451,3 +451,170 @@ background: #ffffff; border-color: rgba(15, 23, 42, 0.14); } + +/* Senior QA: chat readability and bottom-control clipping fixes without forced-priority rules. */ +.page-root .chat-card-topbar, +.page-root .chat-body, +.page-root .empty-state-large, +.page-root .composer { + color: var(--tz-ui-text, var(--tz-cotton, #f8fafc)); +} + +.page-root .chat-card-title .main, +.page-root .empty-state-large h3 { + color: var(--tz-ui-text, #f8fafc); + opacity: 1; + text-shadow: none; +} + +.page-root .empty-state-large p, +.page-root .audio-label, +.page-root .stop-text { + color: var(--tz-ui-text-muted, #cbd5e1); + opacity: 1; + text-shadow: none; +} + +.page-root .chat-card-actions .btn, +.page-root .chat-card-actions .btn span, +.page-root .chat-card-actions .chat-action-icon { + color: inherit; + opacity: 1; + text-shadow: none; +} + +.page-root .chat-card-actions .btn-secondary { + background: var(--tz-turmeric, #e0a82e); + border-color: var(--tz-turmeric, #e0a82e); + color: #171101; +} + +.page-root .chat-card-actions .btn-ghost { + background: var(--tz-ui-control-bg, rgba(30, 41, 59, 0.94)); + border-color: var(--tz-ui-border, rgba(226, 232, 240, 0.24)); + color: var(--tz-ui-text, #f8fafc); +} + +.page-root .chat-body { + padding-bottom: max(12px, env(safe-area-inset-bottom)); +} + +.page-root .chat-scroll-area { + padding-bottom: 24px; +} + +.page-root .composer { + flex: 0 0 auto; + padding: 8px 8px max(14px, env(safe-area-inset-bottom)); + border-radius: 16px 16px 0 0; +} + +.page-root .composer-audio-row { + margin-top: 8px; + margin-right: 8px; + padding-bottom: 2px; +} + +.page-root .record-btn, +.page-root .audio-label, +.page-root .stop-text { + line-height: 1.2; +} + +:root[data-theme="light"] .page-root .chat-card-title .main, +:root[data-theme="light"] .page-root .empty-state-large h3, +:root[data-theme="light"] .page-root .chat-card-actions .btn-ghost { + color: #0f172a; +} + +:root[data-theme="light"] .page-root .empty-state-large p, +:root[data-theme="light"] .page-root .audio-label, +:root[data-theme="light"] .page-root .stop-text { + color: #475569; +} + +:root[data-theme="light"] .page-root .chat-card-actions .btn-ghost { + background: #ffffff; + border-color: rgba(15, 23, 42, 0.14); +} + +/* Senior visual-system pass: chat should read as one stable product surface. */ +.page-root { + min-height: calc(100dvh - 64px); + background: var(--tz-ui-page, var(--tz-app-bg)); + color: var(--tz-ui-text, var(--tz-cotton, #f8fafc)); +} + +.page-root .chat-card-topbar { + width: min(1048px, calc(100vw - 32px)); + margin: clamp(10px, 2vh, 18px) auto 12px; + padding: 12px 14px; + background: var(--tz-ui-surface, rgba(15, 23, 42, 0.78)); + border: 1px solid var(--tz-ui-border, rgba(226, 232, 240, 0.2)); + border-radius: 18px; + box-shadow: var(--tz-ui-shadow-panel, 0 18px 48px rgba(0, 0, 0, 0.22)); +} + +.page-root .chat-body { + width: min(1048px, calc(100vw - 32px)); + height: auto; + min-height: calc(100dvh - 162px); + margin: 0 auto; + display: flex; + flex-direction: column; +} + +.page-root.is-empty .chat-scroll-area { + display: flex; + flex-direction: column; + justify-content: flex-start; + padding-top: clamp(24px, 6vh, 66px); + padding-bottom: 20px; +} + +.page-root .empty-state-large h3 { + color: var(--tz-ui-text, #f8fafc); + font-size: clamp(22px, 2.4vw, 30px); + letter-spacing: -0.025em; +} + +.page-root .empty-state-large p { + color: var(--tz-ui-text-muted, #cbd5e1); + font-size: 15px; +} + +.page-root .chat-card-actions .btn { + min-width: 0; + white-space: nowrap; +} + +.page-root .composer { + width: 100%; + margin: auto auto 0; + background: var(--tz-ui-surface, rgba(15, 23, 42, 0.78)); + border: 1px solid var(--tz-ui-border, rgba(226, 232, 240, 0.2)); + box-shadow: var(--tz-ui-shadow-panel, 0 18px 48px rgba(0, 0, 0, 0.22)); +} + +.page-root .composer-audio-row { + justify-content: flex-end; + flex-wrap: wrap; + gap: 10px; + color: var(--tz-ui-text-muted, #cbd5e1); +} + +:root[data-theme="light"] .page-root .chat-card-topbar, +:root[data-theme="light"] .page-root .composer { + background: var(--tz-ui-surface-raised, rgba(255, 255, 255, 0.96)); + border-color: var(--tz-ui-border, rgba(15, 23, 42, 0.14)); +} + +@media (max-height: 760px) { + .page-root.is-empty .chat-scroll-area { + padding-top: 18px; + } + + .page-root .empty-state-large h3 { + font-size: 22px; + } +} diff --git a/frontend/src/assets/styles/FabricSearch.css b/frontend/src/assets/styles/FabricSearch.css index 1c4bc75..78e322a 100644 --- a/frontend/src/assets/styles/FabricSearch.css +++ b/frontend/src/assets/styles/FabricSearch.css @@ -2510,3 +2510,242 @@ width: 100%; } } + +/* Senior QA: Search visibility + image preview layout guards after component split. */ +.fabric-search:not(.fabric-search--has-results) .fabric-search__body { + padding-top: clamp(48px, 7vh, 88px); +} + +.fabric-search .category-picker, +.fabric-search .db-panel, +.fabric-search .settings-panel__toggle, +.fabric-search .settings-panel__content { + color: var(--tz-ui-text, var(--color-text, #f8fafc)); +} + +.fabric-search .category-picker__title, +.fabric-search .db-panel__label, +.fabric-search .limit-slider__label, +.fabric-search .hero__subtitle, +.fabric-search .hero__divider { + color: var(--tz-ui-text-muted, #cbd5e1); + opacity: 1; + text-shadow: none; +} + +.fabric-search .category-picker__toggle-all { + color: var(--tz-green, var(--color-green, #22c55e)); + opacity: 1; +} + +.fabric-search .settings-panel__toggle, +.fabric-search .settings-panel__toggle span { + color: var(--tz-ui-text, var(--color-text, #f8fafc)); + opacity: 1; + text-shadow: none; +} + +.fabric-search .settings-panel__content .db-panel { + background: var(--tz-ui-surface-raised, var(--color-surface, rgba(20, 16, 12, 0.94))); + border-color: var(--tz-ui-border, var(--color-border, rgba(255, 255, 255, 0.12))); + box-shadow: 0 18px 42px rgba(0, 0, 0, 0.24); +} + +.fabric-search .db-panel__btn { + opacity: 1; + text-shadow: none; +} + +.fabric-search.fabric-search--previewing .fabric-search__top-bar { + align-items: flex-start; + justify-content: flex-end; + margin-bottom: 18px; + min-height: auto; + position: relative; + z-index: 30; +} + +.fabric-search.fabric-search--previewing .fabric-search__settings { + width: 100%; + display: flex; + justify-content: flex-end; +} + +.fabric-search.fabric-search--previewing .settings-panel { + display: flex; + flex-direction: column; + align-items: flex-end; + max-width: min(360px, 100%); +} + +.fabric-search.fabric-search--previewing .settings-panel__content { + position: static; + margin-top: 10px; + min-width: min(340px, calc(100vw - 48px)); + max-width: 100%; +} + +.fabric-search.fabric-search--previewing .image-preview { + position: relative; + z-index: 1; +} + +:root[data-theme="light"] .fabric-search .category-picker__title, +:root[data-theme="light"] .fabric-search .db-panel__label, +:root[data-theme="light"] .fabric-search .settings-panel__toggle, +:root[data-theme="light"] .fabric-search .settings-panel__toggle span, +:root[data-theme="light"] .fabric-search .limit-slider__label, +:root[data-theme="light"] .fabric-search .hero__subtitle, +:root[data-theme="light"] .fabric-search .hero__divider { + color: #0f172a; +} + +:root[data-theme="light"] .fabric-search .settings-panel__content .db-panel { + background: #ffffff; + border-color: rgba(15, 23, 42, 0.14); +} + +@media (max-width: 760px) { + .fabric-search.fabric-search--previewing .fabric-search__settings, + .fabric-search.fabric-search--previewing .settings-panel { + align-items: stretch; + } + + .fabric-search.fabric-search--previewing .settings-panel__toggle, + .fabric-search.fabric-search--previewing .settings-panel__content { + width: 100%; + } +} + +/* Senior visual-system pass: search layout/readability polish after component split. */ +.fabric-search { + color: var(--tz-ui-text, var(--color-text, #f8fafc)); + background: var(--tz-ui-page, var(--color-bg, #0b1020)); + min-height: calc(100dvh - 64px); +} + +.fabric-search:not(.fabric-search--has-results):not(.fabric-search--previewing) .fabric-search__body { + padding-top: clamp(36px, 6vh, 72px); + padding-bottom: clamp(56px, 8vh, 96px); +} + +.fabric-search .hero { + margin-top: 0; +} + +.fabric-search .hero__title { + color: var(--tz-ui-text, #f8fafc); + text-shadow: none; +} + +.fabric-search .hero__subtitle, +.fabric-search .hero__divider, +.fabric-search .category-picker__title, +.fabric-search .db-panel__label, +.fabric-search .limit-slider__label, +.fabric-search .limit-slider__value { + color: var(--tz-ui-text-muted, #cbd5e1); + opacity: 1; +} + +.fabric-search .search-bar, +.fabric-search .category-picker, +.fabric-search .image-preview, +.fabric-search .results-section, +.fabric-search .settings-panel__content, +.fabric-search .db-panel { + background: var(--tz-ui-surface, rgba(15, 23, 42, 0.78)); + border-color: var(--tz-ui-border, rgba(226, 232, 240, 0.2)); + color: var(--tz-ui-text, #f8fafc); + box-shadow: var(--tz-ui-shadow-panel, 0 18px 48px rgba(0, 0, 0, 0.22)); +} + +.fabric-search .search-bar__input, +.fabric-search .search-bar__input::placeholder { + color: var(--tz-ui-text-muted, #cbd5e1); + opacity: 1; +} + +.fabric-search .category-picker__chip { + color: var(--tz-ui-text-soft, #e2e8f0); + background: var(--tz-ui-control-bg, rgba(30, 41, 59, 0.96)); + border-color: var(--tz-ui-border, rgba(226, 232, 240, 0.2)); +} + +.fabric-search .category-picker__chip--active { + color: var(--tz-ui-accent-contrast, #171101); + background: var(--tz-ui-accent, #e0a82e); + border-color: var(--tz-ui-accent, #e0a82e); + box-shadow: 0 12px 26px rgba(224, 168, 46, 0.22); +} + +.fabric-search .category-picker__chip--active .category-picker__chip-label, +.fabric-search .category-picker__chip--active .category-picker__chip-icon, +.fabric-search .category-picker__chip--active .category-picker__chip-check { + color: var(--tz-ui-accent-contrast, #171101); +} + +.fabric-search.fabric-search--previewing .fabric-search__body { + padding-top: clamp(18px, 3vh, 32px); +} + +.fabric-search.fabric-search--previewing .fabric-search__top-bar { + display: flex; + justify-content: flex-end; + width: min(1060px, 100%); + margin-inline: auto; +} + +.fabric-search.fabric-search--previewing .settings-panel__content { + position: static; + transform: none; +} + +.fabric-search .image-preview__pane-title { + color: var(--tz-ui-text, #f8fafc); + background: var(--tz-ui-surface-raised, rgba(23, 31, 51, 0.96)); + border-color: var(--tz-ui-border, rgba(226, 232, 240, 0.2)); +} + +.fabric-search .image-preview__frame-actions .btn { + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.2); +} + +.fabric-search button:focus-visible, +.fabric-search input:focus-visible, +.fabric-search [role="button"]:focus-visible { + outline: 3px solid var(--tz-ui-focus-ring, rgba(224, 168, 46, 0.34)); + outline-offset: 3px; +} + +:root[data-theme="light"] .fabric-search .search-bar, +:root[data-theme="light"] .fabric-search .category-picker, +:root[data-theme="light"] .fabric-search .image-preview, +:root[data-theme="light"] .fabric-search .results-section, +:root[data-theme="light"] .fabric-search .settings-panel__content, +:root[data-theme="light"] .fabric-search .db-panel { + background: var(--tz-ui-surface-raised, rgba(255, 255, 255, 0.96)); + border-color: var(--tz-ui-border, rgba(15, 23, 42, 0.14)); + color: var(--tz-ui-text, #0f172a); +} + +:root[data-theme="light"] .fabric-search .search-bar__input, +:root[data-theme="light"] .fabric-search .search-bar__input::placeholder { + color: var(--tz-ui-text-muted, #475569); +} + +:root[data-theme="light"] .fabric-search .category-picker__chip { + color: var(--tz-ui-text, #0f172a); + background: var(--tz-ui-control-bg, #ffffff); + border-color: var(--tz-ui-border, rgba(15, 23, 42, 0.14)); +} + +@media (max-height: 760px) { + .fabric-search:not(.fabric-search--has-results):not(.fabric-search--previewing) .fabric-search__body { + padding-top: 28px; + } + + .fabric-search .hero__title { + font-size: clamp(40px, 8vw, 72px); + } +} diff --git a/frontend/src/assets/styles/_makeover.css b/frontend/src/assets/styles/_makeover.css index 011c82f..b4b7ec2 100644 --- a/frontend/src/assets/styles/_makeover.css +++ b/frontend/src/assets/styles/_makeover.css @@ -43,6 +43,22 @@ --tz-dropzone-hover-bg: rgba(27, 34, 48, 0.78); --tz-upload-panel-bg: rgba(23, 31, 51, 0.94); --tz-lightbox-bg: rgba(8, 11, 20, 0.86); + --tz-ui-page: var(--tz-app-bg); + --tz-ui-surface: rgba(15, 23, 42, 0.78); + --tz-ui-surface-raised: rgba(23, 31, 51, 0.96); + --tz-ui-surface-subtle: rgba(30, 41, 59, 0.72); + --tz-ui-control-bg: rgba(30, 41, 59, 0.96); + --tz-ui-control-hover-bg: rgba(51, 65, 85, 0.98); + --tz-ui-text: #f8fafc; + --tz-ui-text-soft: #e2e8f0; + --tz-ui-text-muted: #cbd5e1; + --tz-ui-text-faint: #94a3b8; + --tz-ui-border: rgba(226, 232, 240, 0.22); + --tz-ui-border-strong: rgba(226, 232, 240, 0.38); + --tz-ui-accent: var(--tz-turmeric); + --tz-ui-accent-contrast: #171101; + --tz-ui-focus-ring: rgba(224, 168, 46, 0.34); + --tz-ui-shadow-panel: 0 18px 48px rgba(0, 0, 0, 0.28); --reader-page-bg: linear-gradient(135deg, #0a0a0a 0%, #1a1a1a 50%, #0f0f0f 100%); --reader-title-gradient: linear-gradient(135deg, #fff 0%, #aaa 100%); --reader-muted: #888; @@ -111,6 +127,22 @@ --tz-dropzone-hover-bg: rgba(241, 245, 249, 0.96); --tz-upload-panel-bg: rgba(255, 255, 255, 0.90); --tz-lightbox-bg: rgba(17, 24, 39, 0.54); + --tz-ui-page: var(--tz-app-bg); + --tz-ui-surface: rgba(255, 255, 255, 0.82); + --tz-ui-surface-raised: rgba(255, 255, 255, 0.96); + --tz-ui-surface-subtle: rgba(241, 245, 249, 0.86); + --tz-ui-control-bg: rgba(255, 255, 255, 0.96); + --tz-ui-control-hover-bg: #edf2f8; + --tz-ui-text: #0f172a; + --tz-ui-text-soft: #1f2937; + --tz-ui-text-muted: #475569; + --tz-ui-text-faint: #64748b; + --tz-ui-border: rgba(15, 23, 42, 0.14); + --tz-ui-border-strong: rgba(15, 23, 42, 0.24); + --tz-ui-accent: var(--tz-turmeric); + --tz-ui-accent-contrast: #171101; + --tz-ui-focus-ring: rgba(183, 121, 31, 0.28); + --tz-ui-shadow-panel: 0 18px 44px rgba(31, 41, 55, 0.12); --reader-page-bg: linear-gradient(135deg, #f8fafc 0%, #eef4fb 50%, #ffffff 100%); --reader-title-gradient: linear-gradient(135deg, #111827 0%, #365ea8 100%); --reader-muted: #4b5563; diff --git a/frontend/src/components/search/CategoryPicker.tsx b/frontend/src/components/search/CategoryPicker.tsx new file mode 100644 index 0000000..8943264 --- /dev/null +++ b/frontend/src/components/search/CategoryPicker.tsx @@ -0,0 +1,70 @@ +import { useEffect, useState } from "react"; +import { CATEGORIES } from "./searchConfig"; + +interface CategoryPickerProps { + selected: string[]; + onChange: (cats: string[]) => void; + compact?: boolean; +} + +export default function CategoryPicker({ selected, onChange, compact = false }: CategoryPickerProps) { + const [tempSelected, setTempSelected] = useState(selected); + + const toggle = (id: string) => { + const next = tempSelected.includes(id) + ? tempSelected.filter((c) => c !== id) + : [...tempSelected, id]; + setTempSelected(next); + if (!compact) { + onChange(next); + } + }; + + const allOn = tempSelected.length === CATEGORIES.length; + const toggleAll = () => { + const next = allOn ? [] : CATEGORIES.map((c) => c.id); + setTempSelected(next); + if (!compact) { + onChange(next); + } + }; + + useEffect(() => { + setTempSelected(selected); + }, [selected]); + + return ( +
+
+ Filter by Category + +
+ +
+ {CATEGORIES.map((cat) => { + const active = tempSelected.includes(cat.id); + return ( + + ); + })} + {compact && ( + + )} +
+
+ ); +} diff --git a/frontend/src/components/search/CropDrawer.tsx b/frontend/src/components/search/CropDrawer.tsx new file mode 100644 index 0000000..e392fd3 --- /dev/null +++ b/frontend/src/components/search/CropDrawer.tsx @@ -0,0 +1,74 @@ +import type { RefObject, MouseEvent as ReactMouseEvent } from "react"; +import type { CropRect } from "./types"; + +interface CropDrawerProps { + rawImageUrl: string; + cropRect: CropRect; + imgRef: RefObject; + onImageLoad: () => void; + onDragStart: (e: ReactMouseEvent) => void; + onResizeStart: (e: ReactMouseEvent) => void; + onConfirm: () => void; + onCancel: () => void; +} + +export default function CropDrawer({ + rawImageUrl, cropRect, imgRef, + onImageLoad, onDragStart, onResizeStart, + onConfirm, onCancel, +}: CropDrawerProps) { + return ( +
+
+

Crop & Confirm

+ +
+
+ Select crop area +
{ e.preventDefault(); onDragStart(e); }} + > +
{ e.stopPropagation(); onResizeStart(e); }} + /> +
+
+
+ +
+ + +
+
+
+ ); +} diff --git a/frontend/src/components/search/DbControlPanel.tsx b/frontend/src/components/search/DbControlPanel.tsx new file mode 100644 index 0000000..4cfbb89 --- /dev/null +++ b/frontend/src/components/search/DbControlPanel.tsx @@ -0,0 +1,67 @@ +import { useState } from "react"; +import { USER_FRIENDLY_SERVER_ERROR } from "./searchConfig"; +import { callDbEndpoint } from "./searchUtils"; +import type { DbOp, NotificationState } from "./types"; + +export default function DbControlPanel() { + const [activeOp, setActiveOp] = useState(null); + const [notification, setNotification] = useState(null); + + const handleOp = async (op: "create" | "update") => { + if (activeOp) return; + setActiveOp(op); + setNotification(null); + try { + const msg = await callDbEndpoint(op); + setNotification({ message: msg, type: "success" }); + } catch (e: unknown) { + console.error("Database operation failed:", e); + setNotification({ message: USER_FRIENDLY_SERVER_ERROR, type: "error" }); + } finally { + setActiveOp(null); + } + }; + + return ( +
+
+ + Vector Database +
+ +
+ + + +
+ + {notification && ( +
+ {notification.message} +
+ )} +
+ ); +} diff --git a/frontend/src/components/search/ImagePreview.tsx b/frontend/src/components/search/ImagePreview.tsx new file mode 100644 index 0000000..e83f818 --- /dev/null +++ b/frontend/src/components/search/ImagePreview.tsx @@ -0,0 +1,76 @@ +import CategoryPicker from "./CategoryPicker"; +import LimitSlider from "./LimitSlider"; + +interface ImagePreviewProps { + originalUrl: string; + croppedUrl: string | null; + searchLimit: number; + selectedCategories: string[]; + loading: boolean; + onClear: () => void; + onRecrop: () => void; + onSetCategories: (cats: string[]) => void; + onSetLimit: (v: number) => void; + onSearch: () => void; +} + +export default function ImagePreview({ + originalUrl, croppedUrl, searchLimit, + selectedCategories, loading, + onClear, onRecrop, onSetCategories, onSetLimit, onSearch, +}: ImagePreviewProps) { + return ( +
+
+

Original Image

+
+ original +
+ +
+
+ +
+
+
+ + {croppedUrl && ( + <> +
+

Cropped Image

+
+ cropped +
+
+ +
+ +
+ +
+ + +
+ + )} +
+ ); +} diff --git a/frontend/src/components/search/Lightbox.tsx b/frontend/src/components/search/Lightbox.tsx new file mode 100644 index 0000000..a9c76a8 --- /dev/null +++ b/frontend/src/components/search/Lightbox.tsx @@ -0,0 +1,93 @@ +import type { MouseEventHandler, WheelEventHandler } from "react"; + +interface LightboxProps { + src: string; + caption: string | null; + scale: number; + offset: { x: number; y: number }; + onClose: () => void; + onWheel: WheelEventHandler; + onMouseDown: MouseEventHandler; + onMouseMove: MouseEventHandler; + onMouseUp: MouseEventHandler; + onZoomIn: () => void; + onZoomOut: () => void; + onReset: () => void; +} + +export default function Lightbox({ + src, caption, scale, offset, + onClose, onWheel, onMouseDown, onMouseMove, onMouseUp, + onZoomIn, onZoomOut, onReset, +}: LightboxProps) { + return ( +
{ + if ((e.target as HTMLElement).classList.contains("lightbox")) onClose(); + }} + > +
+ {caption + + {caption &&
{caption}
} + +
e.stopPropagation()} + onClick={(e) => e.stopPropagation()} + > + + + + +
+
+
+ ); +} diff --git a/frontend/src/components/search/LimitSlider.tsx b/frontend/src/components/search/LimitSlider.tsx new file mode 100644 index 0000000..712a2c7 --- /dev/null +++ b/frontend/src/components/search/LimitSlider.tsx @@ -0,0 +1,27 @@ +interface LimitSliderProps { + value: number; + onChange: (v: number) => void; + label?: string; +} + +export default function LimitSlider({ value, onChange, label = "Results" }: LimitSliderProps) { + return ( +
+ {label} +
+
+ onChange(Number(e.target.value))} + /> +
+ {value} +
+ ); +} diff --git a/frontend/src/components/search/Pagination.tsx b/frontend/src/components/search/Pagination.tsx new file mode 100644 index 0000000..2c3cb5f --- /dev/null +++ b/frontend/src/components/search/Pagination.tsx @@ -0,0 +1,21 @@ +interface PaginationProps { + page: number; + totalPages: number; + onPrev: () => void; + onNext: () => void; + variant?: "inline" | "bottom"; +} + +export default function Pagination({ page, totalPages, onPrev, onNext, variant = "bottom" }: PaginationProps) { + return ( +
+ + Page {page} / {totalPages} + +
+ ); +} diff --git a/frontend/src/components/search/ResultCard.tsx b/frontend/src/components/search/ResultCard.tsx new file mode 100644 index 0000000..0735d3c --- /dev/null +++ b/frontend/src/components/search/ResultCard.tsx @@ -0,0 +1,52 @@ +import type { ResultItem } from "./types"; +import { cleanName, toCdnUrl } from "./searchUtils"; + +interface ResultCardProps { + item: ResultItem; + index: number; + onZoom: (src: string, caption: string) => void; + onBadImage: (src: string) => void; +} + +export default function ResultCard({ item, index, onZoom, onBadImage }: ResultCardProps) { + const handleClick = () => { + if (!item.imageSrc) return; + onZoom(toCdnUrl(item.imageSrc), cleanName(item.filename)); + }; + + return ( +
+
+ {item.filename} onBadImage(item.imageSrc)} + onClick={handleClick} + /> + +
+ + + + {item.audioSrc && ( +
+
+ )} +
+ ); +} diff --git a/frontend/src/components/search/ResultsSection.tsx b/frontend/src/components/search/ResultsSection.tsx new file mode 100644 index 0000000..2cf4ac3 --- /dev/null +++ b/frontend/src/components/search/ResultsSection.tsx @@ -0,0 +1,49 @@ +import Pagination from "./Pagination"; +import ResultCard from "./ResultCard"; +import type { ResultItem } from "./types"; + +interface ResultsSectionProps { + results: ResultItem[]; + paginatedResults: ResultItem[]; + page: number; + totalPages: number; + selectedCategories: string[]; + isTextSearch: boolean; + onSetCategories: (cats: string[]) => void; + onPrev: () => void; + onNext: () => void; + onZoom: (src: string, caption: string) => void; + onBadImage: (src: string) => void; +} + +export default function ResultsSection({ + results, + paginatedResults, + page, + totalPages, + onPrev, + onNext, + onZoom, + onBadImage, +}: ResultsSectionProps) { + return ( +
+
+
{results.length} results
+ +
+ +
+ {paginatedResults.map((item, idx: number) => ( + + ))} +
+
+ ); +} diff --git a/frontend/src/components/search/SearchHero.tsx b/frontend/src/components/search/SearchHero.tsx new file mode 100644 index 0000000..310bd4f --- /dev/null +++ b/frontend/src/components/search/SearchHero.tsx @@ -0,0 +1,89 @@ +import type { ChangeEvent } from "react"; +import CategoryPicker from "./CategoryPicker"; +import LimitSlider from "./LimitSlider"; + +interface HeroProps { + textQuery: string; + setTextQuery: (v: string) => void; + onTextSearch: () => void; + onFileChange: (e: ChangeEvent) => void; + fileInputId: string; + searchLimit: number; + onSetLimit: (v: number) => void; + selectedCategories: string[]; + onSetCategories: (cats: string[]) => void; + loading: boolean; +} + +export default function SearchHero({ + textQuery, setTextQuery, onTextSearch, + onFileChange, fileInputId, + searchLimit, onSetLimit, + selectedCategories, onSetCategories, + loading, +}: HeroProps) { + return ( +
+
+
Fabric Intelligence
+ +

+ Find the clothing +
+ + you couldn't find. + +

+ +

+ Visual & semantic search — powered by vectors +

+
+ +
+
+ setTextQuery(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && onTextSearch()} + /> + +
+ + + +
or
+ + + +
+ +
+ +
+
+ ); +} diff --git a/frontend/src/components/search/SettingsPanel.tsx b/frontend/src/components/search/SettingsPanel.tsx new file mode 100644 index 0000000..e2b9a55 --- /dev/null +++ b/frontend/src/components/search/SettingsPanel.tsx @@ -0,0 +1,27 @@ +import { useState } from "react"; +import DbControlPanel from "./DbControlPanel"; + +export default function SettingsPanel() { + const [open, setOpen] = useState(false); + + return ( +
+ + + {open && ( +
+ +
+ )} +
+ ); +} diff --git a/frontend/src/components/search/StickySearchBar.tsx b/frontend/src/components/search/StickySearchBar.tsx new file mode 100644 index 0000000..4a6ad24 --- /dev/null +++ b/frontend/src/components/search/StickySearchBar.tsx @@ -0,0 +1,114 @@ +import type { ChangeEvent } from "react"; +import CategoryPicker from "./CategoryPicker"; +import LimitSlider from "./LimitSlider"; + +interface StickySearchBarProps { + textQuery: string; + setTextQuery: (v: string) => void; + onSearch: () => void; + onClear: () => void; + loading: boolean; + isImageMode: boolean; + previewUrl: string | null; + onRecrop: () => void; + fileInputId: string; + onFileChange: (e: ChangeEvent) => void; + selectedCategories: string[]; + onSetCategories: (cats: string[]) => void; + searchLimit: number; + onSetLimit: (v: number) => void; +} + +export default function StickySearchBar({ + textQuery, + setTextQuery, + onSearch, + onClear, + loading, + isImageMode, + previewUrl, + onRecrop, + fileInputId, + onFileChange, + selectedCategories, + onSetCategories, + searchLimit, + onSetLimit, +}: StickySearchBarProps) { + return ( +
+
+ {isImageMode && previewUrl ? ( +
+
+ query +
+ +
+ + + + + +
+
+ ) : ( +
+
+ setTextQuery(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && onSearch()} + /> +
+ + +
+ )} + + +
+ +
+ + + +
+
+ ); +} diff --git a/frontend/src/components/search/searchConfig.ts b/frontend/src/components/search/searchConfig.ts new file mode 100644 index 0000000..0c794a8 --- /dev/null +++ b/frontend/src/components/search/searchConfig.ts @@ -0,0 +1,15 @@ +import { BASE_URL, FULL_API_URL } from "../../constants"; + +export const MIN_CROP_SIZE = 40; + +export const CATEGORIES = [ + { id: "stock", label: "Stock", icon: "📦" }, + { id: "fabric", label: "Fabric", icon: "🧵" }, + { id: "design", label: "Design", icon: "🎨" }, + { id: "product", label: "Product", icon: "🖼️" }, +]; + +export const API_BASE = FULL_API_URL; +export const ASSET_BASE = BASE_URL; +export const CDN_BASE = (import.meta.env.VITE_AWS_PUBLIC_URL ?? "https://cdn.threadzip.com").replace(/\/$/, ""); +export const USER_FRIENDLY_SERVER_ERROR = "Unable to connect to the server, please try after some time."; diff --git a/frontend/src/components/search/searchUtils.ts b/frontend/src/components/search/searchUtils.ts new file mode 100644 index 0000000..f14b9eb --- /dev/null +++ b/frontend/src/components/search/searchUtils.ts @@ -0,0 +1,70 @@ +import type { CropRect, ResultItem } from "./types"; +import { API_BASE, ASSET_BASE, CDN_BASE, MIN_CROP_SIZE } from "./searchConfig"; + +export function clampCropRectToBounds(rect: CropRect, imgWidth: number, imgHeight: number): CropRect { + if (imgWidth <= 0 || imgHeight <= 0) return rect; + + const maxW = Math.max(MIN_CROP_SIZE, imgWidth); + const maxH = Math.max(MIN_CROP_SIZE, imgHeight); + const w = Math.min(Math.max(MIN_CROP_SIZE, rect.w), maxW); + const h = Math.min(Math.max(MIN_CROP_SIZE, rect.h), maxH); + const x = Math.min(Math.max(0, rect.x), Math.max(0, imgWidth - w)); + const y = Math.min(Math.max(0, rect.y), Math.max(0, imgHeight - h)); + + return { + x: Math.round(x), + y: Math.round(y), + w: Math.round(w), + h: Math.round(h), + }; +} + +export function toCdnUrl(src: string | undefined): string { + if (!src) return ""; + + const clean = String(src).trim(); + + if (/^(https?:|blob:|data:)/i.test(clean)) { + return clean; + } + + if (clean.startsWith("/api/") || clean.startsWith("/assets/")) { + return `${ASSET_BASE}${clean}`; + } + + const normalized = clean.replace(/^\/+/, ""); + + if (normalized.startsWith("images/")) { + return `${CDN_BASE}/${normalized}`; + } + + return `${CDN_BASE}/images/${normalized}`; +} + +export function toResultItem(raw: string): ResultItem { + const filename = raw.split("/").pop() ?? raw; + return { imageSrc: raw, filename }; +} + +export function cleanName(filename: string): string { + if (!filename) return ""; + + const withoutExtension = filename.replace(/\.[^.]+$/, ""); + const readable = withoutExtension + .replace(/[_-]+/g, " ") + .replace(/\s+/g, " ") + .trim(); + + return readable || withoutExtension || filename; +} + +export async function callDbEndpoint(op: "create" | "update"): Promise { + const url = + op === "create" + ? `${API_BASE}/database/create/table` + : `${API_BASE}/database/update/table`; + const res = await fetch(url, { method: "PUT" }); + const data = await res.json().catch(() => ({})); + if (!res.ok) throw new Error(data?.detail ?? `Request failed (${res.status})`); + return data?.message ?? "Done."; +} diff --git a/frontend/src/components/search/types.ts b/frontend/src/components/search/types.ts new file mode 100644 index 0000000..91a7727 --- /dev/null +++ b/frontend/src/components/search/types.ts @@ -0,0 +1,23 @@ +export type NotificationState = { message: string; type: "success" | "error" } | null; +export type DbOp = "create" | "update" | null; + +export interface ResultItem { + imageSrc: string; + filename: string; + audioSrc?: string; +} + +export interface SearchApiResponse { + message: string; + results: string[]; + pagination: { + page: number; + per_page: number; + total_results: number; + total_pages: number; + has_next: boolean; + has_prev: boolean; + }; +} + +export type CropRect = { x: number; y: number; w: number; h: number }; diff --git a/frontend/src/components/search/useSearch.ts b/frontend/src/components/search/useSearch.ts new file mode 100644 index 0000000..c531a6b --- /dev/null +++ b/frontend/src/components/search/useSearch.ts @@ -0,0 +1,69 @@ +import { useCallback, useState } from "react"; +import { API_BASE, USER_FRIENDLY_SERVER_ERROR } from "./searchConfig"; +import { toResultItem } from "./searchUtils"; +import type { ResultItem, SearchApiResponse } from "./types"; + +export function useSearch() { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [results, setResults] = useState([]); + + const runImageSearch = useCallback(async (file: File, category?: string[], limit = 20, preserveResultsOnError = false) => { + setLoading(true); + setError(null); + try { + const form = new FormData(); + form.append("file", file); + form.append("limit", String(limit)); + form.append("page", "1"); + form.append("per_page", String(limit)); + if (category?.length) category.forEach((c) => { form.append("category", c); }); + const res = await fetch(`${API_BASE}/search`, { method: "POST", body: form }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err?.detail ?? `Search failed (${res.status})`); + } + const data: SearchApiResponse = await res.json(); + setResults((data.results ?? []).map(toResultItem)); + } catch (e) { + console.error("Image search failed:", e); + setError(USER_FRIENDLY_SERVER_ERROR); + if (!preserveResultsOnError) setResults([]); + } finally { + setLoading(false); + } + }, []); + + const runTextSearch = useCallback(async (term: string, category?: string[], limit = 20, preserveResultsOnError = false) => { + setLoading(true); + setError(null); + try { + const form = new FormData(); + form.append("search_term", term); + form.append("limit", String(limit)); + form.append("page", "1"); + form.append("per_page", String(limit)); + if (category?.length) category.forEach((c) => { form.append("category", c); }); + const res = await fetch(`${API_BASE}/search`, { method: "POST", body: form }); + if (!res.ok) { + const err = await res.json().catch(() => ({})); + throw new Error(err?.detail ?? `Search failed (${res.status})`); + } + const data: SearchApiResponse = await res.json(); + setResults((data.results ?? []).map(toResultItem)); + } catch (e) { + console.error("Text search failed:", e); + setError(USER_FRIENDLY_SERVER_ERROR); + if (!preserveResultsOnError) setResults([]); + } finally { + setLoading(false); + } + }, []); + + const clear = useCallback(() => { + setResults([]); + setError(null); + }, []); + + return { loading, error, results, runImageSearch, runTextSearch, clear }; +} diff --git a/frontend/src/pages/FabricList.tsx b/frontend/src/pages/FabricList.tsx index 184723d..a1924a1 100644 --- a/frontend/src/pages/FabricList.tsx +++ b/frontend/src/pages/FabricList.tsx @@ -45,6 +45,7 @@ export default function ContentGrid() { const draggingRef = useRef(false); const lastPosRef = useRef({ x: 0, y: 0 }); + const previousBodyOverflowRef = useRef(null); const MIN_SCALE = 0.5; const MAX_SCALE = 6; @@ -100,8 +101,14 @@ export default function ContentGrid() { return last.replace(/\.[^.]+$/, ""); } - const cleanName = (filename: string) => - filename?.split("_")[0].split(".")[0] ?? ""; + const cleanName = (filename: string) => { + if (!filename) return ""; + return filename + .replace(/\.[^.]+$/, "") + .replace(/[_-]+/g, " ") + .replace(/\s+/g, " ") + .trim(); + }; // ✅ Hide items with missing/broken images const visibleItems = useMemo(() => { @@ -121,6 +128,7 @@ export default function ContentGrid() { setScale(1); setOffset({ x: 0, y: 0 }); setLightboxOpen(true); + previousBodyOverflowRef.current = document.body.style.overflow; document.body.style.overflow = "hidden"; }; @@ -128,7 +136,8 @@ export default function ContentGrid() { setLightboxOpen(false); setActiveSrc(null); setActiveCaption(null); - document.body.style.overflow = ""; + document.body.style.overflow = previousBodyOverflowRef.current ?? ""; + previousBodyOverflowRef.current = null; }; // biome-ignore lint/correctness/useExhaustiveDependencies: re-render loop. @@ -215,7 +224,7 @@ export default function ContentGrid() { - + Page { page } / { totalPages } - - - + + +
diff --git a/frontend/src/pages/FabricSearch.tsx b/frontend/src/pages/FabricSearch.tsx index be1cf4d..d945625 100644 --- a/frontend/src/pages/FabricSearch.tsx +++ b/frontend/src/pages/FabricSearch.tsx @@ -1,957 +1,22 @@ -import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; -import { BASE_URL, FULL_API_URL } from "../constants"; +import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"; +import type { ChangeEvent, MouseEventHandler, WheelEventHandler } from "react"; import Loader from "../components/Loader"; import Notification from "../components/Notification"; +import CropDrawer from "../components/search/CropDrawer"; +import ImagePreview from "../components/search/ImagePreview"; +import Lightbox from "../components/search/Lightbox"; +import ResultsSection from "../components/search/ResultsSection"; +import SettingsPanel from "../components/search/SettingsPanel"; +import SearchHero from "../components/search/SearchHero"; +import StickySearchBar from "../components/search/StickySearchBar"; +import { MIN_CROP_SIZE } from "../components/search/searchConfig"; +import { clampCropRectToBounds } from "../components/search/searchUtils"; +import type { CropRect, NotificationState } from "../components/search/types"; +import { useSearch } from "../components/search/useSearch"; import { throttle } from "../utils/throttle"; import "@/assets/styles/FabricSearch.css"; -// ─── Types ─────────────────────────────────────────────────────────────────── - -type NotificationState = { message: string; type: "success" | "error" } | null; -type DbOp = "create" | "update" | null; - -interface ResultItem { - imageSrc: string; - filename: string; - audioSrc?: string; -} - -interface SearchApiResponse { - message: string; - results: string[]; - pagination: { - page: number; - per_page: number; - total_results: number; - total_pages: number; - has_next: boolean; - has_prev: boolean; - }; -} - -type CropRect = { x: number; y: number; w: number; h: number }; - -const MIN_CROP_SIZE = 40; - -function clampCropRectToBounds(rect: CropRect, imgWidth: number, imgHeight: number): CropRect { - if (imgWidth <= 0 || imgHeight <= 0) return rect; - - const maxW = Math.max(MIN_CROP_SIZE, imgWidth); - const maxH = Math.max(MIN_CROP_SIZE, imgHeight); - const w = Math.min(Math.max(MIN_CROP_SIZE, rect.w), maxW); - const h = Math.min(Math.max(MIN_CROP_SIZE, rect.h), maxH); - const x = Math.min(Math.max(0, rect.x), Math.max(0, imgWidth - w)); - const y = Math.min(Math.max(0, rect.y), Math.max(0, imgHeight - h)); - - return { - x: Math.round(x), - y: Math.round(y), - w: Math.round(w), - h: Math.round(h), - }; -} - -// ─── Constants ──────────────────────────────────────────────────────────────── - -const CATEGORIES = [ - { id: "stock", label: "Stock", icon: "📦" }, - { id: "fabric", label: "Fabric", icon: "🧵" }, - { id: "design", label: "Design", icon: "🎨" }, - { id: "product", label: "Product", icon: "🖼️" }, -]; - -const API_BASE = FULL_API_URL; -const ASSET_BASE = BASE_URL; -const CDN_BASE = (import.meta.env.VITE_AWS_PUBLIC_URL ?? "https://cdn.threadzip.com").replace(/\/$/, ""); -const USER_FRIENDLY_SERVER_ERROR = "Unable to connect to the server, please try after some time."; - -// ─── Helpers ────────────────────────────────────────────────────────────────── - -function toCdnUrl(src: string | undefined): string { - if (!src) return ""; - - const clean = String(src).trim(); - - if (/^(https?:|blob:|data:)/i.test(clean)) { - return clean; - } - - if (clean.startsWith("/api/") || clean.startsWith("/assets/")) { - return `${ASSET_BASE}${clean}`; - } - - if (clean.startsWith("images/")) { - return `${CDN_BASE}/${clean}`; - } - - return `${CDN_BASE}/images/${clean.replace(/^\/+/, "")}`; -} - -function toResultItem(raw: string): ResultItem { - const filename = raw.split("/").pop() ?? raw; - return { imageSrc: raw, filename }; -} - -function cleanName(filename: string): string { - return filename ? filename.split("_")[0].split(".")[0] : ""; -} - -// ─── useSearch hook ─────────────────────────────────────────────────────────── - -function useSearch() { - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [results, setResults] = useState([]); - - const runImageSearch = useCallback(async (file: File, category?: string[], limit = 20, preserveResultsOnError = false) => { - setLoading(true); - setError(null); - try { - const form = new FormData(); - form.append("file", file); - form.append("limit", String(limit)); - form.append("page", "1"); - form.append("per_page", String(limit)); - if (category?.length) category.forEach((c) => { form.append("category", c) }); - const res = await fetch(`${API_BASE}/search`, { method: "POST", body: form }); - if (!res.ok) { - const err = await res.json().catch(() => ({})); - throw new Error(err?.detail ?? `Search failed (${res.status})`); - } - const data: SearchApiResponse = await res.json(); - setResults((data.results ?? []).map(toResultItem)); - } catch (e) { - console.error("Image search failed:", e); - setError(USER_FRIENDLY_SERVER_ERROR); - if (!preserveResultsOnError) setResults([]); - } finally { - setLoading(false); - } - }, []); - - const runTextSearch = useCallback(async (term: string, category?: string[], limit = 20, preserveResultsOnError = false) => { - setLoading(true); - setError(null); - try { - const form = new FormData(); - form.append("search_term", term); - form.append("limit", String(limit)); - form.append("page", "1"); - form.append("per_page", String(limit)); - if (category?.length) category.forEach((c) => { form.append("category", c) }); - const res = await fetch(`${API_BASE}/search`, { method: "POST", body: form }); - if (!res.ok) { - const err = await res.json().catch(() => ({})); - throw new Error(err?.detail ?? `Search failed (${res.status})`); - } - const data: SearchApiResponse = await res.json(); - setResults((data.results ?? []).map(toResultItem)); - } catch (e) { - console.error("Text search failed:", e); - setError(USER_FRIENDLY_SERVER_ERROR); - if (!preserveResultsOnError) setResults([]); - } finally { - setLoading(false); - } - }, []); - - const clear = useCallback(() => { - setResults([]); - setError(null); - }, []); - - return { loading, error, results, runImageSearch, runTextSearch, clear }; -} - -// ─── DB endpoint ────────────────────────────────────────────────────────────── - -async function callDbEndpoint(op: "create" | "update"): Promise { - const url = - op === "create" - ? `${API_BASE}/database/create/table` - : `${API_BASE}/database/update/table`; - const res = await fetch(url, { method: "PUT" }); - const data = await res.json().catch(() => ({})); - if (!res.ok) throw new Error(data?.detail ?? `Request failed (${res.status})`); - return data?.message ?? "Done."; -} - -// ─── CategoryPicker ─────────────────────────────────────────────────────────── - -interface CategoryPickerProps { - selected: string[]; - onChange: (cats: string[]) => void; - compact?: boolean; -} - -function CategoryPicker({ selected, onChange, compact = false }: CategoryPickerProps) { - const [tempSelected, setTempSelected] = useState(selected); - const toggle = (id: string) => { - const next = tempSelected.includes(id) - ? tempSelected.filter((c) => c !== id) - : [...tempSelected, id]; - setTempSelected(next); - // Non-compact (Hero / ImagePreview): propagate immediately so the parent's - // selectedCategories stays in sync. Compact mode defers to the Apply button. - if (!compact) { - onChange(next); - } - }; - const allOn = tempSelected.length === CATEGORIES.length; - const toggleAll = () => { - const next = allOn ? [] : CATEGORIES.map((c) => c.id); - setTempSelected(next); - if (!compact) { - onChange(next); - } - }; - - const applySearch = () => { - onChange(tempSelected); - }; - - useEffect(() => { - setTempSelected(selected); - }, [selected]); - - return ( -
-
- Filter by Category - -
- -
- {CATEGORIES.map((cat) => { - const active = tempSelected.includes(cat.id); - return ( - - ); - })} - {compact && ( - - )} -
-
- ); -} - -// ─── DbControlPanel ─────────────────────────────────────────────────────────── - -function DbControlPanel() { - const [activeOp, setActiveOp] = useState(null); - const [notification, setNotification] = useState(null); - - const handleOp = async (op: "create" | "update") => { - if (activeOp) return; - setActiveOp(op); - setNotification(null); - try { - const msg = await callDbEndpoint(op); - setNotification({ message: msg, type: "success" }); - } catch (e: unknown) { - console.error("Database operation failed:", e); - setNotification({ message: USER_FRIENDLY_SERVER_ERROR, type: "error" }); - } finally { - setActiveOp(null); - } - }; - - return ( -
-
- - Vector Database -
- -
- - - -
- - {notification && ( -
- {notification.message} -
- )} -
- ); -} - -// ─── SettingsPanel ──────────────────────────────────────────────────────────── - -function SettingsPanel() { - const [open, setOpen] = useState(false); - - return ( -
- - - {open && ( -
- -
- )} -
- ); -} - -// ─── StickySearchBar ────────────────────────────────────────────────────────── - -interface StickySearchBarProps { - textQuery: string; - setTextQuery: (v: string) => void; - onSearch: () => void; - onClear: () => void; - loading: boolean; - isImageMode: boolean; - previewUrl: string | null; - onRecrop: () => void; - fileInputId: string; - onFileChange: (e: React.ChangeEvent) => void; - selectedCategories: string[]; - onSetCategories: (cats: string[]) => void; - searchLimit: number; - onSetLimit: (v: number) => void; -} - -function StickySearchBar({ - textQuery, - setTextQuery, - onSearch, - onClear, - loading, - isImageMode, - previewUrl, - onRecrop, - fileInputId, - onFileChange, - - selectedCategories, - onSetCategories, - searchLimit, - onSetLimit, -}: StickySearchBarProps) { - return ( -
-
- - {isImageMode && previewUrl ? ( -
-
- query -
- -
- - - - - -
-
- ) : ( -
-
- setTextQuery(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && onSearch()} - /> -
- - -
- )} - - -
- - {/* ✅ NEW: filters always visible */} -
- - - -
-
- ); -} -// ─── Pagination ─────────────────────────────────────────────────────────────── - -interface PaginationProps { - page: number; - totalPages: number; - onPrev: () => void; - onNext: () => void; - variant?: "inline" | "bottom"; -} - -function Pagination({ page, totalPages, onPrev, onNext, variant = "bottom" }: PaginationProps) { - return ( -
- - Page {page} / {totalPages} - -
- ); -} - -// ─── ResultCard ─────────────────────────────────────────────────────────────── - -interface ResultCardProps { - item: ResultItem; - index: number; - onZoom: (src: string, caption: string) => void; - onBadImage: (src: string) => void; -} - -function ResultCard({ item, index, onZoom, onBadImage }: ResultCardProps) { - const handleClick = () => { - if (!item.imageSrc) return; - onZoom(toCdnUrl(item.imageSrc), cleanName(item.filename)); - }; - - return ( -
-
- {item.filename} onBadImage(item.imageSrc)} - onClick={handleClick} - /> - -
- -
- {cleanName(item.filename)} -
- - {item.audioSrc && ( -
-
- )} -
- ); -} - -// ─── ResultsSection ─────────────────────────────────────────────────────────── - -interface ResultsSectionProps { - results: ResultItem[]; - paginatedResults: ResultItem[]; - page: number; - totalPages: number; - selectedCategories: string[]; - isTextSearch: boolean; - onSetCategories: (cats: string[]) => void; - onPrev: () => void; - onNext: () => void; - onZoom: (src: string, caption: string) => void; - onBadImage: (src: string) => void; -} - -function ResultsSection({ - results, - paginatedResults, - page, - totalPages, - onPrev, - onNext, - onZoom, - onBadImage, -}: ResultsSectionProps) { - return ( -
- -
-
{results.length} results
- -
- - {/* ✅ ALWAYS visible */} - {/*
- -
*/} - -
- {paginatedResults.map((item, idx: number) => ( - - ))} -
-
- ); -} - -// ─── LimitSlider ────────────────────────────────────────────────────────────── - -interface LimitSliderProps { - value: number; - onChange: (v: number) => void; - label?: string; -} - -function LimitSlider({ value, onChange, label = "Results" }: LimitSliderProps) { - return ( -
- {label} -
-
- onChange(Number(e.target.value))} - /> -
- {value} -
- ); -} - -// ─── Lightbox ───────────────────────────────────────────────────────────────── - -interface LightboxProps { - src: string; - caption: string | null; - scale: number; - offset: { x: number; y: number }; - onClose: () => void; - onWheel: React.WheelEventHandler; - onMouseDown: React.MouseEventHandler; - onMouseMove: React.MouseEventHandler; - onMouseUp: React.MouseEventHandler; - onZoomIn: () => void; - onZoomOut: () => void; - onReset: () => void; -} - -function Lightbox({ - src, caption, scale, offset, - onClose, onWheel, onMouseDown, onMouseMove, onMouseUp, - onZoomIn, onZoomOut, onReset, -}: LightboxProps) { - return ( -
{ - if ((e.target as HTMLElement).classList.contains("lightbox")) onClose(); - }} - > -
- {caption - - {caption &&
{caption}
} - -
e.stopPropagation()} - onClick={(e) => e.stopPropagation()} - > - - - - -
-
-
- ); -} - -// ─── CropDrawer ─────────────────────────────────────────────────────────────── - -interface CropDrawerProps { - rawImageUrl: string; - cropRect: { x: number; y: number; w: number; h: number }; - imgRef: React.RefObject; - onImageLoad: () => void; - onDragStart: (e: React.MouseEvent) => void; - onResizeStart: (e: React.MouseEvent) => void; - onConfirm: () => void; - onCancel: () => void; -} - -function CropDrawer({ - rawImageUrl, cropRect, imgRef, - onImageLoad, onDragStart, onResizeStart, - onConfirm, onCancel, -}: CropDrawerProps) { - return ( -
-
-

Crop & Confirm

- -
-
- Select crop area -
{ e.preventDefault(); onDragStart(e); }} - > -
{ e.stopPropagation(); onResizeStart(e); }} - /> -
-
-
- -
- - -
-
-
- ); -} - -// ─── ImagePreview (pre-results, post-crop) ──────────────────────────────────── - -interface ImagePreviewProps { - originalUrl: string; - croppedUrl: string | null; - searchLimit: number; - selectedCategories: string[]; - loading: boolean; - onClear: () => void; - onRecrop: () => void; - onSetCategories: (cats: string[]) => void; - onSetLimit: (v: number) => void; - onSearch: () => void; -} - -function ImagePreview({ - originalUrl, croppedUrl, searchLimit, - selectedCategories, loading, - onClear, onRecrop, onSetCategories, onSetLimit, onSearch, -}: ImagePreviewProps) { - return ( -
- {/* Original */} -
-

Original Image

-
- original -
- -
-
- -
-
-
- - {/* Cropped */} - {croppedUrl && ( - <> -
-

Cropped Image

-
- cropped -
-
- -
- -
- -
- - -
- - )} -
- ); -} - -// ─── Hero ───────────────────────────────────────────────────────────────────── - -interface HeroProps { - textQuery: string; - setTextQuery: (v: string) => void; - onTextSearch: () => void; - onFileChange: (e: React.ChangeEvent) => void; - fileInputId: string; - searchLimit: number; - onSetLimit: (v: number) => void; - selectedCategories: string[]; - onSetCategories: (cats: string[]) => void; - loading: boolean; -} - -function Hero({ - textQuery, setTextQuery, onTextSearch, - onFileChange, fileInputId, - searchLimit, onSetLimit, - selectedCategories, onSetCategories, - loading, -}: HeroProps) { - return ( -
-
-
Fabric Intelligence
- -

- Find the clothing -
- - you couldn't find. - -

- -

- Visual & semantic search — powered by vectors -

-
- -
- {/* Text search */} -
- setTextQuery(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && onTextSearch()} - /> - -
- - {/* Limit */} - - - {/* Divider */} -
or
- - {/* Image upload */} - - -
- - {/* Category filter */} -
- -
-
- ); -} - // ─── Main Page ──────────────────────────────────────────────────────────────── export default function Search() { @@ -1020,7 +85,7 @@ export default function Search() { // ── File input ───────────────────────────────────────────────────────────── - const onFileChange = (e: React.ChangeEvent) => { + const onFileChange = (e: ChangeEvent) => { const f = e.target.files?.[0] ?? null; e.target.value = ""; if (!f) return; @@ -1223,15 +288,15 @@ export default function Search() { }; }, [lightboxOpen, drawerOpen, rawImageUrl]); - const onLbWheel: React.WheelEventHandler = (e) => { + const onLbWheel: WheelEventHandler = (e) => { e.preventDefault(); setLbScale((s) => Math.min(MAX_SCALE, Math.max(MIN_SCALE, s + (e.deltaY > 0 ? -ZOOM_STEP : ZOOM_STEP)))); }; - const onLbMouseDown: React.MouseEventHandler = (e) => { + const onLbMouseDown: MouseEventHandler = (e) => { draggingLbRef.current = true; lastLbPosRef.current = { x: e.clientX, y: e.clientY }; }; - const onLbMouseMove: React.MouseEventHandler = (e) => { + const onLbMouseMove: MouseEventHandler = (e) => { if (!draggingLbRef.current) return; const dx = e.clientX - lastLbPosRef.current.x; const dy = e.clientY - lastLbPosRef.current.y; @@ -1414,7 +479,7 @@ export default function Search() { {/* Hero */} {showHero && ( - Date: Thu, 9 Jul 2026 19:29:50 +0530 Subject: [PATCH 03/25] Fix final UI theme and alignment issues --- frontend/src/assets/styles/Composer.css | 410 ++++++++ frontend/src/assets/styles/ContentGrid.css | 318 ++++++ frontend/src/assets/styles/DescriptionBox.css | 103 ++ frontend/src/assets/styles/DrawerToggle.css | 31 + frontend/src/assets/styles/EmptyState.css | 122 +++ frontend/src/assets/styles/FabricChat.css | 74 ++ frontend/src/assets/styles/FabricSearch.css | 924 ++++++++++++++++++ .../src/assets/styles/ImageDescription.css | 341 +++++++ .../assets/styles/ImageDescriptorHeader.css | 16 + .../src/assets/styles/ImagePreviewPanel.css | 98 ++ frontend/src/assets/styles/Messages.css | 43 + .../assets/styles/SampleImageGalleryCard.css | 29 + .../src/assets/styles/SuggestionChips.css | 54 + frontend/src/assets/styles/_makeover.css | 50 + frontend/src/components/Composer.tsx | 44 +- frontend/src/components/DrawerToggle.tsx | 2 +- .../src/components/search/CategoryPicker.tsx | 12 +- frontend/src/components/search/Lightbox.tsx | 9 + frontend/src/components/search/searchUtils.ts | 29 +- frontend/src/pages/FabricList.tsx | 72 +- frontend/src/pages/FabricSearch.tsx | 23 +- 21 files changed, 2757 insertions(+), 47 deletions(-) diff --git a/frontend/src/assets/styles/Composer.css b/frontend/src/assets/styles/Composer.css index 0428e01..3123738 100644 --- a/frontend/src/assets/styles/Composer.css +++ b/frontend/src/assets/styles/Composer.css @@ -1088,3 +1088,413 @@ background: rgba(30, 41, 59, 0.96); border-color: rgba(226, 232, 240, 0.24); } + +/* QA follow-up: make upload-confirm modal titles readable after theme switching. */ +.composer-modal--confirm-upload { + width: min(520px, calc(100vw - 32px)); + padding: 18px; + border-radius: 16px; + background: var(--tz-ui-surface-raised, #ffffff); + color: var(--tz-ui-text, #0f172a); +} + +.composer-modal__layout { + display: flex; + gap: 14px; + align-items: center; +} + +.composer-modal__layout--audio { + align-items: flex-start; +} + +.composer-modal__media { + flex: 0 0 128px; + min-width: 0; +} + +.composer-modal__content { + flex: 1 1 auto; + min-width: 0; +} + +.composer-modal__title { + margin-bottom: 7px; + color: var(--tz-ui-text, #0f172a); + font-size: 16px; + font-weight: 900; + line-height: 1.25; +} + +.composer-modal__meta, +.composer-modal__hint { + color: var(--tz-ui-text-muted, #475569); + font-size: 14px; + line-height: 1.35; + overflow-wrap: anywhere; +} + +.composer-modal__hint { + margin-top: 8px; + font-size: 13px; +} + +.composer-modal__actions { + margin-top: 14px; + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.composer-modal__button { + min-height: 36px; + padding: 8px 13px; + border-radius: 9px; + font-weight: 800; + cursor: pointer; +} + +.composer-modal__button--confirm { + background: var(--tz-turmeric, #e0a82e); + border: 1px solid var(--tz-turmeric, #e0a82e); + color: #171101; +} + +.composer-modal__button--cancel { + background: var(--tz-ui-control-bg, #ffffff); + border: 1px solid var(--tz-ui-border, rgba(15, 23, 42, 0.14)); + color: var(--tz-ui-text, #0f172a); +} + +.composer-modal__thumb { + width: 128px; + height: 128px; + border-radius: 10px; + object-fit: contain; +} + +.composer-modal__media audio { + width: 128px; + max-width: 100%; +} + +:root[data-theme="dark"] .composer-modal--confirm-upload { + background: rgba(15, 23, 42, 0.98); + color: #f8fafc; + border-color: rgba(226, 232, 240, 0.24); +} + +:root[data-theme="dark"] .composer-modal__title { + color: #f8fafc; +} + +:root[data-theme="dark"] .composer-modal__meta, +:root[data-theme="dark"] .composer-modal__hint { + color: #cbd5e1; +} + +:root[data-theme="dark"] .composer-modal__button--cancel { + background: rgba(30, 41, 59, 0.96); + border-color: rgba(226, 232, 240, 0.24); + color: #f8fafc; +} + +:root[data-theme="light"] .stop-overlay-btn { + background: #dc2626; + border: 1px solid #b91c1c; + color: #ffffff; + box-shadow: 0 10px 24px rgba(220, 38, 38, 0.22); +} + +:root[data-theme="light"] .stop-overlay-inner { + background: rgba(255, 255, 255, 0.86); + border: 1px solid rgba(220, 38, 38, 0.22); +} + +@media (max-width: 560px) { + .composer-modal__layout { + flex-direction: column; + align-items: stretch; + } + + .composer-modal__media { + flex-basis: auto; + } +} + +/* Senior QA final: composer, stop control, and upload-confirm modals stay readable in both themes. */ +.page-root .composer { + padding: 10px 10px max(16px, env(safe-area-inset-bottom)); + border-radius: 18px; +} + +.page-root .composer-row { + align-items: center; +} + +.page-root .composer-input-wrapper { + border-radius: 16px; + background: var(--tz-ui-control-bg, rgba(255, 255, 255, 0.96)); + border: 1px solid var(--tz-ui-border, rgba(15, 23, 42, 0.14)); +} + +.page-root .composer-input { + color: var(--tz-ui-text, #0f172a); + background: transparent; + border: 0; + min-height: 50px; +} + +.page-root .composer-input::placeholder { + color: var(--tz-ui-text-faint, #64748b); + opacity: 1; +} + +:root[data-theme="dark"] .page-root .composer-input-wrapper { + background: rgba(15, 23, 42, 0.88); + border-color: rgba(226, 232, 240, 0.20); +} + +:root[data-theme="dark"] .page-root .composer-input { + color: #f8fafc; +} + +.page-root .attach-btn.inside, +.page-root .send-btn.inside { + top: 50%; + transform: translateY(-50%); +} + +.page-root .attach-btn.inside { + color: var(--tz-ui-text-muted, #475569); + background: var(--tz-ui-surface-subtle, #f1f5f9); + border: 1px solid var(--tz-ui-border, rgba(15, 23, 42, 0.14)); +} + +:root[data-theme="dark"] .page-root .attach-btn.inside { + color: #e2e8f0; + background: rgba(30, 41, 59, 0.92); + border-color: rgba(226, 232, 240, 0.20); +} + +.page-root .composer-audio-row { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 10px; + padding-top: 8px; +} + +.page-root .record-btn { + min-height: 38px; + border-radius: 999px; + padding: 0 14px; + font-weight: 800; + color: var(--tz-ui-text, #0f172a); + background: var(--tz-ui-control-bg, #ffffff); + border: 1px solid var(--tz-ui-border, rgba(15, 23, 42, 0.16)); +} + +:root[data-theme="dark"] .page-root .record-btn { + color: #f8fafc; + background: rgba(30, 41, 59, 0.92); + border-color: rgba(226, 232, 240, 0.20); +} + +.page-root .stop-overlay-btn { + min-height: var(--tz-control-height, 44px); + padding: 0 18px; + border-radius: 999px; + border: 1px solid rgba(185, 28, 28, 0.42); + background: linear-gradient(135deg, var(--tz-danger, #dc2626), var(--tz-danger-hover, #b91c1c)); + color: var(--tz-danger-text, #ffffff); + font-weight: 900; + box-shadow: 0 12px 28px rgba(220, 38, 38, 0.26); +} + +:root[data-theme="light"] .page-root .stop-overlay-btn { + background: linear-gradient(135deg, #ef4444, #b91c1c); + color: #ffffff; + border-color: rgba(127, 29, 29, 0.46); +} + +.composer-modal-overlay .composer-modal--confirm-upload { + background: var(--tz-ui-surface-raised, #ffffff); + color: var(--tz-ui-text, #0f172a); + border: 1px solid var(--tz-ui-border-strong, rgba(15, 23, 42, 0.24)); + box-shadow: var(--tz-ui-shadow-panel, 0 18px 48px rgba(15, 23, 42, 0.22)); +} + +.composer-modal-overlay .composer-modal__title { + color: var(--tz-ui-text, #0f172a); + font-weight: 900; +} + +.composer-modal-overlay .composer-modal__meta, +.composer-modal-overlay .composer-modal__hint { + color: var(--tz-ui-text-muted, #475569); + opacity: 1; +} + +:root[data-theme="dark"] .composer-modal-overlay .composer-modal--confirm-upload { + background: rgba(15, 23, 42, 0.96); + color: #f8fafc; + border-color: rgba(226, 232, 240, 0.24); +} + +:root[data-theme="dark"] .composer-modal-overlay .composer-modal__title { + color: #f8fafc; +} + +:root[data-theme="dark"] .composer-modal-overlay .composer-modal__meta, +:root[data-theme="dark"] .composer-modal-overlay .composer-modal__hint { + color: #cbd5e1; +} + +@media (max-height: 760px) { + .page-root .composer { + padding-top: 8px; + padding-bottom: max(12px, env(safe-area-inset-bottom)); + } + + .page-root .composer-input { + min-height: 46px; + } +} + +/* Senior QA top-up: upload confirmation audio preview must never overlap title/meta. */ +.composer-modal--audio-confirm { + width: min(560px, calc(100vw - 32px)); +} + +.composer-modal--audio-confirm .composer-modal__layout--audio { + display: block; +} + +.composer-modal--audio-confirm .composer-modal__content--audio { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 10px; +} + +.composer-modal--audio-confirm .composer-modal__title { + margin: 0; + width: 100%; + color: var(--tz-ui-text, #0f172a); + font-size: 17px; + line-height: 1.25; +} + +.composer-modal--audio-confirm .composer-modal__meta { + width: min(420px, 100%); + color: var(--tz-ui-text-muted, #475569); + overflow-wrap: anywhere; +} + +.composer-modal__audio-player { + width: 100%; + display: flex; + justify-content: center; + margin: 2px 0 4px; +} + +.composer-modal__audio-player audio { + width: min(360px, 100%); + min-width: 0; + max-width: 100%; + display: block; +} + +.composer-modal--audio-confirm .composer-modal__actions { + justify-content: center; +} + +:root[data-theme="dark"] .composer-modal--audio-confirm .composer-modal__title { + color: #f8fafc; +} + +:root[data-theme="dark"] .composer-modal--audio-confirm .composer-modal__meta, +:root[data-theme="dark"] .composer-modal--audio-confirm .composer-modal__hint { + color: #cbd5e1; +} + +@media (max-width: 520px) { + .composer-modal--confirm-upload { + padding: 16px; + } + + .composer-modal__layout--image { + display: grid; + grid-template-columns: 1fr; + text-align: center; + } + + .composer-modal__layout--image .composer-modal__media { + justify-self: center; + } + + .composer-modal__layout--image .composer-modal__actions { + justify-content: center; + } +} + + +/* Strict senior QA follow-up: upload confirm modals should read cleanly, not like a noisy blurred glitch. */ +.composer-modal-overlay { + background: rgba(8, 11, 20, 0.54); + backdrop-filter: blur(3px); + -webkit-backdrop-filter: blur(3px); +} + +:root[data-theme="light"] .composer-modal-overlay { + background: rgba(15, 23, 42, 0.34); +} + +.composer-modal--confirm-upload { + border-radius: 20px; + padding: 20px; +} + +.composer-modal--audio-confirm .composer-modal__content--audio { + gap: 12px; +} + +.composer-modal--audio-confirm .composer-modal__audio-player { + order: 2; + margin: 4px 0; +} + +.composer-modal--audio-confirm .composer-modal__title { + order: 1; + text-align: center; +} + +.composer-modal--audio-confirm .composer-modal__meta { + order: 3; + max-width: 440px; + text-align: center; +} + +.composer-modal--audio-confirm .composer-modal__hint { + order: 4; + text-align: center; +} + +.composer-modal--audio-confirm .composer-modal__actions { + order: 5; + margin-top: 4px; +} + +.page-root .composer-audio-row { + padding-bottom: max(8px, env(safe-area-inset-bottom)); +} + +.page-root .error, +.page-root .composer-error, +.page-root .chat-error { + max-width: min(900px, calc(100vw - 48px)); + margin-inline: auto; + border-radius: 14px; +} diff --git a/frontend/src/assets/styles/ContentGrid.css b/frontend/src/assets/styles/ContentGrid.css index 3d211e3..202ce9e 100644 --- a/frontend/src/assets/styles/ContentGrid.css +++ b/frontend/src/assets/styles/ContentGrid.css @@ -624,3 +624,321 @@ figcaption { background: #f1f5f9; border-color: rgba(15, 23, 42, 0.12); } + +/* Senior QA final: Fabric List card rhythm, filename readability, and zoom toolbar polish. */ +.grid-page { + max-width: min(1180px, calc(100vw - 32px)); + padding-bottom: clamp(52px, 8vh, 86px); +} + +.grid-page .grid-header { + flex-direction: row; + justify-content: space-between; + gap: 18px; +} + +.grid-page .grid-left { + min-width: 0; +} + +.grid-page .grid-title-text, +.grid-page .grid-count-inline, +.grid-page .grid-controls.inline .grid-page-indicator, +.grid-page .audio-label, +.grid-page .media-meta, +.grid-page .media-meta time { + opacity: 1; + text-shadow: none; +} + +.grid-page .grid-controls.inline button, +.grid-page .grid-controls.inline .grid-page-indicator { + min-height: 40px; + border-radius: 999px; + font-weight: 850; +} + +.grid-page .grid-controls.inline button:not(:disabled) { + background: linear-gradient(135deg, var(--tz-action-yellow, #e0a82e), var(--tz-action-yellow-hover, #f0c463)); + border-color: var(--tz-action-yellow, #e0a82e); + color: var(--tz-action-yellow-text, #171101); +} + +.grid-page .grid-controls.inline button:disabled { + opacity: 0.74; +} + +.grid-page .media-grid { + gap: 20px; +} + +.grid-page .media-card { + min-height: 100%; + border-radius: 18px; + overflow: hidden; +} + +.grid-page .media-thumb { + width: 100%; + max-width: none; +} + +.grid-page .media-name { + min-height: 44px; + padding: 10px 14px; + display: block; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-style: normal; + font-weight: 850; + letter-spacing: 0.01em; + line-height: 1.25; +} + +.grid-page .zoom-icon { + top: 12px; + right: 12px; + bottom: auto; + width: 40px; + height: 40px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 999px; + background: rgba(15, 23, 42, 0.86); + border: 1px solid rgba(255, 255, 255, 0.28); + color: #ffffff; + box-shadow: 0 10px 22px rgba(0, 0, 0, 0.28); +} + +.grid-page .audio-box { + padding: 10px 12px; + gap: 8px; +} + +.grid-page .lb-stage { + width: min(94vw, 1180px); + height: min(86dvh, 820px); + border-radius: 22px; + padding: clamp(12px, 2vw, 22px); + background: rgba(15, 23, 42, 0.96); + border: 1px solid rgba(226, 232, 240, 0.22); + box-shadow: 0 24px 70px rgba(0, 0, 0, 0.42); +} + +.grid-page .lb-img { + max-width: 100%; + max-height: 100%; + object-fit: contain; + transform-origin: center center; +} + +.grid-page .lb-controls { + left: 50%; + right: auto; + bottom: 18px; + transform: translateX(-50%); + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px; + border-radius: 999px; + background: rgba(15, 23, 42, 0.88); + border: 1px solid rgba(226, 232, 240, 0.22); + box-shadow: 0 14px 34px rgba(0, 0, 0, 0.34); + backdrop-filter: blur(12px); +} + +.grid-page .lb-controls button { + min-width: 42px; + height: 40px; + border-radius: 999px; + border: 1px solid rgba(226, 232, 240, 0.26); + background: rgba(30, 41, 59, 0.92); + color: #f8fafc; + font-weight: 900; + line-height: 1; +} + +.grid-page .lb-controls button:hover:not(:disabled) { + background: rgba(51, 65, 85, 0.98); + transform: translateY(-1px); +} + +.grid-page .lb-controls button:disabled { + opacity: 0.46; +} + +.grid-page .lb-controls .lb-close { + background: linear-gradient(135deg, #ef4444, #b91c1c); + color: #ffffff; + border-color: rgba(248, 113, 113, 0.72); +} + +.grid-page .lb-caption { + left: 18px; + bottom: 18px; + max-width: min(420px, calc(100% - 44px)); + font-size: 14px; + font-weight: 850; + color: #f8fafc; + background: rgba(15, 23, 42, 0.80); + border: 1px solid rgba(226, 232, 240, 0.18); +} + +@media (max-width: 720px) { + .grid-page .grid-header { + flex-direction: column; + align-items: stretch; + } + + .grid-page .grid-controls.inline { + justify-content: center; + flex-wrap: wrap; + } + + .grid-page .lb-controls { + bottom: 12px; + max-width: calc(100% - 24px); + overflow-x: auto; + } + + .grid-page .lb-caption { + display: none; + } +} + +/* Senior QA top-up: List captions must show a clean user-facing filename, not generated storage suffixes. */ +.grid-page .media-name { + min-height: 46px; + padding-inline: 12px; + font-size: clamp(14px, 1.05vw, 17px); + line-height: 1.18; + text-align: center; + display: flex; + align-items: center; + justify-content: center; + white-space: normal; + overflow-wrap: anywhere; + word-break: normal; + hyphens: none; +} + +.grid-page .media-card { + background: var(--tz-ui-surface-raised, rgba(15, 23, 42, 0.94)); +} + +:root[data-theme="light"] .grid-page .media-name { + color: #0f172a; +} + +:root[data-theme="dark"] .grid-page .media-name { + color: #f8fafc; +} + +@media (min-width: 1080px) { + .grid-page .media-grid { + grid-template-columns: repeat(4, minmax(220px, 1fr)); + } +} + + +/* Strict senior QA follow-up: clean List titles and prevent awkward generated-name wrapping. */ +.grid-page .media-name { + min-height: 42px; + padding: 10px 14px; + display: block; + width: 100%; + text-align: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + overflow-wrap: normal; + word-break: normal; + hyphens: manual; + font-size: clamp(13px, 0.95vw, 16px); + line-height: 1.25; +} + +.grid-page .media-audio { + padding: 12px 14px 10px; +} + +.grid-page .audio-box { + min-height: 92px; + border-radius: 18px; +} + +.grid-page .zoom-icon { + top: 10px; + right: 10px; + width: 42px; + height: 42px; +} + +.grid-page .media-grid { + padding-bottom: clamp(42px, 6vh, 72px); +} + +.grid-page .lb-controls { + min-height: 54px; + max-width: min(440px, calc(100vw - 28px)); + overflow: visible; +} + +.grid-page .lb-controls button { + flex: 0 0 auto; + min-width: 42px; +} + +.grid-page .lb-controls button:nth-child(2) { + min-width: 96px; +} + +@media (max-width: 720px) { + .grid-page .media-name { + font-size: 14px; + } + + .grid-page .lb-controls { + gap: 6px; + overflow-x: auto; + } +} + +/* Final senior QA top-up: product-grade List titles and lightbox controls. */ +.grid-page .media-name { + height: 44px; + min-height: 44px; + max-height: 44px; + padding: 10px 14px; + display: flex; + align-items: center; + justify-content: center; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-transform: none; +} + +.grid-page .media-name::first-letter { + text-transform: uppercase; +} + +.grid-page .lb-controls { + min-height: 52px; + gap: 8px; + padding: 8px; +} + +.grid-page .lb-controls button { + height: 42px; + min-width: 42px; + border-radius: 999px; +} + +.grid-page .lb-controls button:nth-child(2) { + min-width: 96px; +} diff --git a/frontend/src/assets/styles/DescriptionBox.css b/frontend/src/assets/styles/DescriptionBox.css index 4e44093..3761475 100644 --- a/frontend/src/assets/styles/DescriptionBox.css +++ b/frontend/src/assets/styles/DescriptionBox.css @@ -299,3 +299,106 @@ opacity: 1 ; visibility: visible ; } + +/* QA follow-up: Prev/Next should visually share the same row level as Short/Long. */ +.analysis-page .description-wrapper .nav-buttons { + margin-top: 18px; + min-height: 46px; + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + transform: translateY(2px); +} + +.analysis-page .description-wrapper .nav-buttons .nav-btn { + min-height: 44px; + min-width: 132px; + border-radius: 999px; + border: 1px solid var(--tz-turmeric, #e0a82e); + background: linear-gradient(135deg, var(--tz-turmeric, #e0a82e), var(--tz-turmeric-soft, #f59e0b)); + color: #ffffff; + font-weight: 850; + box-shadow: 0 10px 24px rgba(224, 168, 46, 0.24); +} + +:root[data-theme="dark"] .analysis-page .description-wrapper .nav-buttons .nav-btn { + color: #171101; +} + +.analysis-page .description-wrapper .nav-buttons .nav-btn.disabled, +.analysis-page .description-wrapper .nav-buttons .nav-btn:disabled { + opacity: 0.56; + cursor: not-allowed; + box-shadow: none; +} + +/* Senior QA final: Prev/Next match Analysis buttons and sit on the same visual segment. */ +.analysis-page .description-wrapper .nav-buttons { + min-height: 46px; + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + margin-top: 18px; + transform: translateY(2px); +} + +.analysis-page .description-wrapper .nav-buttons .nav-btn { + min-height: 44px; + min-width: 142px; + padding: 0 18px; + border-radius: 999px; + border: 1px solid var(--tz-action-yellow, #e0a82e); + background: linear-gradient(135deg, var(--tz-action-yellow, #e0a82e), var(--tz-action-yellow-hover, #f0c463)); + color: var(--tz-action-yellow-text, #171101); + font-weight: 900; + box-shadow: 0 10px 24px rgba(224, 168, 46, 0.24); +} + +.analysis-page .description-wrapper .nav-buttons .nav-btn:not(:disabled):not(.disabled):hover { + transform: translateY(-1px); + box-shadow: 0 14px 30px rgba(224, 168, 46, 0.30); +} + +.analysis-page .description-wrapper .nav-buttons .nav-btn.disabled, +.analysis-page .description-wrapper .nav-buttons .nav-btn:disabled { + opacity: 0.68; + color: rgba(23, 17, 1, 0.72); + box-shadow: none; +} + +/* Senior QA top-up: Prev/Next disabled state remains intentional and aligned. */ +.analysis-page .description-wrapper .nav-buttons { + margin-top: 18px; + min-height: 46px; +} + +:root[data-theme="light"] .analysis-page .description-wrapper .nav-buttons .nav-btn, +:root[data-theme="dark"] .analysis-page .description-wrapper .nav-buttons .nav-btn { + color: #171101; +} + +.analysis-page .description-wrapper .nav-buttons .nav-btn.disabled, +.analysis-page .description-wrapper .nav-buttons .nav-btn:disabled { + background: linear-gradient(135deg, rgba(224, 168, 46, 0.58), rgba(240, 196, 99, 0.58)); + border-color: rgba(224, 168, 46, 0.66); + color: rgba(23, 17, 1, 0.72); +} + + +/* Strict senior QA follow-up: keep Prev/Next visually synced with Short/Long. */ +.analysis-page .description-wrapper .nav-buttons { + margin-top: 18px; + min-height: 46px; + align-items: center; +} + +.analysis-page .description-wrapper .nav-buttons .nav-btn, +.analysis-page .description-wrapper .nav-buttons .nav-btn.disabled, +.analysis-page .description-wrapper .nav-buttons .nav-btn:disabled { + min-height: 44px; + min-width: 142px; + border-radius: 999px; + font-weight: 900; +} diff --git a/frontend/src/assets/styles/DrawerToggle.css b/frontend/src/assets/styles/DrawerToggle.css index c99ac18..82ddd8b 100644 --- a/frontend/src/assets/styles/DrawerToggle.css +++ b/frontend/src/assets/styles/DrawerToggle.css @@ -56,3 +56,34 @@ .analysis-page:not(.drawer-open) .drawer-toggle-button { border-radius: 14px 0 0 14px; } + +/* Final senior QA top-up: drawer handle should not dominate the analysis page. */ +.analysis-page .drawer-toggle-wrapper { + right: clamp(248px, 19vw, 272px); +} + +.analysis-page .drawer-toggle-button { + width: 30px; + height: 58px; + border-radius: 12px 0 0 12px; + background: linear-gradient(135deg, #f0b429, #d97706); + border: 1px solid rgba(120, 53, 15, 0.28); + color: #171101; + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.22); + font-size: 22px; +} + +/* Final micro top-up: drawer handle should follow theme with the drawer. */ +:root[data-theme="light"] .analysis-page .drawer-toggle-button { + background: rgba(255, 255, 255, 0.96); + color: #b45309; + border-color: rgba(15, 23, 42, 0.16); + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.16); +} + +:root[data-theme="dark"] .analysis-page .drawer-toggle-button { + background: linear-gradient(135deg, #f0b429, #d97706); + color: #171101; + border-color: rgba(120, 53, 15, 0.28); + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.22); +} diff --git a/frontend/src/assets/styles/EmptyState.css b/frontend/src/assets/styles/EmptyState.css index 247ce25..0193d60 100644 --- a/frontend/src/assets/styles/EmptyState.css +++ b/frontend/src/assets/styles/EmptyState.css @@ -132,3 +132,125 @@ background: var(--tz-ui-control-bg, rgba(30, 41, 59, 0.96)); border-color: var(--tz-ui-border, rgba(226, 232, 240, 0.24)); } + +/* QA follow-up: keep the four landing prompt buttons on one row on desktop and make each state visually distinct. */ +.page-root .empty-state-large .chips { + width: min(980px, 100%); + margin: 0 auto; + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; + align-items: stretch; + justify-content: center; +} + +.page-root .empty-state-large .chip, +.page-root .empty-state-large .chip:nth-child(odd), +.page-root .empty-state-large .chip:nth-child(even) { + width: 100%; + min-height: 44px; + display: inline-flex; + align-items: center; + justify-content: center; + padding: 10px 14px; + white-space: normal; + line-height: 1.18; + text-align: center; + font-weight: 850; +} + +.page-root .empty-state-large .chip:nth-child(1) { + background: #fff7ed; + border-color: #fed7aa; + color: #7c2d12; +} + +.page-root .empty-state-large .chip:nth-child(2) { + background: #eef2ff; + border-color: #c7d2fe; + color: #312e81; +} + +.page-root .empty-state-large .chip:nth-child(3) { + background: #ecfdf5; + border-color: #bbf7d0; + color: #064e3b; +} + +.page-root .empty-state-large .chip:nth-child(4) { + background: #fdf2f8; + border-color: #fbcfe8; + color: #831843; +} + +:root[data-theme="dark"] .page-root .empty-state-large .chip:nth-child(1) { + background: rgba(251, 146, 60, 0.18); + border-color: rgba(251, 146, 60, 0.42); + color: #fed7aa; +} + +:root[data-theme="dark"] .page-root .empty-state-large .chip:nth-child(2) { + background: rgba(99, 102, 241, 0.2); + border-color: rgba(129, 140, 248, 0.46); + color: #c7d2fe; +} + +:root[data-theme="dark"] .page-root .empty-state-large .chip:nth-child(3) { + background: rgba(16, 185, 129, 0.18); + border-color: rgba(52, 211, 153, 0.42); + color: #bbf7d0; +} + +:root[data-theme="dark"] .page-root .empty-state-large .chip:nth-child(4) { + background: rgba(236, 72, 153, 0.2); + border-color: rgba(244, 114, 182, 0.46); + color: #fbcfe8; +} + +@media (max-width: 960px) { + .page-root .empty-state-large .chips { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 560px) { + .page-root .empty-state-large .chips { + grid-template-columns: 1fr; + } +} + +/* Senior QA final: desktop prompts must look intentional at browser zoom levels. */ +.page-root .empty-state-large { + width: min(980px, 100%); + padding-inline: 8px; +} + +.page-root .empty-state-large .chips { + grid-template-columns: repeat(4, minmax(150px, 1fr)); + align-items: stretch; +} + +.page-root .empty-state-large .chip { + min-width: 0; + min-height: var(--tz-control-height, 44px); + border-radius: var(--tz-control-radius, 999px); + border-width: 1px; + box-shadow: 0 10px 22px rgba(15, 23, 42, 0.10); +} + +.page-root .empty-state-large .chip:hover:not(:disabled) { + transform: translateY(-2px); + box-shadow: 0 16px 30px rgba(15, 23, 42, 0.14); +} + +@media (min-width: 961px) and (max-width: 1180px) { + .page-root .empty-state-large .chips { + grid-template-columns: repeat(4, minmax(132px, 1fr)); + gap: 10px; + } + + .page-root .empty-state-large .chip { + padding-inline: 10px; + font-size: 13px; + } +} diff --git a/frontend/src/assets/styles/FabricChat.css b/frontend/src/assets/styles/FabricChat.css index ff2983c..e237f26 100644 --- a/frontend/src/assets/styles/FabricChat.css +++ b/frontend/src/assets/styles/FabricChat.css @@ -618,3 +618,77 @@ font-size: 22px; } } + +/* Senior QA final: stabilize chat layout on 1366x768 and 125% browser zoom. */ +.page-root { + padding-bottom: max(18px, env(safe-area-inset-bottom)); +} + +.page-root .chat-card-topbar { + flex: 0 0 auto; +} + +.page-root .chat-body { + min-height: min(680px, calc(100dvh - 154px)); + padding-bottom: 6px; +} + +.page-root.is-empty .chat-scroll-area { + justify-content: center; + padding-top: clamp(18px, 4.4vh, 42px); + padding-bottom: clamp(16px, 3vh, 34px); +} + +.page-root .chat-card-actions .btn { + min-height: var(--tz-control-height, 44px); + border-radius: var(--tz-control-radius, 999px); + font-weight: 850; +} + +.page-root .chat-card-actions .btn-secondary:hover:not(:disabled), +.page-root .chat-card-actions .btn-ghost:hover:not(:disabled) { + transform: translateY(-1px); +} + +@media (max-height: 760px) { + .page-root .chat-card-topbar { + margin-top: 8px; + margin-bottom: 8px; + padding-block: 10px; + } + + .page-root .chat-body { + min-height: calc(100dvh - 144px); + } + + .page-root.is-empty .chat-scroll-area { + justify-content: flex-start; + } +} + +/* Final senior QA top-up: make the Chat empty state feel intentional, not empty. */ +.page-root.is-empty .chat-scroll-area { + justify-content: center; + padding-top: clamp(22px, 4vh, 48px); + padding-bottom: clamp(22px, 4vh, 48px); +} + +.page-root .empty-state-large { + max-width: 980px; + margin-inline: auto; +} + +.page-root .empty-state-large .sample-prompts, +.page-root .sample-prompts { + gap: clamp(10px, 1.2vw, 16px); +} + +.page-root .chat-body { + min-height: min(700px, calc(100dvh - 150px)); +} + +@media (max-height: 760px) { + .page-root .chat-body { + min-height: calc(100dvh - 138px); + } +} diff --git a/frontend/src/assets/styles/FabricSearch.css b/frontend/src/assets/styles/FabricSearch.css index 78e322a..b5e5c0b 100644 --- a/frontend/src/assets/styles/FabricSearch.css +++ b/frontend/src/assets/styles/FabricSearch.css @@ -2749,3 +2749,927 @@ font-size: clamp(40px, 8vw, 72px); } } + +/* QA follow-up: Search buttons must remain visible in both themes and while disabled. */ +.fabric-search .hero__search-row .btn--primary, +.fabric-search .image-preview__search-row .btn--primary, +.fabric-search .search-bar__text-row .btn--primary, +.fabric-search .search-bar__image-actions .btn--primary { + background: linear-gradient(135deg, var(--tz-turmeric, #e0a82e), var(--tz-turmeric-soft, #f59e0b)); + border: 1px solid var(--tz-turmeric, #e0a82e); + color: #171101; + opacity: 1; + text-shadow: none; + box-shadow: 0 10px 24px rgba(224, 168, 46, 0.24); +} + +:root[data-theme="light"] .fabric-search .hero__search-row .btn--primary, +:root[data-theme="light"] .fabric-search .image-preview__search-row .btn--primary, +:root[data-theme="light"] .fabric-search .search-bar__text-row .btn--primary, +:root[data-theme="light"] .fabric-search .search-bar__image-actions .btn--primary { + color: #ffffff; +} + +.fabric-search .hero__search-row .btn--primary:disabled, +.fabric-search .image-preview__search-row .btn--primary:disabled, +.fabric-search .search-bar__text-row .btn--primary:disabled, +.fabric-search .search-bar__image-actions .btn--primary:disabled { + opacity: 0.68; + filter: saturate(0.9); + color: #171101; +} + +:root[data-theme="light"] .fabric-search .hero__search-row .btn--primary:disabled, +:root[data-theme="light"] .fabric-search .image-preview__search-row .btn--primary:disabled, +:root[data-theme="light"] .fabric-search .search-bar__text-row .btn--primary:disabled, +:root[data-theme="light"] .fabric-search .search-bar__image-actions .btn--primary:disabled { + color: #ffffff; +} + +/* Senior QA final: Search visual stability, button visibility, settings flow, and zoom toolbar polish. */ +.fabric-search { + --color-muted: var(--tz-ui-text-muted, #cbd5e1); + color: var(--tz-ui-text, #f8fafc); +} + +:root[data-theme="light"] .fabric-search { + --color-muted: #475569; + --color-text: #0f172a; + --color-surface: rgba(255, 255, 255, 0.92); + --color-surface-2: rgba(255, 255, 255, 0.98); + --color-border: rgba(15, 23, 42, 0.12); + --color-border-2: rgba(15, 23, 42, 0.18); + --color-border-gold: rgba(183, 121, 31, 0.36); +} + +.fabric-search__body { + padding-top: clamp(28px, 4vh, 48px); +} + +.fabric-search--has-results { + padding-top: clamp(168px, 22vh, 220px); +} + +.fabric-search .btn, +.fabric-search .pagination__btn, +.fabric-search .settings-panel__toggle, +.fabric-search .category-picker__toggle-all { + min-height: 42px; + border-radius: 999px; + font-weight: 850; +} + +.fabric-search .btn--primary, +.fabric-search .search-bar .btn--primary, +.fabric-search .image-preview__search-row .btn--primary, +.fabric-search .category-picker--compact .btn--primary { + background: linear-gradient(135deg, var(--tz-action-yellow, #e0a82e), var(--tz-action-yellow-hover, #f0c463)); + color: var(--tz-action-yellow-text, #171101); + border: 1px solid rgba(180, 83, 9, 0.36); + box-shadow: 0 12px 28px rgba(224, 168, 46, 0.24); + opacity: 1; + text-shadow: none; +} + +.fabric-search .btn--primary:disabled { + opacity: 0.72; + color: rgba(23, 17, 1, 0.76); + box-shadow: none; +} + +.fabric-search .hero__header { + padding-top: clamp(18px, 4vh, 44px); + padding-bottom: clamp(18px, 3vh, 30px); +} + +.fabric-search .hero__title { + color: var(--tz-ui-text, #f8fafc); + font-size: clamp(34px, 5vw, 60px); + text-shadow: none; +} + +:root[data-theme="light"] .fabric-search .hero__title { + color: #0f172a; +} + +.fabric-search .hero__subtitle, +.fabric-search .hero__eyebrow, +.fabric-search .category-picker__title, +.fabric-search .limit-slider__label, +.fabric-search .limit-slider__value, +.fabric-search .image-preview__pane-title, +.fabric-search .results-section__count-label, +.fabric-search .results-section__filter-prefix, +.fabric-search .pagination__label, +.fabric-search__empty { + color: var(--tz-ui-text-muted, #cbd5e1); + opacity: 1; + text-shadow: none; +} + +:root[data-theme="light"] .fabric-search .hero__subtitle, +:root[data-theme="light"] .fabric-search .hero__eyebrow, +:root[data-theme="light"] .fabric-search .category-picker__title, +:root[data-theme="light"] .fabric-search .limit-slider__label, +:root[data-theme="light"] .fabric-search .limit-slider__value, +:root[data-theme="light"] .fabric-search .image-preview__pane-title, +:root[data-theme="light"] .fabric-search .results-section__count-label, +:root[data-theme="light"] .fabric-search .results-section__filter-prefix, +:root[data-theme="light"] .fabric-search .pagination__label, +:root[data-theme="light"] .fabric-search__empty { + color: #334155; +} + +.fabric-search .settings-panel__toggle { + background: var(--tz-ui-control-bg, rgba(30, 41, 59, 0.94)); + color: var(--tz-ui-text, #f8fafc); + border-color: var(--tz-ui-border, rgba(226, 232, 240, 0.22)); +} + +:root[data-theme="light"] .fabric-search .settings-panel__toggle { + background: #ffffff; + color: #0f172a; + border-color: rgba(15, 23, 42, 0.16); +} + +.fabric-search.fabric-search--previewing .settings-panel__content { + position: static; + margin-top: 10px; + width: min(360px, calc(100vw - 48px)); +} + +.fabric-search.fabric-search--previewing .fabric-search__settings { + width: 100%; + display: flex; + flex-direction: column; + align-items: flex-end; +} + +.fabric-search .settings-panel__content { + color: var(--tz-ui-text, #f8fafc); + background: var(--tz-ui-surface-raised, rgba(15, 23, 42, 0.96)); + border: 1px solid var(--tz-ui-border, rgba(226, 232, 240, 0.22)); + box-shadow: var(--tz-ui-shadow-panel, 0 18px 48px rgba(0, 0, 0, 0.28)); +} + +:root[data-theme="light"] .fabric-search .settings-panel__content { + color: #0f172a; + background: #ffffff; + border-color: rgba(15, 23, 42, 0.16); +} + +.fabric-search .category-picker__chip { + min-height: 40px; + border-radius: 999px; + color: var(--tz-ui-text-muted, #cbd5e1); +} + +.fabric-search .category-picker__chip--active, +:root[data-theme="light"] .fabric-search .category-picker__chip--active, +:root[data-theme="dark"] .fabric-search .category-picker__chip--active { + background: linear-gradient(135deg, var(--tz-action-yellow, #e0a82e), var(--tz-action-yellow-hover, #f0c463)); + color: var(--tz-action-yellow-text, #171101); + border-color: var(--tz-action-yellow, #e0a82e); + box-shadow: 0 0 0 2px rgba(224, 168, 46, 0.22), 0 12px 28px rgba(224, 168, 46, 0.16); +} + +.fabric-search .result-grid--full { + align-items: stretch; +} + +.fabric-search .result-card { + display: flex; + flex-direction: column; + min-height: 100%; + border-color: var(--tz-ui-border, rgba(226, 232, 240, 0.18)); +} + +.fabric-search .result-card__thumb { + background: var(--tz-ui-surface-subtle, rgba(15, 23, 42, 0.72)); +} + +.fabric-search .result-card__thumb img { + object-fit: cover; +} + +.fabric-search .result-card__zoom-btn { + opacity: 1; + transform: none; + width: 38px; + height: 38px; + border-radius: 999px; + background: rgba(15, 23, 42, 0.86); + border: 1px solid rgba(226, 232, 240, 0.24); + color: #ffffff; + box-shadow: 0 10px 22px rgba(0, 0, 0, 0.28); +} + +.fabric-search .result-card__name { + color: var(--tz-ui-text, #f8fafc); + min-height: 40px; + display: block; + text-align: left; + background: transparent; + border: 0; + width: 100%; +} + +:root[data-theme="light"] .fabric-search .result-card__name { + color: #0f172a; +} + +.fabric-search .lightbox__stage { + border-radius: 24px; + border: 1px solid rgba(226, 232, 240, 0.22); + box-shadow: 0 24px 70px rgba(0, 0, 0, 0.44); +} + +.fabric-search .lightbox__controls { + left: 50%; + right: auto; + bottom: 18px; + transform: translateX(-50%); + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px; + border-radius: 999px; + background: rgba(15, 23, 42, 0.88); + border: 1px solid rgba(226, 232, 240, 0.22); + box-shadow: 0 14px 34px rgba(0, 0, 0, 0.34); +} + +.fabric-search .lightbox__control-btn { + width: auto; + min-width: 42px; + height: 40px; + border-radius: 999px; + color: #f8fafc; + background: rgba(30, 41, 59, 0.92); + border: 1px solid rgba(226, 232, 240, 0.26); + font-weight: 900; + line-height: 1; +} + +.fabric-search .lightbox__control-btn:hover:not(:disabled) { + background: rgba(51, 65, 85, 0.98); + transform: translateY(-1px); +} + +.fabric-search .lightbox__control-btn:disabled { + opacity: 0.46; + cursor: not-allowed; +} + +.fabric-search .lightbox__control-btn--reset { + min-width: 96px; +} + +.fabric-search .lightbox__control-btn--close { + background: linear-gradient(135deg, #ef4444, #b91c1c); + color: #ffffff; + border-color: rgba(248, 113, 113, 0.72); +} + +@media (max-width: 760px) { + .fabric-search--has-results { + padding-top: 0; + } + + .search-bar--sticky { + position: sticky; + top: 0; + } + + .fabric-search .lightbox__controls { + max-width: calc(100% - 24px); + overflow-x: auto; + bottom: 12px; + } +} + +/* Senior QA top-up: stabilize Search empty view, primary action, category cards and footer rhythm. */ +.fabric-search { + min-height: calc(100dvh - var(--site-header-height, 64px)); + padding-top: clamp(22px, 4vh, 54px); +} + +.fabric-search .search-page, +.fabric-search .search-shell, +.fabric-search .search-main { + min-height: auto; +} + +.fabric-search .search-form, +.fabric-search .search-row, +.fabric-search .hero-search-row { + align-items: stretch; +} + +.fabric-search .search-button, +.fabric-search .search-btn, +.fabric-search button[type="submit"] { + min-height: 52px; + border-radius: 14px; + border: 1px solid var(--tz-action-yellow, #e0a82e); + background: linear-gradient(135deg, var(--tz-action-yellow, #e0a82e), var(--tz-action-yellow-hover, #f0c463)); + color: var(--tz-action-yellow-text, #171101); + font-weight: 900; + box-shadow: 0 12px 26px rgba(224, 168, 46, 0.22); + opacity: 1; +} + +.fabric-search .search-button:disabled, +.fabric-search .search-btn:disabled, +.fabric-search button[type="submit"]:disabled { + opacity: 0.72; + cursor: not-allowed; + color: rgba(23, 17, 1, 0.78); +} + +.fabric-search .category-picker, +.fabric-search .filter-card, +.fabric-search .filter-panel { + background: var(--tz-ui-surface-raised, rgba(15, 23, 42, 0.86)); + border-color: var(--tz-ui-border, rgba(226, 232, 240, 0.18)); +} + +.fabric-search .category-picker__label, +.fabric-search .filter-label, +.fabric-search .category-picker h3, +.fabric-search .db-control-panel__label, +.fabric-search .settings-panel__label { + color: var(--tz-ui-text, #f8fafc); + opacity: 1; +} + +.fabric-search .category-chip, +.fabric-search .category-button, +.fabric-search .category-picker button { + color: var(--tz-ui-text, #f8fafc); + background: var(--tz-ui-control-bg, rgba(15, 23, 42, 0.78)); + border-color: var(--tz-ui-border, rgba(226, 232, 240, 0.20)); +} + +.fabric-search .category-chip[aria-pressed="true"], +.fabric-search .category-button[aria-pressed="true"], +.fabric-search .category-picker button[aria-pressed="true"] { + color: var(--tz-action-yellow-text, #171101); + background: linear-gradient(135deg, var(--tz-action-yellow, #e0a82e), var(--tz-action-yellow-hover, #f0c463)); + border-color: var(--tz-action-yellow, #e0a82e); +} + +:root[data-theme="light"] .fabric-search .category-picker, +:root[data-theme="light"] .fabric-search .filter-card, +:root[data-theme="light"] .fabric-search .filter-panel { + background: rgba(255, 255, 255, 0.94); + border-color: rgba(15, 23, 42, 0.12); +} + +:root[data-theme="light"] .fabric-search .category-picker__label, +:root[data-theme="light"] .fabric-search .filter-label, +:root[data-theme="light"] .fabric-search .category-picker h3, +:root[data-theme="light"] .fabric-search .db-control-panel__label, +:root[data-theme="light"] .fabric-search .settings-panel__label { + color: #0f172a; +} + +:root[data-theme="light"] .fabric-search .category-chip, +:root[data-theme="light"] .fabric-search .category-button, +:root[data-theme="light"] .fabric-search .category-picker button { + color: #0f172a; + background: rgba(255, 255, 255, 0.96); + border-color: rgba(15, 23, 42, 0.14); +} + +.fabric-search .search-lightbox__toolbar, +.fabric-search .lightbox-toolbar, +.fabric-search .lightbox-controls { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 10px; + padding: 10px; + border-radius: 999px; + background: rgba(8, 11, 20, 0.82); + border: 1px solid rgba(226, 232, 240, 0.18); + box-shadow: 0 18px 44px rgba(0, 0, 0, 0.30); +} + +.fabric-search .search-lightbox__toolbar button, +.fabric-search .lightbox-toolbar button, +.fabric-search .lightbox-controls button { + width: 42px; + height: 42px; + min-width: 42px; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + color: #f8fafc; + background: rgba(15, 23, 42, 0.92); + border: 1px solid rgba(226, 232, 240, 0.20); +} + +.fabric-search .search-lightbox__toolbar button:disabled, +.fabric-search .lightbox-toolbar button:disabled, +.fabric-search .lightbox-controls button:disabled { + opacity: 0.46; + cursor: not-allowed; +} + +@media (max-height: 760px) { + .fabric-search { + padding-top: 18px; + } +} + +/* Senior QA top-up: actual Search lightbox toolbar class names for + / - / Reset / Close polish. */ +.fabric-search .lightbox__controls { + max-width: calc(100vw - 32px); + display: inline-flex; + align-items: center; + justify-content: center; + gap: 10px; + padding: 10px; + border-radius: 999px; + background: rgba(8, 11, 20, 0.86); + border: 1px solid rgba(226, 232, 240, 0.22); + box-shadow: 0 18px 44px rgba(0, 0, 0, 0.34); +} + +.fabric-search .lightbox__control-btn { + width: 42px; + min-width: 42px; + height: 42px; + padding: 0; + border-radius: 999px; + display: inline-flex; + align-items: center; + justify-content: center; + color: #f8fafc; + background: rgba(15, 23, 42, 0.94); + border: 1px solid rgba(226, 232, 240, 0.24); + font-size: 16px; + line-height: 1; +} + +.fabric-search .lightbox__control-btn--reset { + width: auto; + min-width: 98px; + gap: 6px; + padding-inline: 14px; +} + +.fabric-search .lightbox__control-btn:disabled { + opacity: 0.42; + cursor: not-allowed; + transform: none; +} + +.fabric-search .lightbox__control-btn--close { + background: linear-gradient(135deg, #ef4444, #b91c1c); + border-color: rgba(248, 113, 113, 0.72); + color: #ffffff; +} + +@media (max-width: 520px) { + .fabric-search .lightbox__controls { + gap: 6px; + padding: 8px; + } + + .fabric-search .lightbox__control-btn { + width: 38px; + min-width: 38px; + height: 38px; + } + + .fabric-search .lightbox__control-btn--reset { + min-width: 84px; + } +} + + +/* Strict senior QA follow-up: compact search result toolbar, readable apply button, + cleaner filenames, and robust zoom controls at 100–125% browser zoom. */ +.fabric-search.fabric-search--has-results { + padding-top: clamp(142px, 18vh, 184px); +} + +.fabric-search .search-bar--sticky { + min-height: auto; + padding: 10px 0 12px; +} + +.fabric-search .search-bar__filters { + grid-template-columns: minmax(0, 1fr) minmax(210px, 260px); + align-items: center; + gap: 14px; +} + +.fabric-search .search-bar__filters .limit-slider { + align-self: center; + padding: 8px 10px; +} + +.fabric-search .category-picker--compact { + min-height: 0; + padding: 14px 16px; + border-radius: 18px; +} + +.fabric-search .category-picker--compact .category-picker__header { + margin-bottom: 12px; +} + +.fabric-search .category-picker--compact .category-picker__grid { + align-items: center; + gap: 10px; +} + +.fabric-search .category-picker__apply, +.fabric-search .category-picker--compact .category-picker__apply.btn--primary { + min-width: 158px; + min-height: 44px; + padding: 0 18px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + border-radius: 999px; + border: 1px solid var(--tz-action-yellow, #e0a82e); + background: linear-gradient(135deg, var(--tz-action-yellow, #e0a82e), var(--tz-action-yellow-hover, #f0c463)); + color: var(--tz-action-yellow-text, #171101); + font-weight: 900; + letter-spacing: 0.01em; + box-shadow: 0 12px 28px rgba(224, 168, 46, 0.24); + opacity: 1; + text-shadow: none; +} + +.fabric-search .category-picker__apply span { + color: inherit; + opacity: 1; +} + +.fabric-search .result-card__name { + min-height: 42px; + padding: 12px 14px; + display: block; + width: 100%; + color: var(--tz-ui-text, #f8fafc); + font-family: var(--font-sans, Inter, system-ui, sans-serif); + font-size: 13px; + font-weight: 850; + letter-spacing: 0.005em; + line-height: 1.2; + text-transform: none; + text-align: left; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +:root[data-theme="light"] .fabric-search .result-card__name { + color: #0f172a; +} + +.fabric-search .hero__search-row { + align-items: stretch; +} + +.fabric-search .hero__search-row .btn--primary { + min-width: 116px; +} + +.fabric-search:not(.fabric-search--has-results):not(.fabric-search--previewing) .fabric-search__body { + min-height: calc(100dvh - 132px); +} + +.fabric-search .hero__search-controls { + max-width: 1080px; +} + +.fabric-search .lightbox__stage { + max-width: min(1180px, calc(100vw - 32px)); + max-height: min(840px, calc(100dvh - 32px)); + overflow: hidden; +} + +.fabric-search .lightbox__image { + max-width: calc(100vw - 72px); + max-height: calc(100dvh - 116px); + object-fit: contain; + transform-origin: center center; +} + +.fabric-search .lightbox__controls { + bottom: clamp(12px, 2vh, 20px); + min-height: 54px; + max-width: min(440px, calc(100vw - 28px)); + overflow: visible; +} + +.fabric-search .lightbox__control-btn { + flex: 0 0 auto; +} + +@media (max-width: 980px) { + .fabric-search.fabric-search--has-results { + padding-top: 0; + } + + .fabric-search .search-bar--sticky { + position: sticky; + top: 0; + } + + .fabric-search .search-bar__filters { + grid-template-columns: 1fr; + } +} + +@media (max-height: 760px) and (min-width: 981px) { + .fabric-search.fabric-search--has-results { + padding-top: 134px; + } + + .fabric-search .category-picker--compact { + padding: 12px 14px; + } +} + +/* Senior strict top-up: keep the compact Search toolbar action visible in light theme too. */ +.fabric-search .category-picker--compact .category-picker__apply.btn.btn--primary, +:root[data-theme="light"] .fabric-search .category-picker--compact .category-picker__apply.btn.btn--primary, +:root[data-theme="dark"] .fabric-search .category-picker--compact .category-picker__apply.btn.btn--primary { + visibility: visible; + min-width: 166px; + min-height: 46px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + background: linear-gradient(135deg, #e0a82e 0%, #facc15 100%); + color: #171101; + border: 1px solid rgba(180, 83, 9, 0.38); + box-shadow: 0 14px 30px rgba(224, 168, 46, 0.28); + opacity: 1; + text-shadow: none; +} + +.fabric-search .category-picker--compact .category-picker__apply.btn.btn--primary span, +:root[data-theme="light"] .fabric-search .category-picker--compact .category-picker__apply.btn.btn--primary span, +:root[data-theme="dark"] .fabric-search .category-picker--compact .category-picker__apply.btn.btn--primary span { + display: inline-flex; + color: #171101; + opacity: 1; + visibility: visible; + font-weight: 900; + text-shadow: none; +} + +.fabric-search .category-picker--compact .category-picker__apply.btn.btn--primary:hover, +:root[data-theme="light"] .fabric-search .category-picker--compact .category-picker__apply.btn.btn--primary:hover, +:root[data-theme="dark"] .fabric-search .category-picker--compact .category-picker__apply.btn.btn--primary:hover { + background: linear-gradient(135deg, #f0c463 0%, #facc15 100%); + transform: translateY(-1px); +} + +.fabric-search .category-picker--compact .category-picker__apply.btn.btn--primary:focus-visible, +:root[data-theme="light"] .fabric-search .category-picker--compact .category-picker__apply.btn.btn--primary:focus-visible, +:root[data-theme="dark"] .fabric-search .category-picker--compact .category-picker__apply.btn.btn--primary:focus-visible { + outline: 3px solid rgba(224, 168, 46, 0.36); + outline-offset: 3px; +} + +:root[data-theme="light"] .fabric-search .category-picker--compact, +:root[data-theme="light"] .fabric-search .search-bar--sticky { + color: #0f172a; +} + +/* Final senior QA top-up: remove the light-theme Settings wrapper box, compact the + result toolbar, and keep Apply Filter visible only when it actually exists. */ +.fabric-search .fabric-search__settings, +:root[data-theme="light"] .fabric-search .fabric-search__settings, +:root[data-theme="dark"] .fabric-search .fabric-search__settings, +.fabric-search .settings-panel, +:root[data-theme="light"] .fabric-search .settings-panel, +:root[data-theme="dark"] .fabric-search .settings-panel { + background: transparent; + border: 0; + box-shadow: none; + padding: 0; + margin: 0; + border-radius: 0; + backdrop-filter: none; + -webkit-backdrop-filter: none; + overflow: visible; +} + +.fabric-search .settings-panel__toggle, +:root[data-theme="light"] .fabric-search .settings-panel__toggle, +:root[data-theme="dark"] .fabric-search .settings-panel__toggle { + min-height: 48px; + padding: 0 22px; + border-radius: 999px; + background: var(--tz-ui-surface-raised, rgba(15, 23, 42, 0.92)); + border: 1px solid var(--tz-ui-border, rgba(226, 232, 240, 0.22)); + box-shadow: 0 14px 34px rgba(15, 23, 42, 0.14); + color: var(--tz-ui-text, #f8fafc); + overflow: hidden; + outline-offset: 3px; +} + +:root[data-theme="light"] .fabric-search .settings-panel__toggle { + background: rgba(255, 255, 255, 0.92); + border-color: rgba(15, 23, 42, 0.14); + color: #0f172a; + box-shadow: 0 14px 34px rgba(15, 23, 42, 0.10); +} + +.fabric-search .settings-panel__toggle::before, +.fabric-search .settings-panel__toggle::after, +.fabric-search .settings-panel::before, +.fabric-search .settings-panel::after, +.fabric-search .fabric-search__settings::before, +.fabric-search .fabric-search__settings::after { + content: none; + display: none; +} + +.fabric-search .search-bar--sticky { + padding: 10px clamp(10px, 1.2vw, 18px) 12px; +} + +.fabric-search .search-bar__inner, +.fabric-search .search-bar__filters { + max-width: min(1340px, calc(100vw - 44px)); + padding-inline: 0; +} + +.fabric-search .search-bar__filters { + grid-template-columns: minmax(0, 1fr) minmax(184px, 236px); + gap: clamp(10px, 1.2vw, 16px); + align-items: stretch; +} + +.fabric-search .category-picker--compact { + padding: 14px 16px; + min-height: 126px; + display: flex; + flex-direction: column; + justify-content: center; +} + +.fabric-search .category-picker--compact .category-picker__header { + margin-bottom: 10px; +} + +.fabric-search .category-picker--compact .category-picker__grid { + display: flex; + align-items: center; + gap: 9px; + row-gap: 9px; +} + +.fabric-search .category-picker--compact .category-picker__chip { + min-height: 42px; + padding: 0 15px; + border-radius: 999px; +} + +.fabric-search .category-picker--compact .category-picker__apply.btn.btn--primary { + min-width: 150px; + min-height: 42px; + flex: 0 0 auto; +} + +.fabric-search .category-picker__apply-label, +.fabric-search .category-picker__apply-arrow { + display: inline-flex; + align-items: center; + color: inherit; + opacity: 1; + visibility: visible; +} + +.fabric-search .search-bar__filters .limit-slider { + min-height: 126px; + display: flex; + align-items: center; + gap: 10px; + padding: 14px 8px; +} + +.fabric-search .search-bar__image-row { + flex: 1 1 auto; +} + +.fabric-search .search-bar__image-actions .btn, +.fabric-search .search-bar__inner > .btn { + border-radius: 999px; + min-height: 44px; + font-weight: 850; + white-space: nowrap; +} + +.fabric-search .result-card__name { + min-height: 48px; + padding: 12px 14px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-transform: none; +} + +.fabric-search .results-section { + margin-top: clamp(14px, 2vh, 24px); +} + +.fabric-search .lightbox__controls { + gap: 8px; + padding: 8px; + min-height: 52px; +} + +.fabric-search .lightbox__control-btn { + width: 42px; + min-width: 42px; + height: 42px; +} + +.fabric-search .lightbox__control-btn--reset { + min-width: 96px; +} + +@media (max-width: 980px) { + .fabric-search .search-bar__filters { + grid-template-columns: 1fr; + } + + .fabric-search .search-bar__filters .limit-slider, + .fabric-search .category-picker--compact { + min-height: auto; + } +} + +/* Final micro top-up: center Search CTA text and remove light-theme Settings wrapper artefact. */ +.fabric-search .btn, +.fabric-search .btn--primary, +.fabric-search .settings-panel__toggle { + justify-content: center; + text-align: center; +} + +.fabric-search .hero__search-row > .btn--primary, +.fabric-search .search-bar__text-row > .btn--primary, +.fabric-search .image-preview__search-row > .btn--primary { + min-width: 136px; + padding-inline: 24px; + line-height: 1; + display: inline-flex; + align-items: center; + justify-content: center; + text-align: center; +} + +.fabric-search .hero__search-row > .btn--primary { + flex: 0 0 136px; +} + +.fabric-search .settings-panel, +.fabric-search .fabric-search__settings, +.fabric-search .fabric-search__top-bar { + background: transparent; + border: 0; + box-shadow: none; + backdrop-filter: none; + -webkit-backdrop-filter: none; +} + +.fabric-search .settings-panel { + display: inline-flex; + width: max-content; + max-width: max-content; + overflow: visible; + contain: layout; +} + +.fabric-search .settings-panel__toggle { + width: auto; + max-width: max-content; + background-clip: padding-box; + border-radius: 999px; + overflow: hidden; + backdrop-filter: none; + -webkit-backdrop-filter: none; + clip-path: inset(0 round 999px); +} + +:root[data-theme="light"] .fabric-search .settings-panel__toggle { + background: rgba(255, 255, 255, 0.96); + border-color: rgba(15, 23, 42, 0.16); + box-shadow: 0 12px 28px rgba(15, 23, 42, 0.10); +} diff --git a/frontend/src/assets/styles/ImageDescription.css b/frontend/src/assets/styles/ImageDescription.css index e37a5ed..24c5386 100644 --- a/frontend/src/assets/styles/ImageDescription.css +++ b/frontend/src/assets/styles/ImageDescription.css @@ -692,3 +692,344 @@ :root[data-theme="dark"] .analysis-page .analysis-popup__content p { color: #cbd5e1; } + +/* Senior QA final: Analysis page vertical rhythm and header visibility. */ +.analysis-page.home-container { + padding-top: clamp(18px, 3vh, 36px); + padding-bottom: clamp(36px, 5vh, 64px); + min-height: calc(100dvh - 64px); +} + +.analysis-page .animated-header { + min-height: 54px; + margin-bottom: clamp(10px, 2vh, 18px); +} + +.analysis-page .animated-header h1 { + color: var(--tz-ui-text, #f8fafc); + border-bottom-color: var(--tz-action-yellow, #e0a82e); + text-shadow: none; +} + +:root[data-theme="light"] .analysis-page .animated-header h1 { + color: #0f172a; +} + +.analysis-page .grid { + align-items: start; + gap: clamp(18px, 3vw, 30px); +} + +@media (max-height: 760px) { + .analysis-page.home-container { + padding-top: 12px; + } + + .analysis-page .animated-header { + margin-bottom: 8px; + } +} + +/* Senior QA top-up: Analysis layout must stay balanced at 1366x768 and 125% zoom. */ +.analysis-page.home-container { + overflow-x: hidden; +} + +.analysis-page .top-texts { + color: var(--tz-ui-text, #f8fafc); + font-weight: 850; +} + +:root[data-theme="dark"] .analysis-page .sample-text, +:root[data-theme="dark"] .analysis-page .top-texts { + color: #f8fafc; + opacity: 1; +} + +.analysis-page .sample-text { + color: var(--tz-action-yellow, #e0a82e); + text-shadow: none; +} + +.analysis-page .result-wrapper.analysis-grid { + width: min(1120px, calc(100vw - 64px)); +} + +.analysis-page.drawer-open .result-wrapper.analysis-grid { + width: min(920px, calc(100vw - 340px)); +} + +.analysis-page .drawer-panel { + width: 270px; + padding: 18px 14px 24px; +} + +@media (max-width: 1180px) { + .analysis-page.drawer-open .result-wrapper.analysis-grid { + width: min(860px, calc(100vw - 318px)); + gap: 18px; + } +} + +@media (max-width: 980px) { + .analysis-page.drawer-open .result-wrapper.analysis-grid, + .analysis-page .result-wrapper.analysis-grid { + width: min(720px, calc(100vw - 32px)); + } +} + + +/* Strict senior QA follow-up: keep Analysis balanced when the sample drawer is open at laptop height/zoom. */ +.analysis-page.home-container { + padding-top: clamp(14px, 2.4vh, 28px); + padding-bottom: clamp(28px, 4vh, 48px); +} + +.analysis-page .top-texts { + margin-bottom: clamp(12px, 2vh, 18px); +} + +.analysis-page.drawer-open .result-wrapper.analysis-grid { + width: min(900px, calc(100vw - 340px)); + gap: clamp(18px, 2vw, 26px); +} + +.analysis-page .drawer-panel { + width: min(280px, 22vw); + background: var(--tz-ui-surface-raised, #f8fafc); +} + +:root[data-theme="dark"] .analysis-page .drawer-panel { + background: #f8fafc; + color: #0f172a; +} + +.analysis-page .drawer-panel .sample-gallery h2 { + position: sticky; + top: 0; + z-index: 2; + padding: 4px 0 12px; + margin-bottom: 12px; + background: inherit; +} + +@media (max-height: 760px) and (min-width: 981px) { + .analysis-page .dropzone.big-drop, + .analysis-page .preview-image, + .analysis-page .description-wrapper .description-box { + height: clamp(240px, 38dvh, 360px); + min-height: clamp(240px, 38dvh, 360px); + max-height: clamp(240px, 38dvh, 360px); + } + + .analysis-page .drawer-panel .sample-gallery .sample-card { + height: 172px; + } +} + +/* Senior strict top-up: the Analysis sample drawer must keep the same dark, premium surface in both themes. */ +.analysis-page .drawer-panel, +:root[data-theme="light"] .analysis-page .drawer-panel, +:root[data-theme="dark"] .analysis-page .drawer-panel { + background: + radial-gradient(circle at 50% 0%, rgba(224, 168, 46, 0.10), transparent 34%), + linear-gradient(180deg, #0b1020 0%, #0a0f1e 48%, #070b16 100%); + color: #f8fafc; + border-left: 1px solid rgba(226, 232, 240, 0.18); + box-shadow: -24px 0 54px rgba(0, 0, 0, 0.38); +} + +.analysis-page .drawer-panel .sample-gallery, +:root[data-theme="light"] .analysis-page .drawer-panel .sample-gallery, +:root[data-theme="dark"] .analysis-page .drawer-panel .sample-gallery { + color: #f8fafc; +} + +.analysis-page .drawer-panel .sample-gallery h2, +:root[data-theme="light"] .analysis-page .drawer-panel .sample-gallery h2, +:root[data-theme="dark"] .analysis-page .drawer-panel .sample-gallery h2 { + color: #f8fafc; + background: + linear-gradient(180deg, rgba(11, 16, 32, 0.98), rgba(11, 16, 32, 0.88)); + text-shadow: 0 2px 14px rgba(0, 0, 0, 0.42); +} + +.analysis-page .drawer-panel .sample-gallery .sample-card, +:root[data-theme="light"] .analysis-page .drawer-panel .sample-gallery .sample-card, +:root[data-theme="dark"] .analysis-page .drawer-panel .sample-gallery .sample-card { + background: #111827; + border-color: rgba(226, 232, 240, 0.20); + box-shadow: 0 16px 34px rgba(0, 0, 0, 0.34); +} + +.analysis-page .drawer-panel .sample-gallery .sample-img, +:root[data-theme="light"] .analysis-page .drawer-panel .sample-gallery .sample-img, +:root[data-theme="dark"] .analysis-page .drawer-panel .sample-gallery .sample-img { + background: #0f172a; +} + +.analysis-page .drawer-panel::-webkit-scrollbar-thumb, +:root[data-theme="light"] .analysis-page .drawer-panel::-webkit-scrollbar-thumb, +:root[data-theme="dark"] .analysis-page .drawer-panel::-webkit-scrollbar-thumb { + background: rgba(226, 232, 240, 0.36); +} + +.analysis-page .drawer-panel::-webkit-scrollbar-track, +:root[data-theme="light"] .analysis-page .drawer-panel::-webkit-scrollbar-track, +:root[data-theme="dark"] .analysis-page .drawer-panel::-webkit-scrollbar-track { + background: rgba(15, 23, 42, 0.44); +} + +/* Final senior QA top-up: integrate the Analysis sample drawer with the page + instead of making it feel pasted on, and protect laptop/125% zoom layouts. */ +.analysis-page.drawer-open .result-wrapper.analysis-grid, +.analysis-page.drawer-open .result-wrapper.grid { + width: min(960px, calc(100vw - clamp(294px, 24vw, 336px))); + margin-left: clamp(18px, 3vw, 48px); + margin-right: auto; + gap: clamp(16px, 2vw, 24px); +} + +.analysis-page .drawer-panel, +:root[data-theme="light"] .analysis-page .drawer-panel, +:root[data-theme="dark"] .analysis-page .drawer-panel { + width: clamp(248px, 19vw, 272px); + padding: 12px 10px 18px; + background: + radial-gradient(circle at 50% 0%, rgba(224, 168, 46, 0.09), transparent 32%), + linear-gradient(180deg, #111827 0%, #0b1020 48%, #080d18 100%); + border-left: 1px solid rgba(226, 232, 240, 0.16); + box-shadow: -14px 0 34px rgba(15, 23, 42, 0.20); +} + +:root[data-theme="light"] .analysis-page .drawer-panel { + box-shadow: -12px 0 32px rgba(15, 23, 42, 0.18); +} + +.analysis-page .drawer-panel .sample-gallery h2, +:root[data-theme="light"] .analysis-page .drawer-panel .sample-gallery h2, +:root[data-theme="dark"] .analysis-page .drawer-panel .sample-gallery h2 { + margin: 0 0 12px; + padding: 12px 8px 14px; + border-radius: 0 0 16px 16px; + background: linear-gradient(180deg, rgba(15, 23, 42, 0.96), rgba(15, 23, 42, 0.86)); + color: #f8fafc; + font-size: clamp(19px, 1.8vw, 24px); + line-height: 1.15; + letter-spacing: -0.025em; + text-shadow: none; +} + +.analysis-page .drawer-panel .sample-gallery .sample-grid, +:root[data-theme="light"] .analysis-page .drawer-panel .sample-gallery .sample-grid, +:root[data-theme="dark"] .analysis-page .drawer-panel .sample-gallery .sample-grid { + gap: 10px; +} + +.analysis-page .drawer-panel .sample-gallery .sample-card, +:root[data-theme="light"] .analysis-page .drawer-panel .sample-gallery .sample-card, +:root[data-theme="dark"] .analysis-page .drawer-panel .sample-gallery .sample-card { + height: clamp(138px, 20dvh, 166px); + border-radius: 14px; + border-color: rgba(226, 232, 240, 0.18); + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.26); +} + +.analysis-page .drawer-panel .sample-gallery .sample-img { + border-radius: 14px; +} + +.analysis-page .drawer-panel .sample-gallery .sample-card__select { + bottom: 10px; + min-height: 32px; + min-width: 84px; + padding: 0 14px; + font-size: 12px; +} + +.analysis-page .dropzone.big-drop, +.analysis-page .description-wrapper .description-box, +.analysis-page .preview-image { + box-shadow: 0 18px 42px rgba(15, 23, 42, 0.14); +} + +:root[data-theme="light"] .analysis-page .dropzone.big-drop { + background: rgba(255, 255, 255, 0.82); + border-color: rgba(15, 23, 42, 0.10); + box-shadow: 0 18px 42px rgba(15, 23, 42, 0.10); +} + +.analysis-page .top-texts { + margin-bottom: clamp(12px, 1.8vh, 18px); +} + +@media (max-height: 760px) and (min-width: 981px) { + .analysis-page .drawer-panel .sample-gallery .sample-card { + height: 132px; + } + + .analysis-page .drawer-panel .sample-gallery h2 { + font-size: 19px; + padding-block: 10px; + } +} + +/* Final micro top-up: let the Analysis sample drawer follow the active theme. + Previous strict QA patches intentionally forced a dark drawer; this restores + normal light/dark behaviour while keeping both themes readable. */ +:root[data-theme="light"] .analysis-page .drawer-panel { + background: + radial-gradient(circle at 50% 0%, rgba(224, 168, 46, 0.10), transparent 34%), + linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(248, 250, 252, 0.98) 52%, rgba(239, 246, 255, 0.96) 100%); + color: #0f172a; + border-left: 1px solid rgba(15, 23, 42, 0.12); + box-shadow: -14px 0 34px rgba(15, 23, 42, 0.14); +} + +:root[data-theme="light"] .analysis-page .drawer-panel .sample-gallery, +:root[data-theme="light"] .analysis-page .drawer-panel .sample-gallery p, +:root[data-theme="light"] .analysis-page .drawer-panel .sample-gallery span { + color: #0f172a; +} + +:root[data-theme="light"] .analysis-page .drawer-panel .sample-gallery h2 { + background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(241, 245, 249, 0.88)); + color: #0f172a; + border: 1px solid rgba(15, 23, 42, 0.08); + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08); + text-shadow: none; +} + +:root[data-theme="light"] .analysis-page .drawer-panel .sample-gallery .sample-card { + background: rgba(255, 255, 255, 0.96); + border-color: rgba(15, 23, 42, 0.12); + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.12); +} + +:root[data-theme="light"] .analysis-page .drawer-panel .sample-gallery .sample-img { + background: #f8fafc; +} + +:root[data-theme="light"] .analysis-page .drawer-panel .sample-gallery .sample-card__select { + background: #0f172a; + color: #ffffff; + border-color: rgba(15, 23, 42, 0.18); + box-shadow: 0 10px 22px rgba(15, 23, 42, 0.18); +} + +:root[data-theme="light"] .analysis-page .drawer-panel::-webkit-scrollbar-thumb { + background: rgba(15, 23, 42, 0.28); +} + +:root[data-theme="light"] .analysis-page .drawer-panel::-webkit-scrollbar-track { + background: rgba(226, 232, 240, 0.78); +} + +:root[data-theme="dark"] .analysis-page .drawer-panel { + background: + radial-gradient(circle at 50% 0%, rgba(224, 168, 46, 0.09), transparent 32%), + linear-gradient(180deg, #111827 0%, #0b1020 48%, #080d18 100%); + color: #f8fafc; + border-left-color: rgba(226, 232, 240, 0.16); + box-shadow: -14px 0 34px rgba(15, 23, 42, 0.20); +} diff --git a/frontend/src/assets/styles/ImageDescriptorHeader.css b/frontend/src/assets/styles/ImageDescriptorHeader.css index d6279e0..a200a47 100644 --- a/frontend/src/assets/styles/ImageDescriptorHeader.css +++ b/frontend/src/assets/styles/ImageDescriptorHeader.css @@ -43,3 +43,19 @@ opacity: 1; } } + +/* QA follow-up: Analysis heading must be readable in both themes. */ +.analysis-page .animated-header h1 { + color: var(--tz-ui-text, #f8fafc); + border-bottom-color: var(--tz-turmeric, #e0a82e); + text-shadow: 0 2px 14px rgba(0, 0, 0, 0.24); +} + +:root[data-theme="light"] .analysis-page .animated-header h1 { + color: #0f172a; + text-shadow: none; +} + +:root[data-theme="dark"] .analysis-page .animated-header h1 { + color: #f8fafc; +} diff --git a/frontend/src/assets/styles/ImagePreviewPanel.css b/frontend/src/assets/styles/ImagePreviewPanel.css index aa52d50..37ad24b 100644 --- a/frontend/src/assets/styles/ImagePreviewPanel.css +++ b/frontend/src/assets/styles/ImagePreviewPanel.css @@ -321,3 +321,101 @@ :root[data-theme="light"] .analysis-page .analysis-btn.long { color: #ffffff ; } + +/* QA follow-up: Analysis action buttons align with Prev/Next and stay yellow in both themes. */ +.analysis-page .preview-buttons-container { + margin-top: 18px; + transform: none; + min-height: 46px; + display: flex; + align-items: center; + justify-content: center; + gap: 12px; +} + +.analysis-page .preview-buttons-container .analysis-btn, +.analysis-page .preview-buttons-container .analysis-btn.short, +.analysis-page .preview-buttons-container .analysis-btn.long { + min-height: 44px; + min-width: 150px; + border-radius: 999px; + border: 1px solid var(--tz-turmeric, #e0a82e); + background: linear-gradient(135deg, var(--tz-turmeric, #e0a82e), var(--tz-turmeric-soft, #f59e0b)); + color: #ffffff; + font-weight: 850; + box-shadow: 0 10px 24px rgba(224, 168, 46, 0.24); +} + +:root[data-theme="dark"] .analysis-page .preview-buttons-container .analysis-btn, +:root[data-theme="dark"] .analysis-page .preview-buttons-container .analysis-btn.short, +:root[data-theme="dark"] .analysis-page .preview-buttons-container .analysis-btn.long { + color: #171101; +} + +/* Senior QA final: Short/Long controls align visually with Prev/Next and keep one button language. */ +.analysis-page .preview-buttons-container { + min-height: 46px; + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + margin-top: 18px; + transform: translateY(2px); +} + +.analysis-page .analysis-btn, +.analysis-page .analysis-btn.short, +.analysis-page .analysis-btn.long { + min-height: 44px; + min-width: 142px; + padding: 0 18px; + border-radius: 999px; + border: 1px solid var(--tz-action-yellow, #e0a82e); + background: linear-gradient(135deg, var(--tz-action-yellow, #e0a82e), var(--tz-action-yellow-hover, #f0c463)); + color: var(--tz-action-yellow-text, #171101); + font-weight: 900; + box-shadow: 0 10px 24px rgba(224, 168, 46, 0.24); +} + +.analysis-page .analysis-btn:not(.disabled):hover { + transform: translateY(-1px); + box-shadow: 0 14px 30px rgba(224, 168, 46, 0.30); +} + +.analysis-page .analysis-btn.disabled, +.analysis-page .analysis-btn:disabled { + opacity: 0.68; + color: rgba(23, 17, 1, 0.72); + box-shadow: none; +} + +@media (max-width: 720px) { + .analysis-page .preview-buttons-container { + flex-wrap: wrap; + } +} + +/* Senior QA top-up: Short/Long buttons stay readable and vertically synced with Prev/Next. */ +.analysis-page .preview-buttons-container { + margin-top: 18px; + min-height: 46px; +} + +.analysis-page .analysis-btn, +.analysis-page .analysis-btn.short, +.analysis-page .analysis-btn.long { + color: var(--tz-action-yellow-text, #171101); +} + +:root[data-theme="light"] .analysis-page .analysis-btn, +:root[data-theme="light"] .analysis-page .analysis-btn.short, +:root[data-theme="light"] .analysis-page .analysis-btn.long { + color: #171101; + background: linear-gradient(135deg, #e0a82e, #d9900d); +} + +:root[data-theme="dark"] .analysis-page .analysis-btn, +:root[data-theme="dark"] .analysis-page .analysis-btn.short, +:root[data-theme="dark"] .analysis-page .analysis-btn.long { + color: #171101; +} diff --git a/frontend/src/assets/styles/Messages.css b/frontend/src/assets/styles/Messages.css index 787e851..d755332 100644 --- a/frontend/src/assets/styles/Messages.css +++ b/frontend/src/assets/styles/Messages.css @@ -650,3 +650,46 @@ :root[data-theme="light"] .message-wrapper .quick-replies-prompt { color: #334155; } + +/* QA follow-up: keep "Want to know more?" visible in dark theme. */ +:root[data-theme="dark"] .message-wrapper .inline-quick-replies { + background: rgba(15, 23, 42, 0.84); + border: 1px solid rgba(226, 232, 240, 0.18); +} + +:root[data-theme="dark"] .message-wrapper .quick-replies-prompt { + color: #f8fafc; + opacity: 1; + text-shadow: none; +} + +/* Senior QA final: inline quick reply prompt and buttons must be readable, not decorative noise. */ +.message-wrapper .inline-quick-replies { + background: var(--tz-ui-surface-raised, rgba(15, 23, 42, 0.88)); + border: 1px solid var(--tz-ui-border, rgba(226, 232, 240, 0.20)); + box-shadow: 0 12px 26px rgba(15, 23, 42, 0.16); +} + +.message-wrapper .quick-replies-prompt { + color: var(--tz-ui-text, #f8fafc); + opacity: 1; +} + +.message-wrapper .inline-quick-replies .chip { + min-height: 34px; + border-radius: 999px; + font-weight: 850; + color: var(--tz-action-yellow-text, #171101); + background: linear-gradient(135deg, var(--tz-action-yellow, #e0a82e), var(--tz-action-yellow-hover, #f0c463)); + border: 1px solid rgba(180, 83, 9, 0.30); +} + +:root[data-theme="light"] .message-wrapper .inline-quick-replies { + background: #ffffff; + border-color: rgba(15, 23, 42, 0.14); + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08); +} + +:root[data-theme="light"] .message-wrapper .quick-replies-prompt { + color: #0f172a; +} diff --git a/frontend/src/assets/styles/SampleImageGalleryCard.css b/frontend/src/assets/styles/SampleImageGalleryCard.css index f6f41fa..06442d5 100644 --- a/frontend/src/assets/styles/SampleImageGalleryCard.css +++ b/frontend/src/assets/styles/SampleImageGalleryCard.css @@ -245,3 +245,32 @@ color: #171101 ; border-color: #f0c463 ; } + +/* QA follow-up: keep the sample drawer title visible in dark theme. */ +.analysis-page .sample-gallery h2, +:root[data-theme="dark"] .analysis-page .sample-gallery h2 { + color: var(--tz-ui-text, #f8fafc); + text-shadow: 0 2px 12px rgba(0, 0, 0, 0.34); +} + +:root[data-theme="light"] .analysis-page .sample-gallery h2 { + color: #0f172a; + text-shadow: none; +} + +/* Senior QA final: sample drawer title and cards remain visible and balanced in dark mode. */ +.analysis-page .sample-gallery h2 { + color: var(--tz-ui-text, #f8fafc); + font-size: clamp(20px, 2vw, 24px); + font-weight: 900; + letter-spacing: -0.02em; +} + +:root[data-theme="light"] .analysis-page .sample-gallery h2 { + color: #0f172a; +} + +.analysis-page .sample-gallery .sample-card { + border: 1px solid var(--tz-ui-border, rgba(226, 232, 240, 0.20)); + background: var(--tz-ui-surface-raised, rgba(15, 23, 42, 0.92)); +} diff --git a/frontend/src/assets/styles/SuggestionChips.css b/frontend/src/assets/styles/SuggestionChips.css index 2b3e67d..54a9b5f 100644 --- a/frontend/src/assets/styles/SuggestionChips.css +++ b/frontend/src/assets/styles/SuggestionChips.css @@ -82,3 +82,57 @@ color: #f8fafc ; background: rgba(30, 41, 59, 0.96) ; } + +/* Senior QA top-up: desktop prompt chips must stay aligned and readable at 100-125% zoom. */ +.page-root.is-empty .empty-state-large .sample-prompts, +.page-root.is-empty .prompt-buttons, +.page-root.is-empty .quick-question-row, +.page-root.is-empty .empty-state-actions { + width: min(1120px, calc(100vw - 56px)); + margin-inline: auto; +} + +.page-root.is-empty .empty-state-large .chip, +.page-root.is-empty .sample-prompts .chip, +.page-root.is-empty .prompt-buttons button, +.page-root.is-empty .quick-question-row button { + min-height: 54px; + display: inline-flex; + align-items: center; + justify-content: center; + white-space: nowrap; + line-height: 1.15; +} + +@media (min-width: 980px) { + .page-root.is-empty .empty-state-large .sample-prompts, + .page-root.is-empty .prompt-buttons, + .page-root.is-empty .quick-question-row, + .page-root.is-empty .empty-state-actions { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 14px; + } + + .page-root.is-empty .empty-state-large .chip, + .page-root.is-empty .sample-prompts .chip, + .page-root.is-empty .prompt-buttons button, + .page-root.is-empty .quick-question-row button { + width: 100%; + max-width: none; + padding-inline: 14px; + font-size: clamp(13px, 0.95vw, 16px); + } +} + +@media (max-width: 979px) { + .page-root.is-empty .empty-state-large .sample-prompts, + .page-root.is-empty .prompt-buttons, + .page-root.is-empty .quick-question-row, + .page-root.is-empty .empty-state-actions { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + } +} diff --git a/frontend/src/assets/styles/_makeover.css b/frontend/src/assets/styles/_makeover.css index b4b7ec2..364ac91 100644 --- a/frontend/src/assets/styles/_makeover.css +++ b/frontend/src/assets/styles/_makeover.css @@ -2319,3 +2319,53 @@ footer a:hover, .lb-caption { color: var(--tz-ui-text-muted) ; } + +/* ============================================================ + Senior QA visual-system hardening: shared tokens and states + ============================================================ */ +:root { + --tz-action-yellow: #e0a82e; + --tz-action-yellow-hover: #f0c463; + --tz-action-yellow-text: #171101; + --tz-danger: #dc2626; + --tz-danger-hover: #b91c1c; + --tz-danger-text: #ffffff; + --tz-focus: 0 0 0 3px var(--tz-ui-focus-ring, rgba(224, 168, 46, 0.34)); + --tz-control-height: 44px; + --tz-control-radius: 999px; +} + +button:focus-visible, +a:focus-visible, +[role="button"]:focus-visible, +input:focus-visible, +textarea:focus-visible, +select:focus-visible { + outline: 2px solid var(--tz-ui-accent, var(--tz-action-yellow)); + outline-offset: 3px; + box-shadow: var(--tz-focus); +} + +button, +[role="button"] { + -webkit-tap-highlight-color: transparent; +} + +.btn, +button { + text-shadow: none; +} + +.btn:disabled, +button:disabled { + opacity: 0.68; + cursor: not-allowed; +} + +.site-header { + min-height: 64px; +} + +.main-content { + min-height: calc(100dvh - 64px); +} diff --git a/frontend/src/components/Composer.tsx b/frontend/src/components/Composer.tsx index 7c61014..a8f3fef 100644 --- a/frontend/src/components/Composer.tsx +++ b/frontend/src/components/Composer.tsx @@ -533,19 +533,19 @@ export default function Composer({ padding: 16, }} > -
-
-
- Confirm preview +
+
+
+ Confirm preview
-
-
Upload this image?
-
{pendingImage.file.name} — {formatSize(pendingImage.file.size)}
-
- -
@@ -571,22 +571,22 @@ export default function Composer({ padding: 16, }} > -
-
-
-
-
-
Upload this audio?
-
{pendingAudio.file.name} — {formatSize(pendingAudio.file.size)}
-
+
+
+
+
Upload this audio?
+
{pendingAudio.file.name} — {formatSize(pendingAudio.file.size)}
+
+
+
If longer than 1 minute we will trim it automatically.
-
- -
diff --git a/frontend/src/components/DrawerToggle.tsx b/frontend/src/components/DrawerToggle.tsx index 2200417..c4a6047 100644 --- a/frontend/src/components/DrawerToggle.tsx +++ b/frontend/src/components/DrawerToggle.tsx @@ -4,7 +4,7 @@ const DrawerToggle = ({ showDrawer, setShowDrawer }) => { return (
); })} - {compact && ( - )}
diff --git a/frontend/src/components/search/Lightbox.tsx b/frontend/src/components/search/Lightbox.tsx index a9c76a8..0bb6d9f 100644 --- a/frontend/src/components/search/Lightbox.tsx +++ b/frontend/src/components/search/Lightbox.tsx @@ -20,6 +20,9 @@ export default function Lightbox({ onClose, onWheel, onMouseDown, onMouseMove, onMouseUp, onZoomIn, onZoomOut, onReset, }: LightboxProps) { + const canZoomOut = scale > 0.51; + const canZoomIn = scale < 5.99; + return (
− @@ -66,6 +71,7 @@ export default function Lightbox({ onClick={onReset} type="button" aria-label="Reset zoom" + title="Reset zoom" > Reset @@ -75,6 +81,8 @@ export default function Lightbox({ onClick={onZoomIn} type="button" aria-label="Zoom in" + title="Zoom in" + disabled={!canZoomIn} > + @@ -83,6 +91,7 @@ export default function Lightbox({ onClick={onClose} type="button" aria-label="Close preview" + title="Close preview" > ✕ diff --git a/frontend/src/components/search/searchUtils.ts b/frontend/src/components/search/searchUtils.ts index f14b9eb..ba82e7b 100644 --- a/frontend/src/components/search/searchUtils.ts +++ b/frontend/src/components/search/searchUtils.ts @@ -47,15 +47,36 @@ export function toResultItem(raw: string): ResultItem { } export function cleanName(filename: string): string { - if (!filename) return ""; + if (!filename) return "Fabric sample"; - const withoutExtension = filename.replace(/\.[^.]+$/, ""); - const readable = withoutExtension + const raw = decodeURIComponent(String(filename).split(/[?#]/)[0].split("/").pop() ?? filename).trim(); + const withoutExtension = raw.replace(/\.[^.]+$/, ""); + + const normalized = withoutExtension + // Drop backend storage timestamps / IDs, but keep the human filename prefix. + .replace(/(?:[_\s-]?20\d{6}T\d{6})(?:[_\s-]?[a-z0-9]{4,})?$/i, "") + .replace(/(?:[_\s-]?20\d{6})[_\s-]?\d{6}.*$/i, "") + .replace(/[_\s-][a-f0-9]{5,}$/i, "") + .replace(/\b\d{10,}\b/g, "") + .replace(/^[a-f0-9]{6,}\s+/i, "") + .replace(/^\d+(?:[\s_-]+\d+){1,}\s*/i, "") + .replace(/[_-](?:copy|final|submitted)$/i, "") .replace(/[_-]+/g, " ") .replace(/\s+/g, " ") .trim(); - return readable || withoutExtension || filename; + const letters = (normalized.match(/[a-z]/gi) ?? []).length; + const digits = (normalized.match(/\d/g) ?? []).length; + const looksTechnical = + !letters || + digits > letters || + /\b(?:single image|image|img|upload|submitted files?)\b/i.test(normalized) || + /^[a-f0-9]{6,}\b/i.test(normalized); + + if (looksTechnical) return "Fabric sample"; + + const shortName = normalized.split(" ").slice(0, 3).join(" "); + return shortName.replace(/\b\w/g, (char) => char.toUpperCase()) || "Fabric sample"; } export async function callDbEndpoint(op: "create" | "update"): Promise { diff --git a/frontend/src/pages/FabricList.tsx b/frontend/src/pages/FabricList.tsx index a1924a1..ae2945c 100644 --- a/frontend/src/pages/FabricList.tsx +++ b/frontend/src/pages/FabricList.tsx @@ -51,6 +51,19 @@ export default function ContentGrid() { const MAX_SCALE = 6; const ZOOM_STEP = 0.2; + const clampLightboxOffset = (next: { x: number; y: number }) => { + const viewportWidth = typeof window === "undefined" ? 1200 : window.innerWidth; + const viewportHeight = typeof window === "undefined" ? 800 : window.innerHeight; + const scaleAllowance = Math.max(1, scale); + const maxX = Math.round(Math.min(viewportWidth * 0.42, 460 * scaleAllowance)); + const maxY = Math.round(Math.min(viewportHeight * 0.42, 360 * scaleAllowance)); + + return { + x: Math.max(-maxX, Math.min(maxX, next.x)), + y: Math.max(-maxY, Math.min(maxY, next.y)), + }; + }; + // Add page background useEffect(() => { const wrapper = document.querySelector(".app-wrapper"); @@ -94,20 +107,49 @@ export default function ContentGrid() { }; function pickDisplayName(item: MediaItem) { - if (item.basename) return item.basename; - if (item.imageFilename) - return item.imageFilename.replace(/\.[^.]+$/, ""); - const last = (item.imageUrl || "").split("/").pop() || ""; - return last.replace(/\.[^.]+$/, ""); + const fromImageFilename = item.imageFilename?.trim(); + const fromUrl = item.imageUrl + ? decodeURIComponent((item.imageUrl.split(/[?#]/)[0].split("/").pop() || "").trim()) + : ""; + const fromBasename = item.basename?.trim(); + + // Prefer the actual uploaded/stored filename. Basename can be a generated or + // user-facing title and was creating very long card captions on the List page. + return fromImageFilename || fromUrl || fromBasename || "Uploaded fabric"; } const cleanName = (filename: string) => { - if (!filename) return ""; - return filename - .replace(/\.[^.]+$/, "") + const raw = String(filename || "").trim(); + if (!raw) return "Uploaded fabric"; + + const withoutQuery = raw.split(/[?#]/)[0]; + const lastSegment = decodeURIComponent(withoutQuery.split("/").pop() || withoutQuery); + const withoutExtension = lastSegment.replace(/\.[^.]+$/, ""); + + const normalized = withoutExtension + .replace(/(?:[_\s-]?20\d{6}T\d{6})(?:[_\s-]?[a-z0-9]{4,})?$/i, "") + .replace(/(?:[_\s-]?20\d{6})[_\s-]?\d{6}.*$/i, "") + .replace(/[_\s-][a-f0-9]{5,}$/i, "") + .replace(/\b\d{10,}\b/g, "") + .replace(/^[a-f0-9]{6,}\s+/i, "") + .replace(/^\d+(?:[\s_-]+\d+){1,}\s*/i, "") + .replace(/[_-](?:copy|final|submitted)$/i, "") .replace(/[_-]+/g, " ") .replace(/\s+/g, " ") .trim(); + + const letters = (normalized.match(/[a-z]/gi) ?? []).length; + const digits = (normalized.match(/\d/g) ?? []).length; + const looksTechnical = + !letters || + digits > letters || + /\b(?:single image|image|img|upload|submitted files?)\b/i.test(normalized) || + /^[a-f0-9]{6,}\b/i.test(normalized); + + if (looksTechnical) return "Fabric sample"; + + const shortName = normalized.split(" ").slice(0, 3).join(" "); + return shortName.replace(/\b\w/g, (char) => char.toUpperCase()) || "Uploaded fabric"; }; // ✅ Hide items with missing/broken images @@ -166,6 +208,7 @@ export default function ContentGrid() { }; const onMouseDown: React.MouseEventHandler = (e) => { + if ((e.target as HTMLElement).closest(".lb-controls")) return; draggingRef.current = true; lastPosRef.current = { x: e.clientX, y: e.clientY }; }; @@ -174,7 +217,7 @@ export default function ContentGrid() { const dx = e.clientX - lastPosRef.current.x; const dy = e.clientY - lastPosRef.current.y; lastPosRef.current = { x: e.clientX, y: e.clientY }; - setOffset((o) => ({ x: o.x + dx, y: o.y + dy })); + setOffset((o) => clampLightboxOffset({ x: o.x + dx, y: o.y + dy })); }; const onMouseUpOrLeave = () => { draggingRef.current = false; @@ -247,7 +290,8 @@ export default function ContentGrid() { const rawSrc = item.imageUrl; const src = rawSrc?.startsWith("http") ? rawSrc : `${BASE_URL}${rawSrc}`; - const caption = cleanName(pickDisplayName(item)); + const rawDisplayName = pickDisplayName(item); + const caption = cleanName(rawDisplayName); return (
@@ -274,7 +318,7 @@ export default function ContentGrid() {
openLightbox(src, caption) } > { caption } @@ -345,9 +389,9 @@ export default function ContentGrid() { ) }
- - - + + + diff --git a/frontend/src/pages/FabricSearch.tsx b/frontend/src/pages/FabricSearch.tsx index d945625..f4cd186 100644 --- a/frontend/src/pages/FabricSearch.tsx +++ b/frontend/src/pages/FabricSearch.tsx @@ -54,6 +54,19 @@ export default function Search() { const lastLbPosRef = useRef({ x: 0, y: 0 }); const MIN_SCALE = 0.5, MAX_SCALE = 6, ZOOM_STEP = 0.2; + const clampLightboxOffset = useCallback((next: { x: number; y: number }) => { + const viewportWidth = typeof window === "undefined" ? 1200 : window.innerWidth; + const viewportHeight = typeof window === "undefined" ? 800 : window.innerHeight; + const scaleAllowance = Math.max(1, lbScale); + const maxX = Math.round(Math.min(viewportWidth * 0.42, 460 * scaleAllowance)); + const maxY = Math.round(Math.min(viewportHeight * 0.42, 360 * scaleAllowance)); + + return { + x: Math.max(-maxX, Math.min(maxX, next.x)), + y: Math.max(-maxY, Math.min(maxY, next.y)), + }; + }, [lbScale]); + // Misc const [previewUrl, setPreviewUrl] = useState(null); const heroFileId = useId(); @@ -301,7 +314,7 @@ export default function Search() { const dx = e.clientX - lastLbPosRef.current.x; const dy = e.clientY - lastLbPosRef.current.y; lastLbPosRef.current = { x: e.clientX, y: e.clientY }; - setLbOffset((o) => ({ x: o.x + dx, y: o.y + dy })); + setLbOffset((o) => clampLightboxOffset({ x: o.x + dx, y: o.y + dy })); }; const onLbMouseUp = () => { draggingLbRef.current = false; }; @@ -533,8 +546,12 @@ export default function Search() { /> )} - {!loading && file && visibleResults.length === 0 && !error && ( -

— no matches found —

+ {!loading && (file || isTextSearch) && visibleResults.length === 0 && !error && ( +

+ {isTextSearch && textQuery.trim() + ? `No results found for “${textQuery.trim()}”. Try another keyword or category.` + : "No matching fabrics found. Try changing crop, category, or result limit."} +

)}
From 5b7a7296ab5fe4002cb540c08d3989091d400fdb Mon Sep 17 00:00:00 2001 From: DhwaniKhandelwal-tech Date: Tue, 14 Jul 2026 20:19:07 +0530 Subject: [PATCH 04/25] fix(frontend): stabilize analysis, chat, search, and upload-time handling ; Reposition analysis controls, improve chat UI and attachment dialogs, handle transient first-message failures, normalize upload timestamps, harden frontend API behavior, and add regression tests and quality checks. --- frontend/.env.sample | 3 +- frontend/README.md | 34 +- frontend/biome.json | 17 +- frontend/index.html | 1 + frontend/package-lock.json | 710 +++++++++++++++--- frontend/package.json | 20 +- frontend/src/App.css | 77 +- frontend/src/App.tsx | 47 +- frontend/src/Routing.tsx | 14 +- frontend/src/assets/styles/DescriptionBox.css | 13 + .../src/assets/styles/ImagePreviewPanel.css | 8 + frontend/src/assets/styles/SeniorUiFixes.css | 358 +++++++++ frontend/src/assets/styles/navbar.css | 259 ++++--- frontend/src/components/AppErrorBoundary.tsx | 61 ++ frontend/src/components/Composer.tsx | 54 +- frontend/src/components/DescriptionBox.tsx | 14 +- frontend/src/components/DrawerToggle.tsx | 8 +- frontend/src/components/Footer.tsx | 73 +- frontend/src/components/ImagePreviewPanel.tsx | 123 +-- frontend/src/components/MessageList.tsx | 19 +- frontend/src/components/NavBar.tsx | 84 ++- .../src/components/SampleImageGalleryCard.tsx | 7 +- .../src/components/search/searchUtils.test.ts | 51 ++ frontend/src/components/search/searchUtils.ts | 105 ++- frontend/src/components/search/types.ts | 23 +- frontend/src/components/search/useSearch.ts | 176 +++-- frontend/src/hooks/chat.ts | 110 +-- frontend/src/hooks/useImageAnalysis.ts | 5 +- frontend/src/hooks/usePageTracking.ts | 43 +- frontend/src/hooks/useRouteMetadata.ts | 99 +++ frontend/src/hooks/useUploadAndRecord.ts | 13 +- frontend/src/main.tsx | 14 +- frontend/src/pages/AadhaarCardReader.tsx | 101 ++- frontend/src/pages/Contact.tsx | 192 +++-- frontend/src/pages/FabricChat.tsx | 165 ++-- frontend/src/pages/FabricList.tsx | 220 +++--- frontend/src/pages/FabricSearch.tsx | 459 ++++++++--- frontend/src/pages/ImageDescriptor.tsx | 2 +- frontend/src/pages/PanCardReader.tsx | 93 ++- frontend/src/pages/Reader.tsx | 27 +- frontend/src/services/analyze_api.ts | 181 +++-- frontend/src/services/chat_api.ts | 292 +++++-- frontend/src/services/content_api.test.ts | 75 ++ frontend/src/services/content_api.ts | 119 ++- frontend/src/services/search_api.ts | 150 ++-- frontend/src/utils/dateTime.test.ts | 39 + frontend/src/utils/dateTime.ts | 65 ++ frontend/src/utils/http.test.ts | 85 +++ frontend/src/utils/http.ts | 140 ++++ frontend/src/utils/image-helper.ts | 30 +- frontend/tsconfig.app.json | 2 +- frontend/vite.config.ts | 7 +- 52 files changed, 3858 insertions(+), 1229 deletions(-) create mode 100644 frontend/src/assets/styles/SeniorUiFixes.css create mode 100644 frontend/src/components/AppErrorBoundary.tsx create mode 100644 frontend/src/components/search/searchUtils.test.ts create mode 100644 frontend/src/hooks/useRouteMetadata.ts create mode 100644 frontend/src/services/content_api.test.ts create mode 100644 frontend/src/utils/dateTime.test.ts create mode 100644 frontend/src/utils/dateTime.ts create mode 100644 frontend/src/utils/http.test.ts create mode 100644 frontend/src/utils/http.ts diff --git a/frontend/.env.sample b/frontend/.env.sample index d994df4..e1e9b82 100644 --- a/frontend/.env.sample +++ b/frontend/.env.sample @@ -1,4 +1,5 @@ VITE_API_URL="http://localhost:8002" VITE_API_PREFIX="/api/v1" -GROQ_API_KEY="your_groq_key_here" VITE_AWS_PUBLIC_URL="https://assets.threadzip.com" +VITE_ANALYTICS_ENABLED="false" +VITE_GA_MEASUREMENT_ID="" diff --git a/frontend/README.md b/frontend/README.md index e900a6a..216519b 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,12 +1,36 @@ -# TZ-Fabric +# TZ-Fabric frontend -![Node Version](https://badgen.net/badge/Node/20+/blue) +## Requirements + +- Node.js `^20.19.0` or `>=22.12.0` +- npm 10 or newer + +## Development ```sh -npm install +npm ci npm run dev ``` -Note: create `.env` file and put content from `.env.sample` file +Copy `.env.sample` to `.env` and configure the documented `VITE_` variables. +The development proxy target can be changed with `VITE_API_PROXY_TARGET`. + +## Quality checks + +```sh +npm run lint +npm test +npm run build +``` + +## Production preview process + +Build first, then serve the generated `dist` directory on port 5173: + +```sh +npm run build +npm start +``` -NOTE: the `VITE_API_URL` and it should be same as _vite.config.ts_ +`npm start` is intended for the repository's existing PM2 deployment workflow. +A dedicated static web server or CDN is preferable for a larger production setup. diff --git a/frontend/biome.json b/frontend/biome.json index 893a4ff..0c9e480 100644 --- a/frontend/biome.json +++ b/frontend/biome.json @@ -25,7 +25,20 @@ "enabled": true, "rules": { "recommended": true, - "a11y": "off", + "a11y": { + "recommended": true, + "useButtonType": "off", + "noSvgWithoutTitle": "off", + "useSemanticElements": "off", + "useKeyWithClickEvents": "off", + "noStaticElementInteractions": "off", + "useMediaCaption": "off", + "useFocusableInteractive": "off", + "noLabelWithoutControl": "off", + "noRedundantAlt": "off", + "noNoninteractiveElementToInteractiveRole": "off", + "noInteractiveElementToNoninteractiveRole": "off" + }, "suspicious": { "noArrayIndexKey": "off", "noExplicitAny": "warn" @@ -53,4 +66,4 @@ } } } -} \ No newline at end of file +} diff --git a/frontend/index.html b/frontend/index.html index 29be038..ab560f1 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,6 +5,7 @@ + Fabric AI diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 26f34a6..ebd7a22 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,7 +9,7 @@ "version": "1.5.1", "license": "MIT", "dependencies": { - "axios": "^1.11.0", + "axios": "^1.18.1", "dotenv-expand": "^12.0.3", "html-to-image": "^1.11.13", "jspdf": "^4.2.1", @@ -19,7 +19,7 @@ "react-ga4": "^3.0.1", "react-icons": "^5.5.0", "react-markdown": "^10.1.0", - "react-router-dom": "^7.7.1", + "react-router-dom": "^7.18.1", "remark-gfm": "^4.0.1" }, "devDependencies": { @@ -35,7 +35,8 @@ "globals": "^16.3.0", "typescript": "~5.8.3", "typescript-eslint": "^8.35.1", - "vite": "^7.0.4" + "vite": "^7.3.6", + "vitest": "^4.1.10" }, "funding": { "type": "individual", @@ -43,11 +44,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", + "version": "7.29.7", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -56,27 +58,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", + "version": "7.29.7", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.28.5", + "version": "7.29.7", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -93,12 +98,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", + "version": "7.29.7", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -108,12 +115,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", + "version": "7.29.7", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -123,7 +131,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", + "version": "7.29.7", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -131,25 +141,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", + "version": "7.29.7", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", + "version": "7.29.7", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -167,7 +181,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", + "version": "7.29.7", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", "dev": true, "license": "MIT", "engines": { @@ -175,7 +191,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", + "version": "7.29.7", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -183,31 +201,35 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", + "version": "7.29.7", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, - "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.28.4", + "version": "7.29.7", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.5", + "version": "7.29.7", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", "dev": true, - "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.7" }, "bin": { "parser": "bin/babel-parser.js" @@ -254,29 +276,33 @@ } }, "node_modules/@babel/template": { - "version": "7.27.2", + "version": "7.29.7", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", + "version": "7.29.7", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", "debug": "^4.3.1" }, "engines": { @@ -284,12 +310,14 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", + "version": "7.29.7", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -1472,6 +1500,12 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "dev": true, @@ -1509,6 +1543,17 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "license": "MIT", @@ -1516,6 +1561,12 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true + }, "node_modules/@types/estree": { "version": "1.0.8", "license": "MIT" @@ -1762,7 +1813,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", + "version": "2.1.2", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -1857,6 +1910,119 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.15.0", "dev": true, @@ -1876,6 +2042,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/ajv": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", @@ -1912,19 +2090,29 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/asynckit": { "version": "0.4.0", "license": "MIT" }, "node_modules/axios": { - "version": "1.13.6", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", - "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", - "license": "MIT", + "version": "1.18.1", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", "dependencies": { - "follow-redirects": "^1.15.11", + "follow-redirects": "^1.16.0", "form-data": "^4.0.5", - "proxy-from-env": "^1.1.0" + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" } }, "node_modules/bail": { @@ -1949,15 +2137,22 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.9.10", + "version": "2.10.43", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/brace-expansion": { - "version": "1.1.12", + "version": "1.1.16", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -1966,7 +2161,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", + "version": "4.28.6", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", "dev": true, "funding": [ { @@ -1984,11 +2181,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -2017,7 +2214,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001760", + "version": "1.0.30001805", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz", + "integrity": "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==", "dev": true, "funding": [ { @@ -2032,8 +2231,7 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ], - "license": "CC-BY-4.0" + ] }, "node_modules/canvg": { "version": "3.0.11", @@ -2061,6 +2259,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "dev": true, @@ -2154,7 +2362,7 @@ }, "node_modules/cookie": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/cookie/-/cookie-1.1.1.tgz", "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "license": "MIT", "engines": { @@ -2257,10 +2465,9 @@ } }, "node_modules/dompurify": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.3.tgz", - "integrity": "sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==", - "license": "(MPL-2.0 OR Apache-2.0)", + "version": "3.4.12", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/dompurify/-/dompurify-3.4.12.tgz", + "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", "optional": true, "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -2302,7 +2509,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.267", + "version": "1.5.389", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/electron-to-chromium/-/electron-to-chromium-1.5.389.tgz", + "integrity": "sha512-cEto7aeOqBfU1D+c5py5pE+ooscKE75JifxLBdFUZsqAxRS6y7kebtxAZvICszSl05gPjYHDTjY+lXpyGvpJbg==", "dev": true, "license": "ISC" }, @@ -2320,6 +2529,12 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.1", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/es-module-lexer/-/es-module-lexer-2.3.1.tgz", + "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", + "dev": true + }, "node_modules/es-object-atoms": { "version": "1.1.1", "license": "MIT", @@ -2385,6 +2600,8 @@ }, "node_modules/escalade": { "version": "3.2.0", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, "license": "MIT", "engines": { @@ -2559,6 +2776,16 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "dev": true, @@ -2567,6 +2794,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/extend": { "version": "3.0.2", "license": "MIT" @@ -2654,14 +2891,15 @@ } }, "node_modules/flatted": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", - "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", - "dev": true, - "license": "ISC" + "version": "3.4.2", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.11", + "version": "1.16.0", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -2679,14 +2917,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", + "version": "4.0.6", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -2819,7 +3059,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", + "version": "2.0.4", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -2888,6 +3130,19 @@ "node": ">=8.0.0" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ignore": { "version": "5.3.2", "dev": true, @@ -2999,13 +3254,25 @@ }, "node_modules/js-tokens": { "version": "4.0.0", - "dev": true, - "license": "MIT" + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true }, "node_modules/js-yaml": { - "version": "4.1.1", + "version": "4.3.0", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, - "license": "MIT", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "dependencies": { "argparse": "^2.0.1" }, @@ -3015,6 +3282,8 @@ }, "node_modules/jsesc": { "version": "3.1.0", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "license": "MIT", "bin": { @@ -3116,12 +3385,23 @@ }, "node_modules/lru-cache": { "version": "5.1.1", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, "license": "ISC", "dependencies": { "yallist": "^3.0.2" } }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/markdown-table": { "version": "3.0.4", "license": "MIT", @@ -3929,7 +4209,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", + "version": "3.3.16", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "dev": true, "funding": [ { @@ -3951,14 +4233,33 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.27", + "version": "2.0.51", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/normalize-wheel": { "version": "1.0.1", "license": "BSD-3-Clause" }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/optionator": { "version": "0.9.4", "dev": true, @@ -4055,6 +4356,13 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/performance-now": { "version": "2.1.0", "license": "MIT", @@ -4066,9 +4374,10 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", + "version": "4.0.5", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -4077,7 +4386,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", + "version": "8.5.19", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/postcss/-/postcss-8.5.19.tgz", + "integrity": "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ==", "dev": true, "funding": [ { @@ -4095,7 +4406,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -4120,8 +4431,13 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "license": "MIT" + "version": "2.1.0", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/punycode": { "version": "2.3.1", @@ -4215,10 +4531,9 @@ } }, "node_modules/react-router": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.1.tgz", - "integrity": "sha512-td+xP4X2/6BJvZoX6xw++A2DdEi++YypA69bJUV5oVvqf6/9/9nNlD70YO1e9d3MyamJEBQFEzk6mbfDYbqrSA==", - "license": "MIT", + "version": "7.18.1", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/react-router/-/react-router-7.18.1.tgz", + "integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==", "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" @@ -4237,12 +4552,11 @@ } }, "node_modules/react-router-dom": { - "version": "7.13.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.1.tgz", - "integrity": "sha512-UJnV3Rxc5TgUPJt2KJpo1Jpy0OKQr0AjgbZzBFjaPJcFOb2Y8jA5H3LT8HUJAiRLlWrEXWHbF1Z4SCZaQjWDHw==", - "license": "MIT", + "version": "7.18.1", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/react-router-dom/-/react-router-dom-7.18.1.tgz", + "integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==", "dependencies": { - "react-router": "7.13.1" + "react-router": "7.18.1" }, "engines": { "node": ">=20.0.0" @@ -4382,15 +4696,16 @@ }, "node_modules/semver": { "version": "6.3.1", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/set-cookie-parser": { "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, @@ -4413,6 +4728,13 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "dev": true, @@ -4429,6 +4751,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true + }, "node_modules/stackblur-canvas": { "version": "2.7.0", "license": "MIT", @@ -4437,6 +4765,12 @@ "node": ">=0.1.14" } }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true + }, "node_modules/stringify-entities": { "version": "4.0.4", "license": "MIT", @@ -4501,6 +4835,22 @@ "utrie": "^1.0.2" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "dev": true, @@ -4516,6 +4866,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "license": "MIT", @@ -4674,6 +5034,8 @@ }, "node_modules/update-browserslist-db": { "version": "1.2.3", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -4689,7 +5051,6 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" @@ -4742,11 +5103,13 @@ } }, "node_modules/vite": { - "version": "7.3.0", + "version": "7.3.6", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", + "esbuild": "^0.27.0 || ^0.28.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -4814,6 +5177,96 @@ } } }, + "node_modules/vitest": { + "version": "4.1.10", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/vitest/-/vitest-4.1.10.tgz", + "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.10", + "@vitest/mocker": "4.1.10", + "@vitest/pretty-format": "4.1.10", + "@vitest/runner": "4.1.10", + "@vitest/snapshot": "4.1.10", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.10", + "@vitest/browser-preview": "4.1.10", + "@vitest/browser-webdriverio": "4.1.10", + "@vitest/coverage-istanbul": "4.1.10", + "@vitest/coverage-v8": "4.1.10", + "@vitest/ui": "4.1.10", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, "node_modules/which": { "version": "2.0.2", "dev": true, @@ -4828,6 +5281,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "dev": true, @@ -4838,6 +5308,8 @@ }, "node_modules/yallist": { "version": "3.1.1", + "resolved": "https://packages.applied-caas-gateway1.internal.api.openai.org/artifactory/api/npm/npm-public/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true, "license": "ISC" }, diff --git a/frontend/package.json b/frontend/package.json index d4dc678..b15f707 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -54,14 +54,19 @@ "scripts": { "dev": "vite", "build": "tsc -b && vite build", - "lint": "biome lint .", + "lint": "npm run lint:biome && npm run lint:eslint", "fix": " biome format . --write", "preview": "vite preview", "clean": "rm -rf dist rm -rf build", - "nuke": "rm -rf node_modules && rm -f package-lock.json && npm run clean" + "nuke": "rm -rf node_modules && rm -f package-lock.json && npm run clean", + "lint:biome": "biome lint .", + "lint:eslint": "eslint .", + "test": "vitest run", + "test:watch": "vitest", + "start": "vite preview --host 0.0.0.0 --port 5173" }, "dependencies": { - "axios": "^1.11.0", + "axios": "^1.18.1", "dotenv-expand": "^12.0.3", "html-to-image": "^1.11.13", "jspdf": "^4.2.1", @@ -71,7 +76,7 @@ "react-ga4": "^3.0.1", "react-icons": "^5.5.0", "react-markdown": "^10.1.0", - "react-router-dom": "^7.7.1", + "react-router-dom": "^7.18.1", "remark-gfm": "^4.0.1" }, "devDependencies": { @@ -87,6 +92,11 @@ "globals": "^16.3.0", "typescript": "~5.8.3", "typescript-eslint": "^8.35.1", - "vite": "^7.0.4" + "vite": "^7.3.6", + "vitest": "^4.1.10" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0", + "npm": ">=10" } } diff --git a/frontend/src/App.css b/frontend/src/App.css index 03626c2..49d8986 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -17,7 +17,8 @@ #root { height: 100%; margin: 0; - font-family: Inter, "Segoe UI", Roboto, system-ui, -apple-system, Arial, sans-serif; + font-family: + Inter, "Segoe UI", Roboto, system-ui, -apple-system, Arial, sans-serif; } .app-wrapper { @@ -29,14 +30,16 @@ } .app-wrapper.upload-bg { - background-image: url('/fabric_background.png'); - background-image: image-set(url('/fabric_background.webp') type('image/webp'), url('/fabric_background.png') type('image/png')); + background-image: url("/fabric_background.png"); + background-image: image-set( + url("/fabric_background.webp") type("image/webp"), + url("/fabric_background.png") type("image/png") + ); background-size: cover; background-position: center; background-repeat: no-repeat; } - .site-header { width: 100%; display: grid; @@ -45,7 +48,7 @@ background: rgba(255, 255, 255, 0.9); box-shadow: 0 6px 24px rgba(66, 66, 67, 0.767); grid-template-columns: 20% 1fr 20%; - grid-template-areas: 'left center right'; + grid-template-areas: "left center right"; } .header-left { @@ -81,7 +84,6 @@ letter-spacing: 0.1px; } - .main-content { width: 100%; max-width: var(--container-max); @@ -97,7 +99,6 @@ display: flex; } - .brand-name { font-size: 15px; } @@ -145,3 +146,65 @@ flex: 0 0 calc(100dvh - var(--site-header-height, 56px)); overflow: hidden; } + +.app-error { + display: grid; + min-height: min(62vh, 620px); + place-items: center; + padding: clamp(24px, 6vw, 72px) 20px; +} + +.app-error__card { + width: min(100%, 560px); + padding: clamp(24px, 5vw, 40px); + border: 1px solid var(--border-color, rgba(148, 163, 184, 0.35)); + border-radius: 20px; + background: var(--card-bg, rgba(15, 23, 42, 0.92)); + color: var(--text-primary, #f8fafc); + box-shadow: 0 24px 60px rgba(15, 23, 42, 0.2); + text-align: center; +} + +.app-error__card h1 { + margin: 0 0 12px; + font-size: clamp(1.6rem, 4vw, 2.2rem); +} + +.app-error__card p { + margin: 0; + color: var(--text-secondary, #cbd5e1); + line-height: 1.6; +} + +.app-error__actions { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 12px; + margin-top: 24px; +} + +.app-error__actions button, +.app-error__actions a { + display: inline-flex; + min-height: 42px; + align-items: center; + justify-content: center; + padding: 0 18px; + border: 1px solid currentColor; + border-radius: 999px; + font: inherit; + font-weight: 700; + text-decoration: none; + cursor: pointer; +} + +.app-error__actions button { + background: var(--brand-orange, #f59e0b); + color: #111827; +} + +.app-error__actions a { + background: transparent; + color: inherit; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1b0cbf0..d9e7189 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,8 +4,11 @@ import { useLocation } from "react-router-dom"; import "./App.css"; import { Routing } from "./Routing"; +import { AppErrorBoundary } from "./components/AppErrorBoundary"; import Footer from "./components/Footer"; import { NavBar } from "./components/NavBar"; +import { usePageTracking } from "./hooks/usePageTracking"; +import { useRouteMetadata } from "./hooks/useRouteMetadata"; type ThemeMode = "light" | "dark"; @@ -38,17 +41,25 @@ const getInitialTheme = (): ThemeMode => { return "dark"; } - const savedTheme = window.localStorage.getItem("theme"); - if (savedTheme === "light" || savedTheme === "dark") { - return savedTheme; + try { + const savedTheme = window.localStorage.getItem("theme"); + if (savedTheme === "light" || savedTheme === "dark") { + return savedTheme; + } + } catch { + // Restricted storage must not prevent the app from starting. } - return window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark"; + return window.matchMedia("(prefers-color-scheme: light)").matches + ? "light" + : "dark"; }; const App: React.FC = () => { const location = useLocation(); const [theme, setTheme] = useState(getInitialTheme); + usePageTracking(); + useRouteMetadata(); const isChatRoute = location.pathname.startsWith("/chat"); const isAnalysisRoute = location.pathname.startsWith("/analysis"); const isWorkspaceRoute = isChatRoute || isAnalysisRoute; @@ -56,9 +67,15 @@ const App: React.FC = () => { useEffect(() => { document.documentElement.dataset.theme = theme; document.documentElement.style.colorScheme = theme; - window.localStorage.setItem("theme", theme); + try { + window.localStorage.setItem("theme", theme); + } catch { + // Keep the in-memory theme when storage is blocked or unavailable. + } - const themeColor = document.querySelector('meta[name="theme-color"]'); + const themeColor = document.querySelector( + 'meta[name="theme-color"]', + ); if (themeColor) { themeColor.content = theme === "dark" ? "#0e1322" : "#f8fafc"; } @@ -76,17 +93,23 @@ const App: React.FC = () => { >
-
+
- + + +
{!isWorkspaceRoute &&
} diff --git a/frontend/src/Routing.tsx b/frontend/src/Routing.tsx index e0bbfc1..3fc0cdb 100644 --- a/frontend/src/Routing.tsx +++ b/frontend/src/Routing.tsx @@ -1,7 +1,6 @@ import { Suspense, lazy } from "react"; import Home from "./pages/Home"; -import { Route, Routes } from "react-router-dom"; -import { usePageTracking } from "./hooks/usePageTracking"; +import { Navigate, Route, Routes } from "react-router-dom"; const ImageDescription = lazy(() => import("./pages/ImageDescriptor")); const UploadPage = lazy(() => import("./pages/AudioForm")); @@ -16,7 +15,9 @@ const Reader = lazy(() => import("./pages/Reader")); const CardReader = lazy(() => import("./pages/PanCardReader")); const AadhaarCardReader = lazy(() => import("./pages/AadhaarCardReader")); const NotFound = lazy(() => - import("./components/NotFound").then((module) => ({ default: module.NotFound })), + import("./components/NotFound").then((module) => ({ + default: module.NotFound, + })), ); const RouteFallback = () => ( @@ -26,7 +27,6 @@ const RouteFallback = () => ( ); export const Routing = () => { - usePageTracking(); return ( }> @@ -45,7 +45,11 @@ export const Routing = () => { } /> } /> } /> - } /> + } /> + } + /> ); diff --git a/frontend/src/assets/styles/DescriptionBox.css b/frontend/src/assets/styles/DescriptionBox.css index 3761475..38e9845 100644 --- a/frontend/src/assets/styles/DescriptionBox.css +++ b/frontend/src/assets/styles/DescriptionBox.css @@ -402,3 +402,16 @@ border-radius: 999px; font-weight: 900; } + +/* Analysis-page layout: navigation belongs above the analysis result card. + These direct-child rules intentionally override the legacy below-card order + without affecting DescriptionBox in any other page or context. */ +.analysis-page .description-wrapper > .nav-buttons { + order: 1; + margin: 0 0 18px; + transform: none; +} + +.analysis-page .description-wrapper > .description-box { + order: 2; +} diff --git a/frontend/src/assets/styles/ImagePreviewPanel.css b/frontend/src/assets/styles/ImagePreviewPanel.css index 37ad24b..30d8546 100644 --- a/frontend/src/assets/styles/ImagePreviewPanel.css +++ b/frontend/src/assets/styles/ImagePreviewPanel.css @@ -419,3 +419,11 @@ :root[data-theme="dark"] .analysis-page .analysis-btn.long { color: #171101; } + +/* Analysis-page layout: keep the primary analysis actions above the image card. + The component markup follows the same visual order, so keyboard navigation + and screen-reader reading order remain predictable. */ +.analysis-page .image-preview-container > .preview-buttons-container { + margin: 0 0 18px; + transform: none; +} diff --git a/frontend/src/assets/styles/SeniorUiFixes.css b/frontend/src/assets/styles/SeniorUiFixes.css new file mode 100644 index 0000000..6cb6fbc --- /dev/null +++ b/frontend/src/assets/styles/SeniorUiFixes.css @@ -0,0 +1,358 @@ +/* + * Scoped UI corrections requested during senior review. + * + * This file is imported after the existing theme/makeover layer so these + * narrowly targeted rules win without increasing selector priority across + * unrelated pages. + */ + +/* Analysis: keep each action pair above its own card and anchor the actions to + the card's outer corners. These rules support both the original and the + already-reordered component markup. */ +.analysis-page .image-preview-container > .preview-buttons-container, +.analysis-page .description-wrapper > .nav-buttons { + order: 1; + align-self: stretch; + width: 100%; + min-height: 44px; + margin: 0 0 16px; + display: flex; + flex-flow: row nowrap; + align-items: center; + justify-content: space-between; + gap: 16px; + transform: none; +} + +.analysis-page .image-preview-container > .preview-visual, +.analysis-page .description-wrapper > .description-box { + order: 2; +} + +.analysis-page .image-preview-container > .filesize-name { + order: 3; +} + +.analysis-page .preview-buttons-container > .validation-text { + width: 100%; + margin: 0; + text-align: center; +} + +/* Do not let legacy centering/flex rules pull the corner controls inward. */ +.analysis-page .preview-buttons-container > .analysis-btn, +.analysis-page .description-wrapper > .nav-buttons > .nav-btn { + flex: 0 0 auto; + margin: 0; +} + +/* Chat: slightly reduce visual weight and vertical density. This keeps the + existing layout and interaction model while fitting more conversation in the + viewport and reducing unnecessary scrolling on laptop screens. */ +.app-wrapper--chat .page-root .chat-card-title .main { + font-size: 14px; + font-weight: 700; + line-height: 1.25; +} + +.app-wrapper--chat .page-root .chat-card-actions .btn { + font-size: 13px; + font-weight: 700; +} + +.app-wrapper--chat .page-root .empty-state-large h3 { + margin-bottom: 7px; + font-size: clamp(20px, 2vw, 24px); + font-weight: 700; + line-height: 1.25; + letter-spacing: -0.015em; +} + +.app-wrapper--chat .page-root .empty-state-large p { + margin-bottom: 16px; + font-size: 14px; + font-weight: 400; + line-height: 1.4; +} + +.app-wrapper--chat .page-root .empty-state-large .chip, +.app-wrapper--chat .page-root .empty-state-large .chip:nth-child(odd), +.app-wrapper--chat .page-root .empty-state-large .chip:nth-child(even) { + min-height: 42px; + padding: 8px 12px; + font-size: 13px; + font-weight: 650; + line-height: 1.25; +} + +.app-wrapper--chat .message-wrapper .msg-row { + margin-block: 6px; +} + +.app-wrapper--chat .assistant-block, +.app-wrapper--chat .bubble.user { + font-size: 14px; + font-weight: 400; + line-height: 1.45; +} + +.app-wrapper--chat .assistant-block--with-copy { + padding: 9px 44px 9px 12px; +} + +.app-wrapper--chat .assistant-block .assistant-content p { + margin-bottom: 0.5rem; +} + +.app-wrapper--chat .assistant-block ul, +.app-wrapper--chat .assistant-block ol { + margin-block: 6px 9px; + line-height: 1.45; +} + +.app-wrapper--chat .assistant-block li { + margin-block: 4px; + font-size: 14px; +} + +.app-wrapper--chat .assistant-block h1, +.app-wrapper--chat .assistant-block h2, +.app-wrapper--chat .assistant-block h3, +.app-wrapper--chat .assistant-block h4 { + margin: 0.65rem 0 0.35rem; + font-weight: 700; + line-height: 1.3; +} + +.app-wrapper--chat .assistant-block h1 { + font-size: 1.35rem; +} + +.app-wrapper--chat .assistant-block h2 { + font-size: 1.2rem; +} + +.app-wrapper--chat .assistant-block h3, +.app-wrapper--chat .assistant-block h4 { + font-size: 1.08rem; +} + +/* Attachment preview: center the Remove label geometrically inside its pill. + Explicit sizing prevents inherited global button line-height/padding rules + from nudging the text vertically. */ +.app-wrapper--chat .composer .upload-remove { + box-sizing: border-box; + min-height: 34px; + padding: 0 12px; + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 1; + text-align: center; +} + +@media (max-width: 720px) { + .analysis-page .image-preview-container > .preview-buttons-container, + .analysis-page .description-wrapper > .nav-buttons { + gap: 10px; + margin-bottom: 12px; + } + + .analysis-page .preview-buttons-container > .analysis-btn, + .analysis-page .description-wrapper > .nav-buttons > .nav-btn { + flex: 0 1 48%; + min-width: 0; + padding-inline: 12px; + } + + .app-wrapper--chat .page-root .empty-state-large h3 { + font-size: 20px; + } +} + +/* Follow-up: legacy Analysis CSS still centers the result navigation in some + viewport/theme combinations. The repeated class raises specificity above + route-level centering rules even when that stylesheet loads later. */ +.analysis-page .description-wrapper > .nav-buttons.nav-buttons { + box-sizing: border-box; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + width: 100%; + max-width: none; + padding: 0; + justify-content: stretch; + transform: none; +} + +.analysis-page + .description-wrapper + > .nav-buttons.nav-buttons + > .nav-btn:first-child { + justify-self: start; +} + +.analysis-page + .description-wrapper + > .nav-buttons.nav-buttons + > .nav-btn:last-child { + justify-self: end; +} + +/* Chat attachment removal confirmation: remove hard-coded light-theme inline + colours while preserving the existing state and click handlers. */ +.composer-modal-overlay { + position: fixed; + inset: 0; + z-index: 1300; + padding: 16px; + display: flex; + align-items: center; + justify-content: center; +} + +.composer-modal-overlay .composer-modal--remove-attachment { + --remove-dialog-surface: #ffffff; + --remove-dialog-text: #0f172a; + --remove-dialog-muted: #475569; + --remove-dialog-media: #f1f5f9; + --remove-dialog-border: rgba(15, 23, 42, 0.16); + --remove-dialog-media-border: rgba(15, 23, 42, 0.12); + --remove-dialog-cancel: #ffffff; + + box-sizing: border-box; + width: min(520px, calc(100vw - 32px)); + max-width: 100%; + padding: 18px; + border: 1px solid var(--remove-dialog-border); + border-radius: 16px; + background: var(--remove-dialog-surface); + color: var(--remove-dialog-text); + box-shadow: 0 20px 56px rgba(15, 23, 42, 0.28); +} + +:root[data-theme="dark"] + .composer-modal-overlay + .composer-modal--remove-attachment { + --remove-dialog-surface: #0f172a; + --remove-dialog-text: #f8fafc; + --remove-dialog-muted: #cbd5e1; + --remove-dialog-media: #1e293b; + --remove-dialog-border: rgba(226, 232, 240, 0.24); + --remove-dialog-media-border: rgba(226, 232, 240, 0.2); + --remove-dialog-cancel: #1e293b; + + box-shadow: 0 22px 64px rgba(0, 0, 0, 0.5); +} + +.composer-modal-overlay + .composer-modal--remove-attachment + .composer-remove-dialog__layout { + display: grid; + grid-template-columns: 84px minmax(0, 1fr); + gap: 14px; + align-items: center; +} + +.composer-modal-overlay + .composer-modal--remove-attachment + .composer-remove-dialog__media { + width: 84px; + height: 84px; + display: flex; + align-items: center; + justify-content: center; + border: 1px solid var(--remove-dialog-media-border); + border-radius: 10px; + background: var(--remove-dialog-media); + color: var(--remove-dialog-muted); + font-size: 13px; + font-weight: 700; + line-height: 1; +} + +.composer-modal-overlay + .composer-modal--remove-attachment + .composer-remove-dialog__content { + min-width: 0; +} + +.composer-modal-overlay + .composer-modal--remove-attachment + .composer-remove-dialog__title { + margin: 0 0 6px; + color: var(--remove-dialog-text); + font-size: 16px; + font-weight: 800; + line-height: 1.3; +} + +.composer-modal-overlay + .composer-modal--remove-attachment + .composer-remove-dialog__filename { + color: var(--remove-dialog-muted); + font-size: 14px; + font-weight: 500; + line-height: 1.4; + overflow-wrap: anywhere; +} + +.composer-modal-overlay + .composer-modal--remove-attachment + .composer-remove-dialog__actions { + margin-top: 14px; + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.composer-modal-overlay + .composer-modal--remove-attachment + .composer-remove-dialog__button { + min-height: 38px; + padding: 0 14px; + display: inline-flex; + align-items: center; + justify-content: center; + border-radius: 9px; + font-size: 14px; + font-weight: 800; + line-height: 1; + cursor: pointer; +} + +.composer-modal-overlay + .composer-modal--remove-attachment + .composer-remove-dialog__button--confirm { + border: 1px solid var(--tz-turmeric, #e0a82e); + background: var(--tz-turmeric, #e0a82e); + color: #171101; +} + +.composer-modal-overlay + .composer-modal--remove-attachment + .composer-remove-dialog__button--cancel { + border: 1px solid var(--remove-dialog-border); + background: var(--remove-dialog-cancel); + color: var(--remove-dialog-text); +} + +@media (max-width: 520px) { + .composer-modal-overlay + .composer-modal--remove-attachment + .composer-remove-dialog__layout { + grid-template-columns: 1fr; + text-align: center; + } + + .composer-modal-overlay + .composer-modal--remove-attachment + .composer-remove-dialog__media { + justify-self: center; + } + + .composer-modal-overlay + .composer-modal--remove-attachment + .composer-remove-dialog__actions { + justify-content: center; + } +} diff --git a/frontend/src/assets/styles/navbar.css b/frontend/src/assets/styles/navbar.css index 5c759ad..689ea32 100644 --- a/frontend/src/assets/styles/navbar.css +++ b/frontend/src/assets/styles/navbar.css @@ -1,130 +1,129 @@ -.header-nav ul { - list-style: none; - padding: 0; - margin: 0; -} - -.header-nav a { - text-decoration: none; - color: #222; - font-weight: 500; - display: block; - padding: 6px 16px; - border-radius: 999px; - transition: background 0.18s ease, color 0.18s ease; -} - -.header-nav a.active { - /* pill background is applied by _makeover.css; border-bottom is superseded by pill style */ -} - -.nav-toggle { - display: none; -} - -@media (min-width: 769px) { - - /* desktop layout stays intact */ - .hamburger { - display: none ; - } - - .header-nav ul { - position: static; - display: flex; - flex-direction: row; - align-items: center; - gap: 0.25rem; - background: none; - padding: 0; - transform: none; - opacity: 1; - pointer-events: auto; - height: auto; - box-shadow: none; - top: 1.5rem; - } -} - -@media (max-width: 768px) { - - .mobile-nav-wrapper { - position: relative; - z-index: 999; - } - - /* position hamburger & cross */ - .hamburger { - display: flex; - width: 30px; - height: 22px; - flex-direction: column; - justify-content: space-between; - cursor: pointer; - - position: fixed; - /* always visible */ - top: 16px; - right: 16px; - /* 🔹 fully right aligned */ - z-index: 1002; - /* above overlay */ - } - - .hamburger span { - height: 3px; - width: 100%; - background: #222; - border-radius: 3px; - transition: 0.3s ease; - } - - /* FULLSCREEN OVERLAY */ - .header-nav ul { - position: fixed; - inset: 0; - background: #ffffffee; - backdrop-filter: blur(4px); - display: flex; - flex-direction: column; - padding-top: 100px; - padding-left: 24px; - padding-right: 24px; - gap: 1.2rem; - transform: translateY(-100%); - opacity: 0; - pointer-events: none; - transition: 0.35s ease; - } - - /* SHOW MENU */ - .nav-toggle:checked~.header-nav ul { - transform: translateY(0); - opacity: 1; - pointer-events: auto; - } - - /* ANIMATE BUTTON INTO X */ - .nav-toggle:checked+.hamburger span:nth-child(1) { - transform: translateY(9px) rotate(45deg); - } - - .nav-toggle:checked+.hamburger span:nth-child(2) { - opacity: 0; - } - - .nav-toggle:checked+.hamburger span:nth-child(3) { - transform: translateY(-9px) rotate(-45deg); - } - - /* MOBILE ITEM STYLE */ - .header-nav a { - padding: 14px 10px; - font-size: 18px; - border-radius: 6px; - } - - .header-nav a.active { - background: rgba(0, 0, 0, 0.1); - } -} \ No newline at end of file +.header-nav ul { + list-style: none; + padding: 0; + margin: 0; +} + +.header-nav a { + text-decoration: none; + color: #222; + font-weight: 500; + display: block; + padding: 6px 16px; + border-radius: 999px; + transition: + background 0.18s ease, + color 0.18s ease; +} + +.header-nav a.active { + /* pill background is applied by _makeover.css; border-bottom is superseded by pill style */ +} + +@media (min-width: 769px) { + /* desktop layout stays intact */ + .hamburger { + display: none; + } + + .header-nav ul { + position: static; + display: flex; + flex-direction: row; + align-items: center; + gap: 0.25rem; + background: none; + padding: 0; + transform: none; + opacity: 1; + pointer-events: auto; + height: auto; + box-shadow: none; + top: 1.5rem; + } +} + +@media (max-width: 768px) { + .mobile-nav-wrapper { + position: relative; + z-index: 999; + } + + /* position hamburger & cross */ + .hamburger { + display: flex; + width: 30px; + height: 22px; + flex-direction: column; + justify-content: space-between; + cursor: pointer; + padding: 0; + border: 0; + background: transparent; + + position: fixed; + /* always visible */ + top: 16px; + right: 16px; + /* 🔹 fully right aligned */ + z-index: 1002; + /* above overlay */ + } + + .hamburger span { + height: 3px; + width: 100%; + background: #222; + border-radius: 3px; + transition: 0.3s ease; + } + + /* FULLSCREEN OVERLAY */ + .header-nav ul { + position: fixed; + inset: 0; + background: #ffffffee; + backdrop-filter: blur(4px); + display: flex; + flex-direction: column; + padding-top: 100px; + padding-left: 24px; + padding-right: 24px; + gap: 1.2rem; + transform: translateY(-100%); + opacity: 0; + pointer-events: none; + transition: 0.35s ease; + } + + /* SHOW MENU */ + .header-nav[data-open="true"] ul { + transform: translateY(0); + opacity: 1; + pointer-events: auto; + } + + /* ANIMATE BUTTON INTO X */ + .hamburger[aria-expanded="true"] span:nth-child(1) { + transform: translateY(9px) rotate(45deg); + } + + .hamburger[aria-expanded="true"] span:nth-child(2) { + opacity: 0; + } + + .hamburger[aria-expanded="true"] span:nth-child(3) { + transform: translateY(-9px) rotate(-45deg); + } + + /* MOBILE ITEM STYLE */ + .header-nav a { + padding: 14px 10px; + font-size: 18px; + border-radius: 6px; + } + + .header-nav a.active { + background: rgba(0, 0, 0, 0.1); + } +} diff --git a/frontend/src/components/AppErrorBoundary.tsx b/frontend/src/components/AppErrorBoundary.tsx new file mode 100644 index 0000000..510d1bf --- /dev/null +++ b/frontend/src/components/AppErrorBoundary.tsx @@ -0,0 +1,61 @@ +import type { ErrorInfo, ReactNode } from "react"; +import { Component } from "react"; + +type Props = { + children: ReactNode; + resetKey: string; +}; + +type State = { + hasError: boolean; +}; + +/** + * Keeps a route render/lazy-load failure from blanking the entire application. + * Changing routes resets the boundary so navigation remains usable. + */ +export class AppErrorBoundary extends Component { + state: State = { hasError: false }; + + static getDerivedStateFromError(): State { + return { hasError: true }; + } + + componentDidCatch(error: Error, info: ErrorInfo) { + if (import.meta.env.DEV) { + console.error("Route render failed", error, info.componentStack); + } + } + + componentDidUpdate(previousProps: Props) { + if (this.state.hasError && previousProps.resetKey !== this.props.resetKey) { + this.setState({ hasError: false }); + } + } + + private retry = () => { + this.setState({ hasError: false }); + }; + + render() { + if (!this.state.hasError) return this.props.children; + + return ( +
+
+

Something went wrong

+

+ This page could not be displayed. Your uploaded files and chat input + have not been submitted again. +

+
+ + Return home +
+
+
+ ); + } +} diff --git a/frontend/src/components/Composer.tsx b/frontend/src/components/Composer.tsx index a8f3fef..d376bc1 100644 --- a/frontend/src/components/Composer.tsx +++ b/frontend/src/components/Composer.tsx @@ -307,7 +307,11 @@ export default function Composer({ }; // NOTE: keep the rest of your logic unchanged (recording functions etc.) - const handleChipActionDefault = (actionId: string, _opts?: { name?: string }) => { + const handleChipActionDefault = ( + actionId: string, + opts?: { name?: string }, + ) => { + void opts; if (actionId === "image:analyze_short") { setMode("analysis"); onChange(textForAnalysisShort); @@ -456,7 +460,6 @@ export default function Composer({ if (pendingImage) try { URL.revokeObjectURL(pendingImage.url); } catch (e) { console.log(e) } if (pendingAudio) try { URL.revokeObjectURL(pendingAudio.url); } catch (e) { console.log(e) } }; - // eslint-disable-next-line react-hooks/exhaustive-deps }, [pendingImage, pendingAudio]); useEffect(() => { @@ -876,20 +879,41 @@ export default function Composer({ }} /> {pendingRemove && ( -
-
-
-
-
- {pendingRemove.kind === "image" ? "Image" : "Audio"} -
+
+
+
+ -
-
{pendingRemove.kind === "image" ? "Remove this image?" : "Remove this audio?"}
- {pendingRemove.name &&
{pendingRemove.name}
} -
- - +
+
+ {pendingRemove.kind === "image" ? "Remove this image?" : "Remove this audio?"} +
+ {pendingRemove.name && ( +
+ {pendingRemove.name} +
+ )} +
+ +
diff --git a/frontend/src/components/DescriptionBox.tsx b/frontend/src/components/DescriptionBox.tsx index 2e44f2a..b9b258c 100644 --- a/frontend/src/components/DescriptionBox.tsx +++ b/frontend/src/components/DescriptionBox.tsx @@ -4,6 +4,18 @@ import { FaRegCopy } from "react-icons/fa"; import "@/assets/styles/DescriptionBox.css"; import Loader from "./Loader"; +type DescriptionBoxProps = { + isValidImage: boolean | null; + validationMessage: string; + loading: boolean; + responses: string[]; + currentIndex?: number; + typedText?: string; + description?: string; + handlePrev: () => void; + handleNext: () => void | Promise; +}; + const DescriptionBox = ({ isValidImage, validationMessage, @@ -14,7 +26,7 @@ const DescriptionBox = ({ description = "", handlePrev, handleNext, -}) => { +}: DescriptionBoxProps) => { const [copied, setCopied] = useState(false); const isError = isValidImage === false && validationMessage; diff --git a/frontend/src/components/DrawerToggle.tsx b/frontend/src/components/DrawerToggle.tsx index c4a6047..f1aa66f 100644 --- a/frontend/src/components/DrawerToggle.tsx +++ b/frontend/src/components/DrawerToggle.tsx @@ -1,6 +1,12 @@ +import type { Dispatch, SetStateAction } from "react"; import "@/assets/styles/DrawerToggle.css"; -const DrawerToggle = ({ showDrawer, setShowDrawer }) => { +type DrawerToggleProps = { + showDrawer: boolean; + setShowDrawer: Dispatch>; +}; + +const DrawerToggle = ({ showDrawer, setShowDrawer }: DrawerToggleProps) => { return (
{/* Brand + blurb */}
- - - + + + @@ -20,8 +38,8 @@ export default function Footer() {
FabricAI

- Professional fabric analysis powered by artificial intelligence. - Trusted by textile professionals worldwide. + AI-assisted tools for fabric analysis, image search, and + structured documentation.

{/* GitHub icon */} - + tz-fabric @@ -43,17 +67,29 @@ export default function Footer() {

Product

    -
  • Features
  • -
  • Pricing
  • -
  • API
  • +
  • + Analysis +
  • +
  • + Search +
  • +
  • + Chat +

Support

    -
  • Documentation
  • -
  • Contact
  • -
  • Help Center
  • +
  • + Uploaded Fabrics +
  • +
  • + Document Reader +
  • +
  • + Contact +
@@ -61,7 +97,16 @@ export default function Footer() {
- © { new Date().getFullYear() } · RecursiveZero Private Limited . All Rights Reserved. + © {new Date().getFullYear()} ·{" "} + + {" "} + RecursiveZero Private Limited{" "} + + . All Rights Reserved.
); diff --git a/frontend/src/components/ImagePreviewPanel.tsx b/frontend/src/components/ImagePreviewPanel.tsx index b541dad..f308429 100644 --- a/frontend/src/components/ImagePreviewPanel.tsx +++ b/frontend/src/components/ImagePreviewPanel.tsx @@ -1,6 +1,26 @@ import { useState } from "react"; +import type { ChangeEvent, DragEvent, RefObject } from "react"; import "@/assets/styles/ImagePreviewPanel.css"; +type AnalysisMode = "short" | "long"; + +type ImagePreviewPanelProps = { + uploadedImageUrl: string | null; + sampleImageUrl: string | null; + validationLoading: boolean; + isValidImage: boolean | null; + loading: boolean; + currentFile: File | null; + handleRunAnalysis: ( + file: File | null, + mode: AnalysisMode, + ) => void | Promise; + handleUploadedImage: (file: File) => void; + imageInputRef: RefObject; + showButtons?: boolean; + clearImage: () => void; +}; + const ImagePreviewPanel = ({ uploadedImageUrl, sampleImageUrl, @@ -13,13 +33,13 @@ const ImagePreviewPanel = ({ imageInputRef, showButtons = true, clearImage, -}) => { +}: ImagePreviewPanelProps) => { const [showConfirm, setShowConfirm] = useState(false); - const giveFileNameAndSize = (currentFile) => { - if (!currentFile) return ""; - const fileName = currentFile.name; - const fileSize = (currentFile.size / 1024).toFixed(2); // KB + const giveFileNameAndSize = (file: File | null) => { + if (!file) return ""; + const fileName = file.name; + const fileSize = (file.size / 1024).toFixed(2); // KB return `${fileName} (${fileSize} KB)`; }; @@ -32,14 +52,14 @@ const ImagePreviewPanel = ({ !loading && isImageValid; - const onDrop = (e) => { + const onDrop = (e: DragEvent) => { e.preventDefault(); const file = e.dataTransfer.files?.[0]; if (file && typeof handleUploadedImage === "function") handleUploadedImage(file); }; - const onFileChange = (e) => { + const onFileChange = (e: ChangeEvent) => { const file = e.target.files?.[0]; if (file && typeof handleUploadedImage === "function") handleUploadedImage(file); @@ -63,11 +83,54 @@ const ImagePreviewPanel = ({ return (
+ {showButtons && ( +
+ {validationLoading ? ( +

+ 🔍 Checking if this is a valid fabric image... +

+ ) : ( + <> + + + + + )} +
+ )}
{hasImage ? (
Preview @@ -114,50 +177,6 @@ const ImagePreviewPanel = ({ )}
- {showButtons && ( -
- {validationLoading ? ( -

- 🔍 Checking if this is a valid fabric image... -

- ) : ( - <> - - - - - )} -
- )} - {showConfirm && (
diff --git a/frontend/src/components/MessageList.tsx b/frontend/src/components/MessageList.tsx index 2ea5b43..377c2ad 100644 --- a/frontend/src/components/MessageList.tsx +++ b/frontend/src/components/MessageList.tsx @@ -30,6 +30,22 @@ export default function MessageList({ const lastAssistantRef = useRef(null); const lastAssistantMsgRef = useRef(null); const rafRef = useRef(null); + const messageKeysRef = useRef(new WeakMap()); + const nextMessageKeyRef = useRef(0); + + const getMessageKey = (message: Message): string => { + const messageWithId = message as Message & { id?: unknown }; + if (typeof messageWithId.id === "string" && messageWithId.id.trim()) { + return `id:${messageWithId.id}`; + } + + const existing = messageKeysRef.current.get(message); + if (existing) return existing; + + const generated = `message:${nextMessageKeyRef.current++}`; + messageKeysRef.current.set(message, generated); + return generated; + }; // Always keep scroll pinned to bottom on commit useLayoutEffect(() => { @@ -148,7 +164,6 @@ export default function MessageList({ rafRef.current = null; } }; - // eslint-disable-next-line react-hooks/exhaustive-deps }, [messages, onLastAssistantRendered, scrollerRef]); // helper to decide whether a message contains the "ask more" prompt @@ -214,7 +229,7 @@ export default function MessageList({ return (
{ - const navClass = ({ isActive }) => (isActive ? "active" : ""); + const [isOpen, setIsOpen] = useState(false); + const location = useLocation(); + const toggleRef = useRef(null); + const navRef = useRef(null); + const navClass = ({ isActive }: NavLinkRenderProps) => + isActive ? "active" : ""; + + useEffect(() => { + if (location.pathname) setIsOpen(false); + }, [location.pathname]); + + useEffect(() => { + if (!isOpen) return; + + const firstLink = navRef.current?.querySelector("a"); + firstLink?.focus(); + + const onKeyDown = (event: KeyboardEvent) => { + if (event.key !== "Escape") return; + setIsOpen(false); + toggleRef.current?.focus(); + }; + + document.addEventListener("keydown", onKeyDown); + return () => document.removeEventListener("keydown", onKeyDown); + }, [isOpen]); + return (
- - - - -