Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions design-system/packages/design-tokens/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ box in `OverflowText`. The browser includes the selected font's ascent/descent;
existing numeric leading roles cannot express those platform-dependent metrics.
The outer line box retains any larger leading supplied by its owning control.

`layout.searchDialog` owns the shared Lab/product search composition: 800 × 460
when space permits, 20px inset and query-to-scope gap, and a 30px query row.
`layout.searchDialog` owns the shared Lab/product search composition: 744 × 460
when space permits (approximately the golden ratio), 20px inset and query-to-scope gap, and a 30px query row.
Only the query shell scopes `control.searchField.height.sm`; general Input and Button sizes
retain their defaults. Results scroll within the available viewport.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -750,7 +750,7 @@
"layout": {
"$type": "dimension",
"searchDialog": {
"width": { "$value": "{overlay.dialog.maxInlineSizeXlarge}" },
"width": { "$value": "744px" },
"height": { "$value": "460px" },
"padding": { "$value": "{space.5}" },
"queryHeight": { "$value": "30px" },
Expand Down
6 changes: 6 additions & 0 deletions design-system/packages/theme-openbitfun/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import { themeModes, themeTokenCatalog, themes } from "@openbitfun/theme-openbit

`themeTokenCatalog` exposes the complete public semantic theme contract for visual authoring: colors, elevation shadows, surface filters, and state opacity. UI components consume this semantic contract and never depend on reference colors directly.

`effect.blur.strong` provides 32px blur and 1.3 saturation for translucent floating
panels such as Global Search. The existing 12px `medium` filter leaves detailed
content too distinct at lower surface opacity; its smaller blur remains appropriate
for compact controls. The stronger role lets panels widen their blur radius
independently and retain opaque high-contrast and reduced-transparency fallbacks.

## Foundational color scales

Named numeric scales live below the semantic theme layer. Step numbers increase from light to dark, for example `ref.color.neutral.50`, `ref.color.blue.500`, and `ref.color.red.700`. Semantic tokens map those stable palette values to roles such as `color.content.primary` or `color.status.danger.content` independently in each theme mode.
Expand Down
3 changes: 2 additions & 1 deletion design-system/packages/theme-openbitfun/src/dark.tokens.json
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,8 @@
"blur": {
"subtle": { "$type": "filter", "$value": "blur(4px) saturate(1.05)" },
"base": { "$type": "filter", "$value": "blur(8px) saturate(1.1)" },
"medium": { "$type": "filter", "$value": "blur(12px) saturate(1.2)" }
"medium": { "$type": "filter", "$value": "blur(12px) saturate(1.2)" },
"strong": { "$description": "Wide-radius frosting for translucent floating panels over detailed content.", "$type": "filter", "$value": "blur(32px) saturate(1.3)" }
}
},
"opacity": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,8 @@
"blur": {
"subtle": { "$type": "filter", "$value": "blur(4px) saturate(1.02)" },
"base": { "$type": "filter", "$value": "blur(8px) saturate(1.05)" },
"medium": { "$type": "filter", "$value": "blur(12px) saturate(1.2)" }
"medium": { "$type": "filter", "$value": "blur(12px) saturate(1.2)" },
"strong": { "$description": "Wide-radius frosting for translucent floating panels over detailed content.", "$type": "filter", "$value": "blur(32px) saturate(1.3)" }
}
},
"opacity": {
Expand Down
4 changes: 3 additions & 1 deletion src/apps/data-tools/ui/generated/design-system.css
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@
--openbitfun-layout-search-dialog-query-height: 30px;
--openbitfun-layout-search-dialog-results-gap: 30px;
--openbitfun-layout-search-dialog-scope-gap: var(--openbitfun-space-5);
--openbitfun-layout-search-dialog-width: var(--openbitfun-overlay-dialog-max-inline-size-xlarge);
--openbitfun-layout-search-dialog-width: 744px;
--openbitfun-layout-spinner-matrix-cell-lg: 8px;
--openbitfun-layout-spinner-matrix-cell-md: 6px;
--openbitfun-layout-spinner-matrix-cell-sm: 4px;
Expand Down Expand Up @@ -917,6 +917,7 @@
--openbitfun-component-empty-media: color-mix(in srgb, #6a6a6a 35%, #f7f7f7);
--openbitfun-effect-blur-base: blur(8px) saturate(1.05);
--openbitfun-effect-blur-medium: blur(12px) saturate(1.2);
--openbitfun-effect-blur-strong: blur(32px) saturate(1.3);
--openbitfun-effect-blur-subtle: blur(4px) saturate(1.02);
--openbitfun-opacity-disabled: 0.55;
--openbitfun-opacity-focus: 0.9;
Expand Down Expand Up @@ -1101,6 +1102,7 @@
--openbitfun-component-empty-media: color-mix(in srgb, #858585 35%, #0e0e10);
--openbitfun-effect-blur-base: blur(8px) saturate(1.1);
--openbitfun-effect-blur-medium: blur(12px) saturate(1.2);
--openbitfun-effect-blur-strong: blur(32px) saturate(1.3);
--openbitfun-effect-blur-subtle: blur(4px) saturate(1.05);
--openbitfun-opacity-disabled: 0.6;
--openbitfun-opacity-focus: 0.9;
Expand Down
1 change: 1 addition & 0 deletions src/apps/desktop/src/generated/bootstrap_theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
--openbitfun-component-empty-media: color-mix(in srgb, #858585 35%, #0e0e10);
--openbitfun-effect-blur-base: blur(8px) saturate(1.1);
--openbitfun-effect-blur-medium: blur(12px) saturate(1.2);
--openbitfun-effect-blur-strong: blur(32px) saturate(1.3);
--openbitfun-effect-blur-subtle: blur(4px) saturate(1.05);
--openbitfun-opacity-disabled: 0.6;
--openbitfun-opacity-focus: 0.9;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1157,9 +1157,7 @@ const SessionsSection: React.FC<SessionsSectionProps> = ({
sourceView ? t('nav.sessions.removeSourceConfirmTitle') : t('nav.sessions.deleteConfirmTitle'),
sourceView ? t('nav.sessions.removeSourceConfirmMessage', {
name: session ? resolveSessionTitle(session) : t('nav.sessions.untitled'),
}) : t('nav.sessions.deleteConfirmMessage', {
name: session ? resolveSessionTitle(session) : t('nav.sessions.untitled'),
}),
}) : t('nav.sessions.deleteConfirmMessage'),
{ confirmText: sourceView ? t('nav.sessions.removeSource') : t('nav.sessions.delete') },
);
if (!confirmed) return;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('session action confirmation policy', () => {
expect(source).not.toContain('confirmWarning');
expect(deleteHandler).toContain('await confirmDanger(');
expect(deleteHandler).toContain("t('nav.sessions.deleteConfirmTitle')");
expect(deleteHandler).toContain("t('nav.sessions.deleteConfirmMessage', {");
expect(deleteHandler).toContain("t('nav.sessions.deleteConfirmMessage')");
expect(deleteHandler).toContain("{ confirmText: sourceView ? t('nav.sessions.removeSource') : t('nav.sessions.delete') }");
expect(deleteHandler.indexOf('await confirmDanger(')).toBeLessThan(
deleteHandler.indexOf('await flowChatManager.deleteChatSession(sessionId)'),
Expand Down
43 changes: 43 additions & 0 deletions src/web-ui/src/app/global-search/GlobalSearchContent.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,49 @@ describe('GlobalSearchContent accessibility', () => {
await act(async () => input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })));
expect(fixtures.activate).toHaveBeenCalledWith(snapshot.items[0].target, expect.anything());
});
it('renders app and entity search hits as actionable cards while keeping content hits in lists', async () => {
const cardItems: GlobalSearchSnapshot['items'] = [
snapshot.items[0],
{ id: 'setting', providerId: 'capabilities', group: 'settings', title: 'Appearance settings', score: 90,
target: { kind: 'capability', capabilityId: 'setting.appearance' } },
{ id: 'feature', providerId: 'capabilities', group: 'capabilities', title: 'Project features', score: 90,
target: { kind: 'capability', capabilityId: 'feature.projects' } },
{ id: 'workspace', providerId: 'entities', group: 'workspaces', title: 'Project workspace', score: 90,
target: { kind: 'workspace', workspaceId: 'workspace' } },
{ id: 'assistant', providerId: 'entities', group: 'assistants', title: 'Project assistant', score: 90,
target: { kind: 'assistant', workspaceId: 'assistant' } },
];
const contentItems: GlobalSearchSnapshot['items'] = [
{ id: 'message', providerId: 'sessions', group: 'messages', title: 'Message match', score: 90,
target: { kind: 'session', sessionId: 'session', workspaceId: 'workspace', workspacePath: '/project', turnId: 'turn' } },
{ id: 'session', providerId: 'sessions', group: 'sessions', title: 'Session match', score: 90,
target: { kind: 'session', sessionId: 'session', workspaceId: 'workspace', workspacePath: '/project' } },
{ id: 'file', providerId: 'files', group: 'files', title: 'File match', score: 90,
target: { kind: 'file', workspaceId: 'workspace', workspacePath: '/project', filePath: '/project/notes.md', fileName: 'notes.md' } },
];
fixtures.search.mockImplementationOnce((_providers, _request, _signal, onUpdate) =>
onUpdate({ ...snapshot, items: [...cardItems, ...contentItems] }));
useGlobalSearchStore.getState().openSearch('project');
act(() => root.render(<GlobalSearchRoot />));
await act(async () => { await vi.runOnlyPendingTimersAsync(); });
const dialog = document.querySelector<HTMLElement>('[data-testid="global-search-dialog"]')!;

for (const item of cardItems) {
const group = dialog.querySelector(`[data-search-group="${item.group}"]`)!;
expect(group.querySelector('[data-search-layout="cards"]')).not.toBeNull();
expect(group.querySelector('[data-openbitfun-component="action-card"] button')?.getAttribute('aria-label')).toBe(item.title);
}
for (const item of contentItems) {
const group = dialog.querySelector(`[data-search-group="${item.group}"]`)!;
expect(group.querySelector('[data-search-layout="cards"]')).toBeNull();
expect(group.querySelector('.global-search__result')?.getAttribute('aria-label')).toBe(item.title);
}

await act(async () => dialog.querySelector<HTMLButtonElement>('[data-search-group="settings"] button[aria-label="Appearance settings"]')!.click());
expect(fixtures.activate).toHaveBeenCalledWith(cardItems[1].target, expect.anything());
expect(useGlobalSearchStore.getState().open).toBe(false);
});

it('shows source diagnostics and retries without clearing the query or healthy results', async () => {
fixtures.search.mockImplementationOnce((_providers, _request, _signal, onUpdate) => onUpdate(diagnosticSnapshot));
fixtures.search.mockImplementationOnce((_providers, _request, _signal, onUpdate, retry) =>
Expand Down
130 changes: 130 additions & 0 deletions src/web-ui/src/app/global-search/GlobalSearchRoot.scss
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,25 @@
*/
$chevron-box-slack: 5px;

.global-search-overlay {
background: transparent;
-webkit-backdrop-filter: none;
backdrop-filter: none;
// An opacity-animated ancestor would isolate the dialog's backdrop filter.
animation: none;
}

.global-search-dialog {
isolation: isolate;
// The material and content clip themselves; the elevation layer stays outside.
overflow: visible;
// Keep ancestor opacity at 1 so the glass layer can sample the page backdrop.
animation-name: global-search-surface-enter;

&[data-state='exiting'] {
animation-name: global-search-surface-exit;
}

// The results viewport absorbs short windows and longer result sets.
inline-size: var(--openbitfun-layout-search-dialog-width);
block-size: min(
Expand All @@ -19,9 +37,83 @@ $chevron-box-slack: 5px;
);
}

@keyframes global-search-surface-enter {
from { transform: translateY(var(--openbitfun-motion-distance-sm)) scale(0.98); }
to { transform: none; }
}

@keyframes global-search-surface-exit {
from { transform: none; }
to { transform: translateY(var(--openbitfun-motion-distance-sm)) scale(0.98); }
}

.global-search-dialog__material {
display: none;
}

@supports (backdrop-filter: none) or (-webkit-backdrop-filter: none) {
.global-search-dialog {
// The outer surface owns elevation; only the glass layer is clipped.
background: transparent;
box-shadow: var(--openbitfun-shadow-inner-highlight-hover), var(--openbitfun-shadow-overlay);
}

.global-search-dialog__material {
display: block;
position: absolute;
z-index: 0;
inset: 0;
border-radius: inherit;
clip-path: inset(0 round var(--openbitfun-overlay-dialog-surface-radius));
background: color-mix(
in srgb,
color-mix(in srgb, var(--openbitfun-color-surface-raised) 90%, var(--openbitfun-color-content-on-dark)) 58%,
transparent
);
-webkit-backdrop-filter: var(--openbitfun-effect-blur-strong);
backdrop-filter: var(--openbitfun-effect-blur-strong);
// Retain the glass plane when hover marquees promote content to new layers.
transform: translateZ(0);
pointer-events: none;
}
}

:where([data-contrast='high']) .global-search-dialog {
background: var(--openbitfun-color-surface-raised);
-webkit-backdrop-filter: none;
backdrop-filter: none;

.global-search-dialog__material { display: none; }
}

@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
.global-search-dialog {
background: var(--openbitfun-color-surface-raised);
-webkit-backdrop-filter: none;
backdrop-filter: none;

.global-search-dialog__material { display: none; }
}
}

@media (forced-colors: active) {
.global-search-dialog {
background: Canvas;
-webkit-backdrop-filter: none;
backdrop-filter: none;

.global-search-dialog__material { display: none; }
}
}

.global-search-modal-content {
position: relative;
z-index: 1;
display: flex;
// Hover animations repaint only the content plane, away from the glass edges.
contain: paint;
overflow: hidden;
border-radius: inherit;
}

.global-search {
Expand Down Expand Up @@ -840,6 +932,16 @@ $chevron-box-slack: 5px;
}

.global-search--modal {
container-type: inline-size;

.global-search__result:hover:not(.is-selected) {
border-color: transparent;
}

.global-search__action-card:hover:not([data-selected='true']) {
outline-color: transparent;
}

.global-search__query-system-shell {
--openbitfun-control-search-field-height-sm: var(--openbitfun-layout-search-dialog-query-height);
}
Expand Down Expand Up @@ -889,6 +991,34 @@ $chevron-box-slack: 5px;
}
}

// Queried cards use the host's available width; the default overview keeps its layout.
.global-search--modal,
.global-search--embedded {
.global-search__group-items[data-search-layout='cards'] {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: var(--openbitfun-space-3);
}
}

@container (max-width: 600px) {
.global-search--modal,
.global-search--embedded {
.global-search__group-items[data-search-layout='cards'] {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
}

@container (max-width: 420px) {
.global-search--modal,
.global-search--embedded {
.global-search__group-items[data-search-layout='cards'] {
grid-template-columns: minmax(0, 1fr);
}
}
}

.global-search__diagnostics {
min-width: 0;
margin-right: auto;
Expand Down
10 changes: 8 additions & 2 deletions src/web-ui/src/app/global-search/GlobalSearchRoot.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ function iconForItem(item: GlobalSearchItem): IconSource {

function resultVariant(group: GlobalSearchGroupId): 'action' | 'entity' | 'standard' {
if (group === 'actions') return 'action';
if (group === 'workspaces' || group === 'assistants') return 'entity';
if (group === 'workspaces' || group === 'assistants' || group === 'settings' || group === 'capabilities') return 'entity';
return 'standard';
}

Expand Down Expand Up @@ -578,7 +578,11 @@ export const GlobalSearchContent: React.FC<GlobalSearchContentProps> = ({
</div>
)}
</div></div>
<div className="global-search__group-items" role="presentation">
<div
className="global-search__group-items"
role="presentation"
data-search-layout={parsedQuery.query && resultVariant(groupId) !== 'standard' ? 'cards' : undefined}
>
{groupItems.map((item) => {
const itemIcon = iconForItem(item);
const selected = item.id === activeId;
Expand Down Expand Up @@ -816,8 +820,10 @@ const GlobalSearchRoot: React.FC = () => {
size="xl"
aria-label={tCommon('nav.search.dialogLabel')}
className="global-search-dialog"
overlayProps={{ className: 'global-search-overlay' }}
data-testid="global-search-dialog"
>
<div className="global-search-dialog__material" aria-hidden="true" />
<DialogBody className="global-search-modal-content">
<GlobalSearchContent
active={open}
Expand Down
Loading
Loading