You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/09/14 14:23:16 UTC

(maka) branch main updated: fix(ui): expand model wheels in place without empty ends (#5305)

This is an automated email from the ASF dual-hosted git repository.

M4n5ter pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/main by this push:
     new c22768c3b0 fix(ui): expand model wheels in place without empty ends (#5305)
c22768c3b0 is described below

commit c22768c3b0dc47518f6f8584e864f86f0b1e5379
Author: Wang <m4...@gmail.com>
AuthorDate: Mon Sep 14 22:23:11 2026 +0800

    fix(ui): expand model wheels in place without empty ends (#5305)
    
    Generated-by: Codex
---
 apps/desktop/e2e-budget.json                       |  2 +-
 apps/desktop/e2e/workhub-layout.spec.ts            | 19 ++++----
 apps/desktop/src/renderer/styles/workhub.css       |  9 ++--
 apps/desktop/stories/workhub.stories.tsx           | 15 ++++---
 .../ui/src/__tests__/model-wheel-picker.test.tsx   | 12 +++--
 packages/ui/src/model-wheel-picker.tsx             | 52 ++++++++++++++--------
 packages/ui/src/styles.css                         |  6 +--
 packages/ui/stories/model-picker.stories.tsx       | 13 ++++++
 8 files changed, 80 insertions(+), 48 deletions(-)

diff --git a/apps/desktop/e2e-budget.json b/apps/desktop/e2e-budget.json
index a86076bed8..bb6b7f6561 100644
--- a/apps/desktop/e2e-budget.json
+++ b/apps/desktop/e2e-budget.json
@@ -79,7 +79,7 @@
     },
     "workhub-layout.spec.ts": {
       "tests": 2,
-      "electron": "WorkHub model configuration and attachment sending cross the Host boundary; the compact native window grows to contain the model overlay without moving composer controls on screen, and pointer dragging scrolls the no-drag wheel instead of moving the frameless window; native floating preserves macOS Dock visibility; a held-open Host Turn verifies immediate prompt visibility, durable transcript handoff and persistence after renderer reload"
+      "electron": "WorkHub model configuration and attachment sending cross the Host boundary; the compact native window grows only with the inline composer wheel and keeps its bottom anchor, and pointer dragging scrolls the no-drag wheel instead of moving the frameless window; native floating preserves macOS Dock visibility; a held-open Host Turn verifies immediate prompt visibility, durable transcript handoff and persistence after renderer reload"
     },
     "workhub-reconstruction.spec.ts": {
       "tests": 1,
diff --git a/apps/desktop/e2e/workhub-layout.spec.ts b/apps/desktop/e2e/workhub-layout.spec.ts
index 15158441d6..ed953639a8 100644
--- a/apps/desktop/e2e/workhub-layout.spec.ts
+++ b/apps/desktop/e2e/workhub-layout.spec.ts
@@ -20,10 +20,10 @@
 import { FAKE_HOLD_OPEN_PROMPT } from '@maka/runtime/test-only/fake-backend';
 import { awaitSendReady, COMPOSER_INPUT, expect, test, getWorkHubPage } from './fixtures';
 
-// The compact native window must contain the top-layer wheel while keeping
-// composer controls fixed on screen; native dragging must scroll the no-drag
+// The compact native window must contain the inline wheel and retain its
+// bottom anchor; native dragging must scroll the no-drag
 // wheel instead of moving the frameless window. A browser has neither boundary.
-test('WorkHub uses its coordination model and shared attachment composer', async ({ sessionLocalWindow: { page, app } }) => {
+test('WorkHub uses its coordination model and shared attachment composer', async ({ sessionLocalWindow: { page, app } }, testInfo) => {
   await page.evaluate(async () => {
     const { connections } = await window.maka.connections.getSnapshot();
     const connection = connections.find((entry) => entry.slug === 'e2e')!;
@@ -201,22 +201,22 @@ test('WorkHub uses its coordination model and shared attachment composer', async
       return { x: x + rect.x, y: y + rect.y, width: rect.width, height: rect.height };
     }), origin);
   };
-  const compactControls = await screenLayout();
   await model.click();
   const wheel = workhub.getByRole('listbox');
   await expect(wheel).toBeVisible();
   await expect(wheel.getByRole('option')).toHaveCount(3);
   const modelChoices = await workhub.evaluate(async (id) => (await window.maka.connections.getSnapshot(id)).chatModelChoices, sessionId);
-  await expect(wheel.locator('.maka-model-wheel-label')).toHaveText(modelChoices.map((choice) => choice.label));
+  await expect(wheel.getByRole('option').locator('.maka-model-wheel-label')).toHaveText(modelChoices.map((choice) => choice.label));
   await expect(workhub.locator('.workHubHistory')).toBeHidden();
   await expect.poll(() => workhub.evaluate(() => innerHeight)).toBeGreaterThan(compactHeight);
   await expect.poll(floatingBottom).toBe(anchoredBottom);
-  await expect.poll(screenLayout).toEqual(compactControls);
+  await expect.poll(() => workhub.evaluate(() => innerHeight)).toBeLessThanOrEqual(compactHeight + 132);
   const expectWheelInsideWindow = () => expect.poll(() => wheel.evaluate((element) => {
     const rect = element.getBoundingClientRect();
     return rect.top >= 0 && rect.left >= 0 && rect.bottom <= innerHeight && rect.right <= innerWidth;
   })).toBe(true);
   await expectWheelInsideWindow();
+  await workhub.screenshot({ path: testInfo.outputPath('floating-composer-wheel.png') });
   const expectSnappedSelection = async (index: number) => {
     const choice = modelChoices[index]!;
     const option = wheel.getByRole('option').nth(index);
@@ -229,7 +229,7 @@ test('WorkHub uses its coordination model and shared attachment composer', async
     }, sessionId)).toEqual({ connectionId: choice.connectionId, model: choice.model });
     return choice;
   };
-  const initialIndex = Math.round(await wheel.evaluate((element) => element.scrollTop) / 44);
+  const initialIndex = await wheel.getByRole('option').evaluateAll((options) => options.findIndex((option) => option.getAttribute('aria-selected') === 'true'));
   const scrollDirection = initialIndex < modelChoices.length - 1 ? 1 : -1;
   await wheel.hover();
   await workhub.mouse.wheel(0, scrollDirection * 30);
@@ -251,11 +251,10 @@ test('WorkHub uses its coordination model and shared attachment composer', async
   await workhub.mouse.up();
   await expect.poll(screenLayout).toEqual(beforeDrag);
   await expect(wheel).toBeVisible();
-  await expect.poll(() => wheel.evaluate((element) => element.scrollTop)).toBe(Math.round((dragInitialTop - dragDistance) / 44) * 44);
-  const draggedIndex = Math.round((dragInitialTop - dragDistance) / 44);
+  const draggedIndex = Math.round((dragInitialTop - dragDistance + 44) / 44) % modelChoices.length;
   await expectSnappedSelection(draggedIndex);
   await wheel.press('ArrowDown');
-  await expectSnappedSelection(Math.min(draggedIndex + 1, modelChoices.length - 1));
+  await expectSnappedSelection((draggedIndex + 1) % modelChoices.length);
   await wheel.press('Escape');
   await expect(wheel).toHaveCount(0);
   await expect(model).toBeFocused();
diff --git a/apps/desktop/src/renderer/styles/workhub.css b/apps/desktop/src/renderer/styles/workhub.css
index 05d5b7e2db..a05497c757 100644
--- a/apps/desktop/src/renderer/styles/workhub.css
+++ b/apps/desktop/src/renderer/styles/workhub.css
@@ -156,11 +156,10 @@
   .workHubLiveError { padding: var(--space-3); }
   .workHubComposerSurface { position: relative; width: 100%; }
   .workHubComposerContent { position: relative; }
-  /* Top-layer content still needs room inside the native window. Measuring
-     this leading space grows the bottom-anchored window upward, without
-     changing the composer's layout or its controls' screen positions. */
-  .workHubLive[data-placement='floating'][data-conversation-expanded='false'] .workHubComposerSurface:has(.maka-model-wheel-anchor[data-open='true'])::before {
-    content: ''; display: block; height: 136px;
+  /* Compact WorkHub keeps its original inline wheel footprint. Other hosts
+     keep the button footprint and paint the same wheel over that position. */
+  .workHubLive[data-placement='floating'][data-conversation-expanded='false'] .maka-model-wheel-anchor[data-open='true'] {
+    width: min(264px, 55vw); height: var(--maka-model-wheel-height);
   }
   .workHubComposerActions { display: flex; align-items: center; gap: 2px; margin-left: auto; }
   .workHubRevealMark { position: absolute; inset: 32px 0 calc(var(--workhub-composer-height, 96px) + 12px); display: grid; place-items: center; z-index: 3; color: var(--foreground); opacity: 0; pointer-events: none; }
diff --git a/apps/desktop/stories/workhub.stories.tsx b/apps/desktop/stories/workhub.stories.tsx
index ea833934e1..eed5ef63c5 100644
--- a/apps/desktop/stories/workhub.stories.tsx
+++ b/apps/desktop/stories/workhub.stories.tsx
@@ -189,10 +189,15 @@ export const StandardComposer: Story = {
       return { x, y, width, height };
     });
     const before = layout();
+    const anchor = trigger.getBoundingClientRect();
     await userEvent.click(trigger);
     const wheel = canvas.getByRole('listbox', { name: /切换当前任务模型/ });
     await waitFor(() => expect(wheel).toHaveFocus());
     expect(layout()).toEqual(before);
+    expect(trigger).not.toBeVisible();
+    const expanded = wheel.getBoundingClientRect();
+    const center = Math.max(expanded.height / 2, Math.min(innerHeight - expanded.height / 2, (anchor.top + anchor.bottom) / 2));
+    expect(Math.abs((expanded.top + expanded.bottom) / 2 - center)).toBeLessThanOrEqual(1);
     await expect(within(wheel).getByRole('option', { name: /model-a/, selected: true })).toBeInTheDocument();
     await userEvent.keyboard('{End}');
     await waitFor(() => expect(writes.model).toHaveBeenCalledWith(sessionId, expect.objectContaining({ expectedRevision: 1, modelTarget: expect.objectContaining({ model: 'model-b' }) })));
@@ -212,11 +217,6 @@ export const StandardComposer: Story = {
     expect(layout()).toEqual(before);
     await userEvent.click(trigger);
     await waitFor(() => expect(canvas.getByRole('listbox')).toHaveFocus());
-    await userEvent.click(trigger);
-    await waitFor(() => expect(canvas.queryByRole('listbox')).not.toBeInTheDocument());
-    await waitFor(() => expect(trigger).toHaveFocus());
-    await userEvent.click(trigger);
-    await waitFor(() => expect(canvas.getByRole('listbox')).toHaveFocus());
     await userEvent.tab();
     await waitFor(() => expect(canvas.queryByRole('listbox')).not.toBeInTheDocument());
     expect(canvasElement.ownerDocument.activeElement).not.toBe(canvasElement.ownerDocument.body);
@@ -241,11 +241,12 @@ export const ProgressModelPicker: Story = {
     await userEvent.click(trigger);
     const wheel = await canvas.findByRole('listbox');
     await waitFor(() => expect(wheel).toHaveFocus());
-    const surface = wheel.closest('.maka-model-wheel-popup')!;
+    const surface = wheel.closest('.maka-model-wheel-expanded')!;
     const pixels = new OffscreenCanvas(1, 1).getContext('2d')!;
     pixels.fillStyle = getComputedStyle(surface).backgroundColor;
     pixels.fillRect(0, 0, 1, 1);
-    expect(pixels.getImageData(0, 0, 1, 1).data[3]).toBe(255);
+    expect(pixels.getImageData(0, 0, 1, 1).data[3]).toBeGreaterThanOrEqual(230);
+    expect(pixels.getImageData(0, 0, 1, 1).data[3]).toBeLessThan(255);
     expect(editor).toHaveTextContent('Keep this draft readable while choosing a model.');
   },
 };
diff --git a/packages/ui/src/__tests__/model-wheel-picker.test.tsx b/packages/ui/src/__tests__/model-wheel-picker.test.tsx
index 800ba3df4e..4d6233cd64 100644
--- a/packages/ui/src/__tests__/model-wheel-picker.test.tsx
+++ b/packages/ui/src/__tests__/model-wheel-picker.test.tsx
@@ -58,14 +58,14 @@ test('the wheel applies settled selection once and restores the saved model on f
   try {
     await act(() => root.render(<Harness />));
     const wheel = document.querySelector<HTMLElement>('[role="listbox"]')!;
-    assert.equal(wheel.scrollTop, 44, 'opening centers the current model');
+    assert.equal(wheel.scrollTop, 132, 'opening centers the current model');
     await act(() => wheel.dispatchEvent(event('scroll')));
     await settle();
     assert.deepEqual(calls, [], 'initial positioning never changes the model');
 
     await act(() => {
       wheel.dispatchEvent(event('wheel'));
-      wheel.scrollTop = 88;
+      wheel.scrollTop = 176;
       wheel.dispatchEvent(event('scroll'));
     });
     await settle();
@@ -85,12 +85,16 @@ test('the wheel applies settled selection once and restores the saved model on f
     await act(() => wheel.dispatchEvent(Object.assign(event('keydown'), { key: 'Home' })));
     assert.deepEqual(calls, ['C', 'A'], 'keyboard navigation also applies immediately');
     await act(async () => { finish?.(false); });
-    assert.equal(wheel.scrollTop, 88, 'a failed save recenters the authoritative value');
+    assert.equal(wheel.scrollTop, 176, 'a failed save recenters the authoritative value');
     assert.equal(wheel.querySelector('[aria-selected="true"]')?.textContent, 'C');
 
+    await act(() => wheel.dispatchEvent(Object.assign(event('keydown'), { key: 'ArrowDown' })));
+    assert.deepEqual(calls, ['C', 'A', 'A'], 'the last model wraps to its visible next neighbor');
+    await act(async () => { finish?.(true); });
+
     await act(() => root.render(<Harness disabled />));
     await act(() => wheel.dispatchEvent(Object.assign(event('keydown'), { key: 'Home' })));
-    assert.deepEqual(calls, ['C', 'A'], 'disabled navigation does not change the model');
+    assert.deepEqual(calls, ['C', 'A', 'A'], 'disabled navigation does not change the model');
   } finally {
     await act(() => root.unmount());
     Object.assign(globalThis, original);
diff --git a/packages/ui/src/model-wheel-picker.tsx b/packages/ui/src/model-wheel-picker.tsx
index 0c81d59d72..ef6bfb1870 100644
--- a/packages/ui/src/model-wheel-picker.tsx
+++ b/packages/ui/src/model-wheel-picker.tsx
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-import { type ReactNode, useId, useLayoutEffect, useRef, useState } from 'react';
+import { type CSSProperties, type ReactNode, useId, useLayoutEffect, useRef, useState } from 'react';
 import { Button, useLayer } from '@astryxdesign/core';
 import { Check, ICON_SIZE } from './icons.js';
 
@@ -29,7 +29,7 @@ export interface ModelWheelOption {
   disabled?: boolean;
 }
 
-/** Shared overlay picker: the model at the settled snap position takes effect. */
+/** The trigger becomes a wheel in place; the settled model takes effect. */
 export function ModelWheelPicker(props: {
   options: readonly ModelWheelOption[];
   value?: string;
@@ -48,6 +48,7 @@ export function ModelWheelPicker(props: {
   const trigger = useRef<HTMLButtonElement>(null);
   const anchor = useRef<HTMLSpanElement>(null);
   const open = props.open ?? internalOpen;
+  const height = ROW_HEIGHT * (props.options.length > 1 ? 3 : 1);
   const setOpen = (next: boolean) => {
     if (props.open === undefined) setInternalOpen(next);
     props.onOpenChange?.(next);
@@ -70,21 +71,26 @@ export function ModelWheelPicker(props: {
     element.style.width = `${element.getBoundingClientRect().width}px`;
     return () => { element.style.removeProperty('width'); };
   }, [open]);
-  return <span ref={anchor} className="maka-model-wheel-anchor" data-open={open}>
-    <Button ref={(element) => { trigger.current = element; layer.ref(element); }} type="button" variant="ghost" size={props.size ?? 'sm'}
+  return <span ref={(element) => { anchor.current = element; layer.ref(element); }}
+    className="maka-model-wheel-anchor" data-open={open}
+    style={{ '--maka-model-wheel-height': `${height}px` } as CSSProperties}>
+    <Button ref={trigger} type="button" variant="ghost" size={props.size ?? 'sm'}
     label={props.label} icon={props.icon} tooltip={open ? undefined : props.tooltip}
     isDisabled={props.disabled || props.options.length === 0}
     className={props.triggerClassName} aria-label={props.ariaLabel}
     aria-haspopup="listbox" aria-expanded={open} aria-controls={open ? layer.id : undefined}
-    onMouseDown={(event) => { if (open) event.preventDefault(); }}
-    onClick={() => open ? close(true) : setOpen(true)}
+    onClick={() => setOpen(true)}
     onKeyDown={(event) => {
       if (!props.disabled && props.options.length > 0 && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
         event.preventDefault(); event.stopPropagation(); setOpen(true);
       }
     }} />
     {layer.render(open && layer.isOpen ? <ModelWheel {...props} onClose={close} /> : null,
-      { placement: 'above', alignment: 'start', offset: 4, className: 'maka-model-wheel-popup' })}
+      { positioning: 'custom', className: 'maka-model-wheel-expanded', style: {
+        position: 'fixed', inset: 'auto',
+        top: `clamp(0px, calc(anchor(center) - ${height / 2}px), calc(100dvh - ${height}px))`,
+        left: 'clamp(0px, anchor(left), calc(100vw - min(264px, 55vw)))',
+      } })}
   </span>;
 }
 
@@ -101,12 +107,20 @@ function ModelWheel(props: Parameters<typeof ModelWheelPicker>[0] & {
   const saving = useRef(false);
   const [pending, setPending] = useState<string | null>(null);
   const currentValue = pending ?? props.value;
+  const count = props.options.length;
+  const height = ROW_HEIGHT * (count > 1 ? 3 : 1);
+  const inset = (height - ROW_HEIGHT) / 2;
+  // Neighboring copies let the first and last models have real neighbors,
+  // rather than empty padding. Only the middle copy is exposed to assistive technology.
+  const copies = count > 1 ? [0, 1, 2] : [1];
+  const centeredTop = (index: number) => (count > 1 ? count + index : 0) * ROW_HEIGHT - inset;
   const selectedIndex = Math.max(0, props.options.findIndex((option) => option.value === currentValue));
   const [preview, setPreview] = useState(selectedIndex);
   const latest = useRef(props);
   latest.current = props;
   const disabled = props.disabled || pending !== null;
-  const indexAt = (element: HTMLDivElement) => Math.max(0, Math.min(latest.current.options.length - 1, Math.round(element.scrollTop / ROW_HEIGHT)));
+  const rowAt = (element: HTMLDivElement) => Math.round((element.scrollTop + inset) / ROW_HEIGHT);
+  const indexAt = (element: HTMLDivElement) => count ? ((rowAt(element) % count) + count) % count : 0;
   const clearTimer = () => clearTimeout(timer.current);
   const pick = async (index: number) => {
     const option = latest.current.options[index];
@@ -122,15 +136,16 @@ function ModelWheel(props: Parameters<typeof ModelWheelPicker>[0] & {
     clearTimer();
     if (!interacted.current || element.dataset.dragging || saving.current) return;
     const index = indexAt(element);
-    if (Math.abs(element.scrollTop - index * ROW_HEIGHT) > 1) return;
+    if (Math.abs(element.scrollTop - (rowAt(element) * ROW_HEIGHT - inset)) > 1) return;
     interacted.current = false;
+    element.scrollTop = centeredTop(index);
     void pick(index);
   };
   const moveTo = (element: HTMLDivElement, index: number) => {
     clearTimer();
     interacted.current = false;
-    const next = Math.max(0, Math.min(latest.current.options.length - 1, index));
-    element.scrollTop = next * ROW_HEIGHT;
+    const next = count ? ((index % count) + count) % count : 0;
+    element.scrollTop = centeredTop(next);
     setPreview(next);
     void pick(next);
   };
@@ -138,8 +153,8 @@ function ModelWheel(props: Parameters<typeof ModelWheelPicker>[0] & {
     if (!element.dataset.dragging) return;
     delete element.dataset.dragging;
     // Keep `moved` until the synthetic click has been suppressed.
-    const next = indexAt(element);
-    element.scrollTo({ top: next * ROW_HEIGHT, behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'instant' : 'smooth' });
+    const next = rowAt(element);
+    element.scrollTo({ top: next * ROW_HEIGHT - inset, behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'instant' : 'smooth' });
     clearTimer();
     timer.current = setTimeout(() => settle(element), 180);
   };
@@ -147,7 +162,7 @@ function ModelWheel(props: Parameters<typeof ModelWheelPicker>[0] & {
     if (!viewport.current) return;
     interacted.current = false;
     clearTimer();
-    viewport.current.scrollTop = selectedIndex * ROW_HEIGHT;
+    viewport.current.scrollTop = centeredTop(selectedIndex);
     setPreview(selectedIndex);
   }, [selectedIndex, pending, props.value, props.options.length]);
   useLayoutEffect(() => {
@@ -159,7 +174,7 @@ function ModelWheel(props: Parameters<typeof ModelWheelPicker>[0] & {
     <div ref={viewport} className="maka-model-wheel-viewport" role="listbox" tabIndex={0}
       aria-label={props.ariaLabel} aria-disabled={disabled} aria-busy={pending !== null}
       aria-activedescendant={props.options.length ? `${id}-${preview}` : undefined}
-      style={{ height: ROW_HEIGHT * 3, paddingBlock: ROW_HEIGHT }}
+      style={{ height }}
       onBlur={(event) => {
         if (event.currentTarget.contains(event.relatedTarget)) return;
         if (interacted.current) void pick(indexAt(event.currentTarget));
@@ -213,8 +228,9 @@ function ModelWheel(props: Parameters<typeof ModelWheelPicker>[0] & {
         if (next === undefined) return;
         event.preventDefault(); event.stopPropagation(); moveTo(event.currentTarget, next);
       }}>
-      {props.options.map((option, index) => <div key={option.value} id={`${id}-${index}`}
-        className="maka-model-wheel-option" role="option" aria-selected={option.value === currentValue}
+      {copies.flatMap((copy) => props.options.map((option, index) => <div key={`${copy}-${option.value}`} id={copy === 1 ? `${id}-${index}` : undefined}
+        className="maka-model-wheel-option" role={copy === 1 ? 'option' : undefined} aria-hidden={copy !== 1 || undefined}
+        aria-selected={copy === 1 ? option.value === currentValue : undefined}
         data-active={index === preview} aria-disabled={disabled || option.disabled}
         style={{ height: ROW_HEIGHT }} title={[option.label, option.heading, option.description].filter(Boolean).join(' · ')}
         onMouseDown={(event) => event.preventDefault()}
@@ -222,7 +238,7 @@ function ModelWheel(props: Parameters<typeof ModelWheelPicker>[0] & {
         <span className="maka-model-wheel-label">{option.label}</span>
         {option.heading && <span className="maka-model-wheel-provider">{option.heading}</span>}
         {option.value === currentValue && <span className="maka-model-wheel-check"><Check size={ICON_SIZE.control} aria-hidden="true" /></span>}
-      </div>)}
+      </div>))}
     </div>
   </div>;
 }
diff --git a/packages/ui/src/styles.css b/packages/ui/src/styles.css
index 1c72dc37fd..cc98577854 100644
--- a/packages/ui/src/styles.css
+++ b/packages/ui/src/styles.css
@@ -21,10 +21,10 @@
 
 .maka-model-wheel-anchor { display: inline-flex; min-width: 0; max-width: 100%; }
 .maka-model-wheel-anchor > button { min-width: 0; max-width: 100%; }
-.maka-model-wheel-anchor[data-open='true'] > button { width: 100%; }
-.maka-model-wheel-popup { border-radius: var(--radius-container); background: var(--background-elevated); box-shadow: var(--shadow-med); }
+.maka-model-wheel-anchor[data-open='true'] > button { visibility: hidden; }
+.maka-model-wheel-expanded { border-radius: 10px; background: color-mix(in oklch, var(--background-elevated) 94%, transparent); backdrop-filter: blur(12px); }
 .maka-model-wheel { position: relative; width: min(264px, 55vw); max-width: 100%; }
-.maka-model-wheel::before { content: ''; position: absolute; inset: 44px 0; border-radius: 10px; background: color-mix(in oklch, var(--foreground) 6%, transparent); pointer-events: none; }
+.maka-model-wheel::before { content: ''; position: absolute; inset: 50% 0 auto; height: 44px; translate: 0 -50%; border-radius: 10px; background: color-mix(in oklch, var(--foreground) 6%, transparent); pointer-events: none; }
 .maka-model-wheel-viewport { position: relative; box-sizing: border-box; overflow-y: auto; overscroll-behavior: contain; scroll-snap-type: y mandatory; scrollbar-width: none; mask-image: linear-gradient(transparent, black 32%, black 68%, transparent); outline: none; }
 .maka-model-wheel-viewport:focus-visible { outline: var(--focus-ring-width) solid var(--focus-ring); outline-offset: calc(-1 * var(--focus-ring-width)); }
 .maka-model-wheel-viewport[data-dragging='true'] { scroll-snap-type: none; cursor: grabbing; }
diff --git a/packages/ui/stories/model-picker.stories.tsx b/packages/ui/stories/model-picker.stories.tsx
index e801c397b0..ffd4221f5f 100644
--- a/packages/ui/stories/model-picker.stories.tsx
+++ b/packages/ui/stories/model-picker.stories.tsx
@@ -400,7 +400,20 @@ export const ManyConnections: Story = {
     await userEvent.click(trigger);
     const menu = within(document.body);
     await expect(await menu.findAllByRole('option')).toHaveLength(MANY_CHOICES.length);
+    const wheel = menu.getByRole('listbox');
+    const expectFilledEdges = async () => waitFor(() => {
+      const bounds = wheel.getBoundingClientRect();
+      const rows = Array.from(wheel.querySelectorAll('.maka-model-wheel-option'))
+        .map((row) => row.getBoundingClientRect());
+      for (const y of [bounds.top + 8, bounds.bottom - 8]) {
+        expect(rows.some((row) => row.top <= y && row.bottom >= y)).toBe(true);
+      }
+      const selected = wheel.querySelector('[aria-selected="true"]')!.getBoundingClientRect();
+      expect(Math.abs((selected.top + selected.bottom - bounds.top - bounds.bottom) / 2)).toBeLessThanOrEqual(1);
+    });
+    await expectFilledEdges();
     await userEvent.keyboard('{End}');
+    await expectFilledEdges();
     await expect(await menu.findByRole('option', { name: /vendor\/gamma/, selected: true })).toBeInTheDocument();
   },
 };


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org