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:08:29 UTC
(maka) branch fix/model-wheel-in-place created (now 3f9792d9ad)
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a change to branch fix/model-wheel-in-place
in repository https://gitbox.apache.org/repos/asf/maka.git
at 3f9792d9ad fix(ui): expand model wheels in place without empty ends
This branch includes the following new commits:
new 3f9792d9ad fix(ui): expand model wheels in place without empty ends
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org
(maka) 01/01: fix(ui): expand model wheels in place without empty ends
Posted by wy...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch fix/model-wheel-in-place
in repository https://gitbox.apache.org/repos/asf/maka.git
commit 3f9792d9ad6070b9fed19baceca5673094110360
Author: M4n5ter <m4...@gmail.com>
AuthorDate: Mon Sep 14 22:07:11 2026 +0800
fix(ui): expand model wheels in place without empty ends
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