Transform search bar in Quote mode to Airbnb-style segmented input
All checks were successful
Build Docker Image / build (push) Successful in 3m21s
All checks were successful
Build Docker Image / build (push) Successful in 3m21s
- Remove mode toggle [Explore/Quote] tabs from header - In Quote mode: show segmented input (Product | Hub | Quantity) + Search button - In Explore mode: keep regular pill input with chips - Add productLabel, hubLabel, supplierLabel computed values to useCatalogSearch - Pass Quote mode props to MainNavigation
This commit is contained in:
@@ -9,84 +9,105 @@
|
|||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Center: Search input + chips -->
|
<!-- Center: Search input (transforms based on mode) -->
|
||||||
<div class="flex-1 flex flex-col items-center max-w-2xl mx-auto gap-2">
|
<div class="flex-1 flex flex-col items-center max-w-2xl mx-auto gap-2">
|
||||||
<!-- Big pill input -->
|
<!-- Quote mode: Segmented input like Airbnb -->
|
||||||
<div
|
<template v-if="catalogMode === 'quote'">
|
||||||
class="flex items-center gap-3 w-full px-5 py-3 rounded-full border border-base-300 bg-base-100 focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20 transition-all cursor-text"
|
<div class="flex items-center gap-3 w-full">
|
||||||
@click="focusInput"
|
<div class="flex items-center flex-1 rounded-full border border-base-300 bg-base-100 shadow-sm divide-x divide-base-300">
|
||||||
>
|
<!-- Product segment -->
|
||||||
<Icon name="lucide:search" size="22" class="text-primary flex-shrink-0" />
|
|
||||||
|
|
||||||
<!-- Tokens + input inline -->
|
|
||||||
<div class="flex items-center gap-2 flex-wrap flex-1 min-w-0">
|
|
||||||
<!-- Active filter tokens -->
|
|
||||||
<div
|
|
||||||
v-for="token in activeTokens"
|
|
||||||
:key="token.type"
|
|
||||||
class="badge badge-lg gap-1.5 cursor-pointer hover:opacity-80 transition-all flex-shrink-0 text-white"
|
|
||||||
:style="{ backgroundColor: getTokenColor(token.type) }"
|
|
||||||
@click.stop="$emit('edit-token', token.type)"
|
|
||||||
>
|
|
||||||
<Icon :name="token.icon" size="14" />
|
|
||||||
<span class="max-w-28 truncate">{{ token.label }}</span>
|
|
||||||
<button
|
<button
|
||||||
class="hover:text-error"
|
class="flex-1 px-4 py-2.5 text-left hover:bg-base-200/50 rounded-l-full transition-colors min-w-0"
|
||||||
@click.stop="$emit('remove-token', token.type)"
|
@click="$emit('edit-token', 'product')"
|
||||||
>
|
>
|
||||||
<Icon name="lucide:x" size="14" />
|
<div class="text-xs text-base-content/60">{{ $t('catalog.quote.product') }}</div>
|
||||||
|
<div class="font-medium truncate">{{ productLabel || $t('catalog.quote.selectProduct') }}</div>
|
||||||
|
</button>
|
||||||
|
<!-- Hub segment -->
|
||||||
|
<button
|
||||||
|
class="flex-1 px-4 py-2.5 text-left hover:bg-base-200/50 transition-colors min-w-0"
|
||||||
|
@click="$emit('edit-token', 'hub')"
|
||||||
|
>
|
||||||
|
<div class="text-xs text-base-content/60">{{ $t('catalog.quote.hub') }}</div>
|
||||||
|
<div class="font-medium truncate">{{ hubLabel || $t('catalog.quote.selectHub') }}</div>
|
||||||
|
</button>
|
||||||
|
<!-- Quantity segment -->
|
||||||
|
<button
|
||||||
|
class="flex-1 px-4 py-2.5 text-left hover:bg-base-200/50 rounded-r-full transition-colors min-w-0"
|
||||||
|
@click="$emit('edit-token', 'quantity')"
|
||||||
|
>
|
||||||
|
<div class="text-xs text-base-content/60">{{ $t('catalog.quote.quantity') }}</div>
|
||||||
|
<div class="font-medium">{{ quantity ? `${quantity} ${$t('units.t')}` : '—' }}</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Search button -->
|
||||||
<!-- Search input -->
|
<button
|
||||||
<input
|
class="btn btn-primary btn-circle shadow-lg"
|
||||||
ref="inputRef"
|
:disabled="!canSearch"
|
||||||
v-model="localSearchQuery"
|
@click="$emit('search')"
|
||||||
type="text"
|
>
|
||||||
:placeholder="placeholder"
|
<Icon name="lucide:search" size="20" />
|
||||||
class="flex-1 min-w-32 bg-transparent outline-none text-lg"
|
</button>
|
||||||
@input="$emit('update:search-query', localSearchQuery)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
|
|
||||||
<!-- Chips below -->
|
<!-- Explore mode: Regular pill input + chips -->
|
||||||
<div
|
<template v-else>
|
||||||
v-if="availableChips.length > 0"
|
<!-- Big pill input -->
|
||||||
class="flex items-center justify-center gap-2"
|
<div
|
||||||
>
|
class="flex items-center gap-3 w-full px-5 py-3 rounded-full border border-base-300 bg-base-100 focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20 transition-all cursor-text"
|
||||||
<button
|
@click="focusInput"
|
||||||
v-for="chip in availableChips"
|
|
||||||
:key="chip.type"
|
|
||||||
class="btn btn-xs btn-ghost gap-1"
|
|
||||||
@click="$emit('start-select', chip.type)"
|
|
||||||
>
|
>
|
||||||
<Icon name="lucide:plus" size="12" />
|
<Icon name="lucide:search" size="22" class="text-primary flex-shrink-0" />
|
||||||
{{ chip.label }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Catalog mode toggle (only on catalog pages) -->
|
<!-- Tokens + input inline -->
|
||||||
<div v-if="showCatalogModeToggle" class="flex-shrink-0 py-2.5">
|
<div class="flex items-center gap-2 flex-wrap flex-1 min-w-0">
|
||||||
<div class="tabs tabs-boxed tabs-sm bg-base-200">
|
<!-- Active filter tokens -->
|
||||||
<button
|
<div
|
||||||
class="tab"
|
v-for="token in activeTokens"
|
||||||
:class="{ 'tab-active': catalogMode === 'explore' }"
|
:key="token.type"
|
||||||
@click="$emit('set-catalog-mode', 'explore')"
|
class="badge badge-lg gap-1.5 cursor-pointer hover:opacity-80 transition-all flex-shrink-0 text-white"
|
||||||
|
:style="{ backgroundColor: getTokenColor(token.type) }"
|
||||||
|
@click.stop="$emit('edit-token', token.type)"
|
||||||
|
>
|
||||||
|
<Icon :name="token.icon" size="14" />
|
||||||
|
<span class="max-w-28 truncate">{{ token.label }}</span>
|
||||||
|
<button
|
||||||
|
class="hover:text-error"
|
||||||
|
@click.stop="$emit('remove-token', token.type)"
|
||||||
|
>
|
||||||
|
<Icon name="lucide:x" size="14" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search input -->
|
||||||
|
<input
|
||||||
|
ref="inputRef"
|
||||||
|
v-model="localSearchQuery"
|
||||||
|
type="text"
|
||||||
|
:placeholder="placeholder"
|
||||||
|
class="flex-1 min-w-32 bg-transparent outline-none text-lg"
|
||||||
|
@input="$emit('update:search-query', localSearchQuery)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chips below -->
|
||||||
|
<div
|
||||||
|
v-if="availableChips.length > 0"
|
||||||
|
class="flex items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
<Icon name="lucide:compass" size="14" class="mr-1" />
|
<button
|
||||||
{{ $t('catalog.modes.explore') }}
|
v-for="chip in availableChips"
|
||||||
</button>
|
:key="chip.type"
|
||||||
<button
|
class="btn btn-xs btn-ghost gap-1"
|
||||||
class="tab"
|
@click="$emit('start-select', chip.type)"
|
||||||
:class="{ 'tab-active': catalogMode === 'quote' }"
|
>
|
||||||
@click="$emit('set-catalog-mode', 'quote')"
|
<Icon name="lucide:plus" size="12" />
|
||||||
>
|
{{ chip.label }}
|
||||||
<Icon name="lucide:search" size="14" class="mr-1" />
|
</button>
|
||||||
{{ $t('catalog.modes.quote') }}
|
</div>
|
||||||
</button>
|
</template>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right: AI + Globe + Team + User -->
|
<!-- Right: AI + Globe + Team + User -->
|
||||||
@@ -232,8 +253,12 @@ const props = defineProps<{
|
|||||||
selectMode?: SelectMode
|
selectMode?: SelectMode
|
||||||
searchQuery?: string
|
searchQuery?: string
|
||||||
// Catalog mode props
|
// Catalog mode props
|
||||||
showCatalogModeToggle?: boolean
|
|
||||||
catalogMode?: CatalogMode
|
catalogMode?: CatalogMode
|
||||||
|
// Quote mode props
|
||||||
|
productLabel?: string
|
||||||
|
hubLabel?: string
|
||||||
|
quantity?: string
|
||||||
|
canSearch?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
defineEmits([
|
defineEmits([
|
||||||
@@ -247,8 +272,8 @@ defineEmits([
|
|||||||
'edit-token',
|
'edit-token',
|
||||||
'remove-token',
|
'remove-token',
|
||||||
'update:search-query',
|
'update:search-query',
|
||||||
// Catalog mode
|
// Quote mode
|
||||||
'set-catalog-mode'
|
'search'
|
||||||
])
|
])
|
||||||
|
|
||||||
const localePath = useLocalePath()
|
const localePath = useLocalePath()
|
||||||
|
|||||||
@@ -243,6 +243,11 @@ export function useCatalogSearch() {
|
|||||||
return !!(productId.value && (hubId.value || supplierId.value))
|
return !!(productId.value && (hubId.value || supplierId.value))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Labels for Quote mode display
|
||||||
|
const productLabel = computed(() => getLabel('product', productId.value))
|
||||||
|
const hubLabel = computed(() => getLabel('hub', hubId.value))
|
||||||
|
const supplierLabel = computed(() => getLabel('supplier', supplierId.value))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// State
|
// State
|
||||||
selectMode,
|
selectMode,
|
||||||
@@ -262,6 +267,9 @@ export function useCatalogSearch() {
|
|||||||
activeTokens,
|
activeTokens,
|
||||||
availableChips,
|
availableChips,
|
||||||
canSearch,
|
canSearch,
|
||||||
|
productLabel,
|
||||||
|
hubLabel,
|
||||||
|
supplierLabel,
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
startSelect,
|
startSelect,
|
||||||
|
|||||||
@@ -16,8 +16,11 @@
|
|||||||
:available-chips="availableChips"
|
:available-chips="availableChips"
|
||||||
:select-mode="selectMode"
|
:select-mode="selectMode"
|
||||||
:search-query="searchQuery"
|
:search-query="searchQuery"
|
||||||
:show-catalog-mode-toggle="isCatalogSection"
|
|
||||||
:catalog-mode="catalogMode"
|
:catalog-mode="catalogMode"
|
||||||
|
:product-label="productLabel"
|
||||||
|
:hub-label="hubLabel"
|
||||||
|
:quantity="quantity"
|
||||||
|
:can-search="canSearch"
|
||||||
@toggle-theme="toggleTheme"
|
@toggle-theme="toggleTheme"
|
||||||
@sign-out="onClickSignOut"
|
@sign-out="onClickSignOut"
|
||||||
@sign-in="signIn()"
|
@sign-in="signIn()"
|
||||||
@@ -27,7 +30,7 @@
|
|||||||
@edit-token="editFilter"
|
@edit-token="editFilter"
|
||||||
@remove-token="removeFilter"
|
@remove-token="removeFilter"
|
||||||
@update:search-query="searchQuery = $event"
|
@update:search-query="searchQuery = $event"
|
||||||
@set-catalog-mode="setCatalogMode"
|
@search="onSearch"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Sub Navigation (section-specific tabs) - only for non-catalog sections -->
|
<!-- Sub Navigation (section-specific tabs) - only for non-catalog sections -->
|
||||||
@@ -61,7 +64,10 @@ const {
|
|||||||
removeFilter,
|
removeFilter,
|
||||||
editFilter,
|
editFilter,
|
||||||
catalogMode,
|
catalogMode,
|
||||||
setCatalogMode
|
productLabel,
|
||||||
|
hubLabel,
|
||||||
|
quantity,
|
||||||
|
canSearch
|
||||||
} = useCatalogSearch()
|
} = useCatalogSearch()
|
||||||
|
|
||||||
// Collapsible header for catalog pages
|
// Collapsible header for catalog pages
|
||||||
@@ -234,4 +240,15 @@ watch(theme, (value) => applyTheme(value))
|
|||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
theme.value = theme.value === 'night' ? 'cupcake' : 'night'
|
theme.value = theme.value === 'night' ? 'cupcake' : 'night'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Search handler for Quote mode - emits event that page can handle
|
||||||
|
const localePath = useLocalePath()
|
||||||
|
const router = useRouter()
|
||||||
|
const onSearch = () => {
|
||||||
|
// Navigate to catalog page which will handle the search
|
||||||
|
if (!route.path.includes('/catalog')) {
|
||||||
|
router.push({ path: localePath('/catalog'), query: { ...route.query, mode: 'quote' } })
|
||||||
|
}
|
||||||
|
// The page component will react to canSearch becoming true and perform the search
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user