From da2ac8bbcc0c83b7ffe27c1073ca19bb2cb3c937 Mon Sep 17 00:00:00 2001 From: Lovehsigure_520 <62863834+Neko-233@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:34:24 +0800 Subject: [PATCH] fix(ui): standardize scrollable areas (#2399) --- .../onboarding/step-permissions.vue | 22 +- .../InteractiveArea.browser.test.ts | 90 +++++ .../renderer/components/InteractiveArea.vue | 360 +++++++++--------- .../chat-viewport-layout.browser.test.ts | 73 ++++ .../components/chat-viewport-layout.vue | 57 +++ .../src/renderer/pages/about.vue | 17 +- .../src/renderer/pages/chat-page-shell.vue | 13 + .../src/renderer/pages/chat.browser.test.ts | 37 ++ .../src/renderer/pages/chat.vue | 7 +- .../renderer/pages/settings/models/index.vue | 1 - .../widgets/artistry/components/Comfy.vue | 52 +-- apps/stage-tamagotchi/vitest.config.ts | 2 + .../characters/components/CharacterDialog.vue | 10 +- docs/ai/context/ui-components.md | 15 + .../src/pages/settings/models/index.vue | 2 +- .../settings/providers/artistry/comfyui.vue | 54 +-- .../src/components/data-pane/pane.vue | 10 +- .../scenarios/chat/JournalPreviewModal.vue | 9 +- ...t-history-scroll-container.browser.test.ts | 95 +++++ .../chat-history-scroll-container.vue | 130 +++++++ .../chat/components/history.browser.test.ts | 83 ++++ .../scenarios/chat/components/history.vue | 43 +-- .../scrollable-area.browser.test.ts | 106 ++++++ .../sessions-dialog.browser.test.ts | 36 +- .../chat/components/sessions-dialog.vue | 13 +- .../composables/use-chat-history-scroll.ts | 4 +- .../composables/use-virtualizer-scroll.ts | 2 +- .../dialogs/onboarding/onboarding-dialog.vue | 2 +- .../step-provider-configuration.vue | 174 ++++----- .../onboarding/step-provider-selection.vue | 6 +- .../settings/model-settings/panel.vue | 155 ++++---- packages/ui/src/components/layouts/index.ts | 1 + .../components/layouts/scrollable-area.vue | 147 +++++++ .../src/components/misc/container-error.vue | 38 +- 34 files changed, 1387 insertions(+), 479 deletions(-) create mode 100644 apps/stage-tamagotchi/src/renderer/components/chat-viewport-layout.browser.test.ts create mode 100644 apps/stage-tamagotchi/src/renderer/components/chat-viewport-layout.vue create mode 100644 apps/stage-tamagotchi/src/renderer/pages/chat-page-shell.vue create mode 100644 apps/stage-tamagotchi/src/renderer/pages/chat.browser.test.ts create mode 100644 packages/stage-ui/src/components/scenarios/chat/components/chat-history-scroll-container.browser.test.ts create mode 100644 packages/stage-ui/src/components/scenarios/chat/components/chat-history-scroll-container.vue create mode 100644 packages/stage-ui/src/components/scenarios/chat/components/scrollable-area.browser.test.ts create mode 100644 packages/ui/src/components/layouts/scrollable-area.vue diff --git a/apps/stage-pocket/src/components/onboarding/step-permissions.vue b/apps/stage-pocket/src/components/onboarding/step-permissions.vue index 93e68a605..1ea11e7dd 100644 --- a/apps/stage-pocket/src/components/onboarding/step-permissions.vue +++ b/apps/stage-pocket/src/components/onboarding/step-permissions.vue @@ -1,5 +1,5 @@ diff --git a/apps/stage-tamagotchi/src/renderer/components/chat-viewport-layout.browser.test.ts b/apps/stage-tamagotchi/src/renderer/components/chat-viewport-layout.browser.test.ts new file mode 100644 index 000000000..5439433be --- /dev/null +++ b/apps/stage-tamagotchi/src/renderer/components/chat-viewport-layout.browser.test.ts @@ -0,0 +1,73 @@ +import { ScrollableArea } from '@proj-airi/ui' +import { describe, expect, it, vi } from 'vitest' +import { render } from 'vitest-browser-vue' +import { defineComponent } from 'vue' + +import ChatViewportLayout from './chat-viewport-layout.vue' + +import '@unocss/reset/tailwind.css' +import 'virtual:uno.css' + +describe('desktop chat viewport layout', () => { + // ROOT CAUSE: + // + // The history viewport must stop above the fixed composer so the scrollbar + // belongs to messages only instead of extending beside the input controls. + it('keeps the message viewport above a fixed composer', async () => { + const TestHost = defineComponent({ + components: { ChatViewportLayout, ScrollableArea }, + template: ` + + + + + `, + }) + + const screen = await render(TestHost) + const layout = screen.getByTestId('chat-viewport-layout').element() as HTMLElement + const historyLayer = screen.getByTestId('chat-history-layer').element() as HTMLElement + const composer = screen.getByTestId('chat-composer-layer').element() as HTMLElement + const history = screen.container.querySelector('.chat-history-list') + const scrollbar = screen.container.querySelector('.scrollable-area-scrollbar--vertical') + + expect(history).not.toBeNull() + expect(scrollbar).not.toBeNull() + if (!history || !scrollbar) + throw new Error('Expected the chat history viewport and its custom scrollbar.') + + await vi.waitFor(() => { + expect(getComputedStyle(history).paddingBottom).toBe('16px') + expect(history.scrollHeight).toBeGreaterThan(history.clientHeight) + }) + + const layoutRect = layout.getBoundingClientRect() + const historyRect = historyLayer.getBoundingClientRect() + const composerRect = composer.getBoundingClientRect() + expect(historyRect.top).toBe(layoutRect.top) + expect(historyRect.right).toBe(layoutRect.right) + expect(historyRect.bottom).toBe(composerRect.top) + expect(getComputedStyle(history).borderRadius).toBe('0px') + + const scrollbarRect = scrollbar.getBoundingClientRect() + expect(scrollbarRect.top).toBe(historyRect.top) + expect(scrollbarRect.right).toBe(historyRect.right) + expect(scrollbarRect.bottom).toBe(historyRect.bottom) + expect(layoutRect.right - composerRect.right).toBe(16) + + const composerTop = composer.getBoundingClientRect().top + history.scrollTop = 120 + history.dispatchEvent(new Event('scroll')) + expect(composer.getBoundingClientRect().top).toBe(composerTop) + }) +}) diff --git a/apps/stage-tamagotchi/src/renderer/components/chat-viewport-layout.vue b/apps/stage-tamagotchi/src/renderer/components/chat-viewport-layout.vue new file mode 100644 index 000000000..d59439079 --- /dev/null +++ b/apps/stage-tamagotchi/src/renderer/components/chat-viewport-layout.vue @@ -0,0 +1,57 @@ + + + diff --git a/apps/stage-tamagotchi/src/renderer/pages/about.vue b/apps/stage-tamagotchi/src/renderer/pages/about.vue index 0e6e7d945..68dff6190 100644 --- a/apps/stage-tamagotchi/src/renderer/pages/about.vue +++ b/apps/stage-tamagotchi/src/renderer/pages/about.vue @@ -8,7 +8,7 @@ import semver from 'semver' import { useElectronAutoUpdater, useElectronEventaInvoke } from '@proj-airi/electron-vueuse' import { AboutContent, BugReportDialog, createBugReportPageContext, MarkdownRenderer } from '@proj-airi/stage-ui/components' import { useAnalytics, useBreakpoints, useBuildInfo } from '@proj-airi/stage-ui/composables' -import { Button, ContainerError, DoubleCheckButton, FieldSelect, Progress } from '@proj-airi/ui' +import { Button, ContainerError, DoubleCheckButton, FieldSelect, Progress, ScrollableArea } from '@proj-airi/ui' import { useClipboard } from '@vueuse/core' import { DialogContent, DialogDescription, DialogOverlay, DialogPortal, DialogRoot, DialogTitle } from 'reka-ui' import { DrawerContent, DrawerDescription, DrawerHandle, DrawerOverlay, DrawerPortal, DrawerRoot, DrawerTitle } from 'vaul-vue' @@ -417,9 +417,13 @@ onMounted(() => { {{ t('tamagotchi.stage.about.update.dialog.description', { version: updateState.info?.version }) }} -
+ -
+
-
-
-
- Generated Prompt + +
+
+
+ Generated Prompt +
+
+ {{ currentImage?.prompt || prompt || 'No prompt available for this frame.' }} +
-
- {{ currentImage?.prompt || prompt || 'No prompt available for this frame.' }} -
-
-
-
-
- Remix ID +
+
+
+ Remix ID +
+
+ #{{ currentImage?.remixId || remixId || '000000' }} +
-
- #{{ currentImage?.remixId || remixId || '000000' }} -
-
-
-
- Time -
-
- {{ renderTime || '--.--s' }} +
+
+ Time +
+
+ {{ renderTime || '--.--s' }} +
-
+
+
diff --git a/docs/ai/context/ui-components.md b/docs/ai/context/ui-components.md index 8ebeeac20..0badbe8ad 100644 --- a/docs/ai/context/ui-components.md +++ b/docs/ai/context/ui-components.md @@ -79,6 +79,21 @@ Responsive screen component that calculates canvas dimensions based on breakpoin **Props**: None | **Slots**: `default({ width, height })` +### ScrollableArea + +Reka UI scroll area with shared light-mode and dark-mode scrollbar styles. +The component forwards HTML attributes to the scroll-area root. + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `contentAsChild` | `boolean?` | `false` | Render the viewport content wrapper through the single default slot child | +| `orientation` | `'vertical' \| 'horizontal' \| 'both'` | `'vertical'` | Scrollbar orientations to render | +| `type` | `ScrollAreaRootProps['type']?` | `'auto'` | Reka UI scrollbar visibility behavior | +| `viewportClass` | `string \| string[]?` | — | Classes for the Reka UI viewport | + +**Slots**: `default` +**Exposed**: `viewport` (the native scroll owner. Reka UI hides its native scrollbar and renders the configured custom track.) + ### Skeleton Loading placeholder with animation. diff --git a/packages/stage-pages/src/pages/settings/models/index.vue b/packages/stage-pages/src/pages/settings/models/index.vue index d3adf830d..b5682a331 100644 --- a/packages/stage-pages/src/pages/settings/models/index.vue +++ b/packages/stage-pages/src/pages/settings/models/index.vue @@ -30,7 +30,7 @@ async function extractColorsFromModel() {
-
-
-
- {{ node.title }} - ({{ node.type }}) -
-
-
+
{{ t('settings.pages.providers.provider.comfyui.settings.upload.fields_exposed', { count: totalExposed }) }} diff --git a/packages/stage-ui/src/components/data-pane/pane.vue b/packages/stage-ui/src/components/data-pane/pane.vue index 4823c6cab..05e3b5960 100644 --- a/packages/stage-ui/src/components/data-pane/pane.vue +++ b/packages/stage-ui/src/components/data-pane/pane.vue @@ -1,11 +1,13 @@ diff --git a/packages/stage-ui/src/components/scenarios/chat/JournalPreviewModal.vue b/packages/stage-ui/src/components/scenarios/chat/JournalPreviewModal.vue index 38dc47101..7a4aa22ee 100644 --- a/packages/stage-ui/src/components/scenarios/chat/JournalPreviewModal.vue +++ b/packages/stage-ui/src/components/scenarios/chat/JournalPreviewModal.vue @@ -1,4 +1,5 @@ + + + + diff --git a/packages/stage-ui/src/components/scenarios/chat/components/history.browser.test.ts b/packages/stage-ui/src/components/scenarios/chat/components/history.browser.test.ts index 1602e89bb..87c42ca62 100644 --- a/packages/stage-ui/src/components/scenarios/chat/components/history.browser.test.ts +++ b/packages/stage-ui/src/components/scenarios/chat/components/history.browser.test.ts @@ -19,6 +19,81 @@ function createEnglishI18n() { } describe('chat history', () => { + // ROOT CAUSE: + // + // The desktop chat needs the styled Reka viewport, but forcing its track to + // stay mounted leaves an inert scrollbar visible when short content cannot scroll. + // Reka's automatic visibility must own the track without changing the viewport. + it('uses a Reka viewport without showing a scrollbar for a short desktop chat', async () => { + const screen = await render(ChatHistory, { + props: { + messages: [{ id: 'user-1', role: 'user', content: 'Hello' }], + style: 'height: 240px; width: 320px; overflow-y: auto;', + }, + global: { + plugins: [createEnglishI18n()], + }, + }) + + await vi.waitFor(() => { + expect(screen.container.querySelector('.chat-history-list')).not.toBeNull() + }) + + const history = screen.container.querySelector('.chat-history-list') + expect(history).not.toBeNull() + if (!history) + throw new Error('Expected a chat history viewport.') + + const track = screen.container.querySelector('.scrollable-area-scrollbar--vertical') + expect(history.matches('[data-reka-scroll-area-viewport]')).toBe(true) + expect(track === null || track.dataset.state === 'hidden').toBe(true) + }) + + it('virtualizes long desktop history inside one Reka viewport', async () => { + const messages: ChatHistoryItem[] = Array.from({ length: 100 }, (_, index) => ({ + id: `desktop-user-${index}`, + role: 'user', + content: `Desktop message ${index} `.repeat(index % 6 + 1), + createdAt: index, + })) + + const screen = await render(ChatHistory, { + props: { + messages, + style: 'height: 240px; width: 320px;', + }, + global: { + plugins: [createEnglishI18n()], + }, + }) + + await vi.waitFor(() => { + const renderedMessages = screen.container.querySelectorAll('.chat-message-item') + expect(renderedMessages.length).toBeGreaterThan(0) + expect(renderedMessages.length).toBeLessThan(messages.length) + }) + + const history = screen.container.querySelector('.chat-history-list') + expect(history).not.toBeNull() + if (!history) + throw new Error('Expected a desktop chat history viewport.') + + expect(history.matches('[data-reka-scroll-area-viewport]')).toBe(true) + expect(screen.container.querySelectorAll('.chat-history-list')).toHaveLength(1) + await vi.waitFor(() => { + expect(screen.container.querySelector('.scrollable-area-scrollbar--vertical')).not.toBeNull() + expect(history.scrollHeight).toBeGreaterThan(history.clientHeight) + expect(screen.container.textContent).toContain('Desktop message 99') + }) + + history.scrollTop = 0 + history.dispatchEvent(new Event('scroll')) + + await vi.waitFor(() => { + expect(screen.container.textContent).toContain('Desktop message 0') + }) + }) + // ROOT CAUSE: // // Virtua keeps its internal content root at least as tall as the viewport, but @@ -112,6 +187,14 @@ describe('chat history', () => { if (!history) throw new Error('Expected a chat history viewport.') + expect(history.matches('[data-reka-scroll-area-viewport]')).toBe(false) + expect(screen.container.querySelectorAll('.chat-history-list')).toHaveLength(1) + await vi.waitFor(() => { + expect(history.scrollHeight).toBeGreaterThan(history.clientHeight) + }) + expect(getComputedStyle(history).overflowY).toBe('auto') + expect(screen.container.querySelector('.scrollable-area-scrollbar--vertical')).toBeNull() + history.scrollTop = 0 history.dispatchEvent(new Event('scroll')) diff --git a/packages/stage-ui/src/components/scenarios/chat/components/history.vue b/packages/stage-ui/src/components/scenarios/chat/components/history.vue index dbc33e1c9..bba2a332c 100644 --- a/packages/stage-ui/src/components/scenarios/chat/components/history.vue +++ b/packages/stage-ui/src/components/scenarios/chat/components/history.vue @@ -9,6 +9,7 @@ import { computed, useTemplateRef } from 'vue' import { useI18n } from 'vue-i18n' import ChatAssistantItem from './assistant-item.vue' +import ChatHistoryScrollContainer from './chat-history-scroll-container.vue' import ChatErrorItem from './error-item.vue' import ChatHistoryMessageFrame from './history-message-frame.vue' import ChatUserItem from './user-item.vue' @@ -18,6 +19,10 @@ import { useChatHistoryTopFade } from '../composables/use-chat-history-top-fade' import { useVirtualizerBottomAlignment, useVirtualizerScroll } from '../composables/use-virtualizer-scroll' import { getChatHistoryItemKey } from '../utils' +defineOptions({ + inheritAttrs: false, +}) + const props = withDefaults(defineProps<{ messages: ChatHistoryItem[] streamingMessage?: StreamingAssistantMessage @@ -44,7 +49,8 @@ const emit = defineEmits<{ /** Keeps about two mobile viewports ready so fast flicks do not expose an unmounted gap. */ const CHAT_HISTORY_OVERSCAN = 600 -const chatHistoryRef = useTemplateRef('chatHistory') +const scrollContainerRef = useTemplateRef>('scroll-container') +const chatHistoryRef = computed(() => scrollContainerRef.value?.viewport ?? null) const virtualizerRef = useTemplateRef('virtualizer') const { scrollToIndex } = useVirtualizerScroll(virtualizerRef) @@ -134,20 +140,17 @@ function emitToolCallRerun( - - diff --git a/packages/stage-ui/src/components/scenarios/chat/components/scrollable-area.browser.test.ts b/packages/stage-ui/src/components/scenarios/chat/components/scrollable-area.browser.test.ts new file mode 100644 index 000000000..429d9b2a6 --- /dev/null +++ b/packages/stage-ui/src/components/scenarios/chat/components/scrollable-area.browser.test.ts @@ -0,0 +1,106 @@ +import { ScrollableArea } from '@proj-airi/ui' +import { describe, expect, it } from 'vitest' +import { render } from 'vitest-browser-vue' +import { defineComponent, ref, shallowRef } from 'vue' + +describe('shared scrollable area', () => { + it('exposes the Reka viewport and renders the requested scrollbar', async () => { + const area = shallowRef>() + const TestHost = defineComponent({ + components: { ScrollableArea }, + setup: () => ({ area }), + template: ` + +
Scrollable content
+
+ `, + }) + + const screen = await render(TestHost) + const viewport = screen.container.querySelector('[data-reka-scroll-area-viewport]') + + expect(viewport).not.toBeNull() + expect(viewport?.clientHeight).toBe(120) + expect(screen.container.querySelector('[data-orientation="vertical"]')).not.toBeNull() + expect(area.value?.viewport).toBe(viewport) + }) + + it('renders both scrollbar orientations around the same viewport', async () => { + const TestHost = defineComponent({ + components: { ScrollableArea }, + template: ` + +
Two-axis content
+
+ `, + }) + + const screen = await render(TestHost) + const viewport = screen.container.querySelector('[data-reka-scroll-area-viewport]') + const verticalTrack = screen.container.querySelector('[data-orientation="vertical"]') + const horizontalTrack = screen.container.querySelector('[data-orientation="horizontal"]') + const verticalThumb = verticalTrack?.firstElementChild as HTMLElement | null + const horizontalThumb = horizontalTrack?.firstElementChild as HTMLElement | null + + expect(viewport).not.toBeNull() + expect(verticalThumb?.getBoundingClientRect().width).toBeGreaterThan(0) + expect(verticalThumb?.getBoundingClientRect().height).toBeGreaterThan(0) + expect(horizontalThumb?.getBoundingClientRect().width).toBeGreaterThan(0) + expect(horizontalThumb?.getBoundingClientRect().height).toBeGreaterThan(0) + expect(viewport?.textContent).toContain('Two-axis content') + }) + + // ROOT CAUSE: + // Reka UI 2.10.3 clears both enabled-axis flags when either scrollbar unmounts. + // Switching away from `both` therefore leaves the surviving axis disabled. + // We restore the requested flags after Reka finishes the scrollbar DOM update. + // Report: https://github.com/moeru-ai/airi/pull/2399#discussion_r3886316598 + it('keeps the remaining axis scrollable when orientation changes', async () => { + const orientation = ref<'vertical' | 'horizontal' | 'both'>('both') + const TestHost = defineComponent({ + components: { ScrollableArea }, + setup: () => ({ orientation }), + template: ` + +
Two-axis content
+
+ `, + }) + + const screen = await render(TestHost) + const viewport = screen.container.querySelector('[data-reka-scroll-area-viewport]') + + orientation.value = 'vertical' + await expect.poll(() => viewport?.style.overflowY).toBe('scroll') + expect(viewport?.style.overflowX).toBe('hidden') + expect(screen.container.querySelector('[data-orientation="vertical"]')).not.toBeNull() + expect(screen.container.querySelector('[data-orientation="horizontal"]')).toBeNull() + viewport!.scrollTop = 40 + expect(viewport?.scrollTop).toBe(40) + + orientation.value = 'horizontal' + await expect.poll(() => viewport?.style.overflowX).toBe('scroll') + expect(viewport?.style.overflowY).toBe('hidden') + expect(screen.container.querySelector('[data-orientation="vertical"]')).toBeNull() + expect(screen.container.querySelector('[data-orientation="horizontal"]')).not.toBeNull() + viewport!.scrollLeft = 40 + expect(viewport?.scrollLeft).toBe(40) + }) + + it('keeps max-height panels scrollable without a fixed height', async () => { + const TestHost = defineComponent({ + components: { ScrollableArea }, + template: ` + +
Tall preview
+
+ `, + }) + + const screen = await render(TestHost) + const viewport = screen.container.querySelector('[data-reka-scroll-area-viewport]') + + expect(viewport?.clientHeight).toBe(120) + expect(viewport?.scrollHeight).toBe(240) + }) +}) diff --git a/packages/stage-ui/src/components/scenarios/chat/components/sessions-dialog.browser.test.ts b/packages/stage-ui/src/components/scenarios/chat/components/sessions-dialog.browser.test.ts index 85f6cbdba..21167397a 100644 --- a/packages/stage-ui/src/components/scenarios/chat/components/sessions-dialog.browser.test.ts +++ b/packages/stage-ui/src/components/scenarios/chat/components/sessions-dialog.browser.test.ts @@ -39,7 +39,10 @@ function sessionMeta(sessionId: string, updatedAt: number): ChatSessionMeta { } } -function createHarness() { +function createHarness(rows = [ + { meta: sessionMeta('session-one', 2), preview: 'First chat', isActive: true, updatedAtLabel: 'now' }, + { meta: sessionMeta('session-two', 1), preview: 'Second chat', isActive: false, updatedAtLabel: 'yesterday' }, +]) { return defineComponent({ name: 'SessionsDialogHarness', components: { SessionsDialog }, @@ -52,10 +55,7 @@ function createHarness() { created, deleted, selected, - rows: [ - { meta: sessionMeta('session-one', 2), preview: 'First chat', isActive: true, updatedAtLabel: 'now' }, - { meta: sessionMeta('session-two', 1), preview: 'Second chat', isActive: false, updatedAtLabel: 'yesterday' }, - ], + rows, } }, template: ` @@ -77,6 +77,30 @@ function createHarness() { } describe('sessions dialog actions', () => { + it('constrains long mobile session lists to a scrollable viewport', async () => { + const rows = Array.from({ length: 30 }, (_, index) => ({ + meta: sessionMeta(`session-${index}`, 30 - index), + preview: `Chat ${index}`, + isActive: index === 0, + updatedAtLabel: 'now', + })) + + await render(createHarness(rows), { + global: { + plugins: [createTestI18n()], + }, + }) + + const viewport = document.querySelector('[data-reka-scroll-area-viewport]') + + expect(viewport).not.toBeNull() + await expect.poll(() => viewport?.clientHeight ?? 0).toBeLessThan(viewport?.scrollHeight ?? 0) + await expect.poll(() => document.querySelector('.scrollable-area-scrollbar--vertical')).not.toBeNull() + + viewport!.scrollTop = 120 + expect(viewport?.scrollTop).toBe(120) + }) + // https://github.com/moeru-ai/airi/issues/2085 it('keeps add, switch, and delete actions independent for Issue #2085', async () => { // ROOT CAUSE: @@ -91,6 +115,8 @@ describe('sessions dialog actions', () => { }, }) + expect(document.querySelector('[data-reka-scroll-area-viewport]')).not.toBeNull() + await screen.getByRole('button', { name: 'New chat' }).click() await expect.element(screen.getByLabelText('created-session-count')).toHaveTextContent('1') diff --git a/packages/stage-ui/src/components/scenarios/chat/components/sessions-dialog.vue b/packages/stage-ui/src/components/scenarios/chat/components/sessions-dialog.vue index 091a9cb76..adc15f908 100644 --- a/packages/stage-ui/src/components/scenarios/chat/components/sessions-dialog.vue +++ b/packages/stage-ui/src/components/scenarios/chat/components/sessions-dialog.vue @@ -1,6 +1,7 @@ diff --git a/packages/ui/src/components/layouts/index.ts b/packages/ui/src/components/layouts/index.ts index 3a28422b5..eaeb62d8e 100644 --- a/packages/ui/src/components/layouts/index.ts +++ b/packages/ui/src/components/layouts/index.ts @@ -1,4 +1,5 @@ export { default as Collapsible } from './collapsible.vue' export { default as Screen } from './screen.vue' +export { default as ScrollableArea } from './scrollable-area.vue' export { default as Skeleton } from './skeleton.vue' export { default as Truncatable } from './truncatable.vue' diff --git a/packages/ui/src/components/layouts/scrollable-area.vue b/packages/ui/src/components/layouts/scrollable-area.vue new file mode 100644 index 000000000..bddaeb1a5 --- /dev/null +++ b/packages/ui/src/components/layouts/scrollable-area.vue @@ -0,0 +1,147 @@ + + + diff --git a/packages/ui/src/components/misc/container-error.vue b/packages/ui/src/components/misc/container-error.vue index fd269020b..1f4dc2677 100644 --- a/packages/ui/src/components/misc/container-error.vue +++ b/packages/ui/src/components/misc/container-error.vue @@ -1,8 +1,8 @@