+
+
@@ -36,18 +35,13 @@
overflow: hidden;
}
-.ui-component-card summary {
+.ui-component-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
- cursor: pointer;
+ border-bottom: 1px solid var(--border, #2a2a3e);
user-select: none;
- list-style: none;
-}
-
-.ui-component-card summary::-webkit-details-marker {
- display: none;
}
.ui-component-icon {
@@ -73,26 +67,7 @@
color: var(--accent, #4ec9b0);
}
-.ui-component-chevron {
- transition: transform 0.2s;
- color: var(--text-muted, #6c7086);
-}
-
-.ui-component-card[open] .ui-component-chevron {
- transform: rotate(180deg);
-}
-
.ui-component-body {
- border-top: 1px solid var(--border, #2a2a3e);
-}
-
-.ui-component-payload {
- margin: 0;
- padding: 14px;
- white-space: pre-wrap;
- font-family: "IBM Plex Mono", monospace;
- font-size: 12px;
- line-height: 1.6;
- color: var(--text, #cdd6f4);
+ padding: 12px 14px;
}
diff --git a/webclient/tests/unit/components/CardGrid.test.js b/webclient/tests/unit/components/CardGrid.test.js
new file mode 100644
index 0000000..18407dc
--- /dev/null
+++ b/webclient/tests/unit/components/CardGrid.test.js
@@ -0,0 +1,73 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest'
+import { mount } from '@vue/test-utils'
+import { nextTick } from 'vue'
+import CardGrid from '@/components/messages/CardGrid.vue'
+
+describe('CardGrid', () => {
+ const sample = {
+ title: 'Sample results',
+ cards: [
+ {
+ id: 'c1',
+ title: 'Card one',
+ subtitle: 'Subtitle one',
+ image: 'https://example.com/1.jpg',
+ meta: [
+ { label: 'Price', value: '$80k' },
+ { label: 'Area', value: '58m²' }
+ ],
+ description: 'Short description',
+ details: [
+ { label: 'Address', value: 'St. One' },
+ { label: 'Floor', value: '4/9' }
+ ],
+ actions: [{ label: 'Open', url: 'https://example.com' }]
+ },
+ { id: 'c2', title: 'Card two', subtitle: 'Subtitle two' },
+ { id: 'c3', title: 'Card three' },
+ { id: 'c4', title: 'Card four' },
+ { id: 'c5', title: 'Card five' }
+ ]
+ }
+
+ beforeEach(() => {
+ const el = document.createElement('div')
+ el.id = 'teleport-target'
+ document.body.appendChild(el)
+ })
+
+ it('renders title and up to 4 cards', () => {
+ const wrapper = mount(CardGrid, { props: { data: sample } })
+ expect(wrapper.text()).toContain('Sample results')
+ const items = wrapper.findAll('.card-grid-item')
+ expect(items).toHaveLength(4)
+ })
+
+ it('shows +N more indicator when more than 4 cards', () => {
+ const wrapper = mount(CardGrid, { props: { data: sample } })
+ expect(wrapper.find('.card-grid-more-text').exists()).toBe(true)
+ expect(wrapper.text()).toContain('+1 more')
+ })
+
+ it('renders card meta labels and values', () => {
+ const wrapper = mount(CardGrid, { props: { data: sample } })
+ expect(wrapper.text()).toContain('Price')
+ expect(wrapper.text()).toContain('$80k')
+ })
+
+ it('opens modal on card click', async () => {
+ const wrapper = mount(CardGrid, { props: { data: sample } })
+ const firstCard = wrapper.find('.card-grid-item')
+ await firstCard.trigger('click')
+ await nextTick()
+ expect(document.querySelector('.card-modal')).not.toBeNull()
+ expect(document.querySelector('.card-modal-title')?.textContent).toBe('Card one')
+ })
+
+ it('sets active card on click', async () => {
+ const wrapper = mount(CardGrid, { props: { data: sample }, attachTo: document.body })
+ await wrapper.find('.card-grid-item').trigger('click')
+ await nextTick()
+ expect(wrapper.vm.activeCard?.id).toBe('c1')
+ })
+})
diff --git a/webclient/tests/unit/stores/chat.test.js b/webclient/tests/unit/stores/chat.test.js
index 4eb6f1e..2198a16 100644
--- a/webclient/tests/unit/stores/chat.test.js
+++ b/webclient/tests/unit/stores/chat.test.js
@@ -214,6 +214,22 @@
expect(store.messages[0]).toMatchObject({ role: 'assistant', type: 'ui_component', component: 'chart', payload: { value: 42 } })
})
+ it('onUiComponent stores card_grid payload', () => {
+ const store = useChatStore()
+ store.onUiComponent({
+ component: 'card_grid',
+ payload: {
+ title: 'Results',
+ cards: [
+ { id: 'c1', title: 'A', subtitle: 'desc', meta: [{ label: 'Price', value: '100' }] }
+ ]
+ }
+ })
+ expect(store.messages[0].component).toBe('card_grid')
+ expect(store.messages[0].payload.cards).toHaveLength(1)
+ expect(store.messages[0].payload.cards[0].id).toBe('c1')
+ })
+
it('onError pushes error message', () => {
const store = useChatStore()
store.onError({ message: 'boom' })