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
8 changes: 7 additions & 1 deletion packages/oc-docs/e2e/components/playground.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,13 @@ export class PlaygroundComponent extends BaseComponent {
}

async selectTab(id: string): Promise<void> {
await this.tab(id).click();
const direct = this.tab(id);
if ((await direct.count()) > 0 && (await direct.isVisible())) {
await direct.click();
return;
}
await this.page.getByTestId('tabs-more').click();
await this.page.getByTestId(`tabs-more-${id}`).click();
}

async close(): Promise<void> {
Expand Down
15 changes: 15 additions & 0 deletions packages/oc-docs/e2e/tests/playground/playground-tabs.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { test, expect } from '../../playwright';

test.describe('Playground request tabs — responsive overflow', () => {
test.use({ viewport: { width: 900, height: 800 } });

test('collapses overflowing tabs into a ⋯ dropdown and selects from it', async ({ page, playground }) => {
await playground.open('bottom');
await playground.openRequest('get users');

await expect(page.getByTestId('tabs-more')).toBeVisible();

await playground.selectTab('tests');
await expect(page.getByTestId('tabs-tab-tests')).toHaveAttribute('aria-selected', 'true');
});
});
22 changes: 22 additions & 0 deletions packages/oc-docs/src/assets/icons/ChevronsRightIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from 'react';

export const ChevronsRightIcon: React.FC<{ size?: number }> = ({ size = 18 }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
strokeWidth="2"
stroke="currentColor"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<polyline points="7 7 12 12 7 17" />
<polyline points="13 7 18 12 13 17" />
</svg>
);

export default ChevronsRightIcon;
26 changes: 26 additions & 0 deletions packages/oc-docs/src/assets/icons/DotIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React from 'react';

export const DotIcon: React.FC<{ width?: number | string }> = ({ width = 10 }) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={width}
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
style={{ display: 'inline-block' }}
aria-hidden="true"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path
d="M12 7a5 5 0 1 1 -4.995 5.217l-.005 -.217l.005 -.217a5 5 0 0 1 4.995 -4.783z"
strokeWidth="0"
fill="currentColor"
/>
</svg>
);

export default DotIcon;
4 changes: 3 additions & 1 deletion packages/oc-docs/src/assets/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ export * from './SidebarToggleIcon';
export * from './SettingsIcon';
export * from './TrashIcon';
export * from './ExampleIcon';
export * from './CheckIcon';
export * from './DotIcon';
export * from './ChevronsRightIcon';
export * from './CheckIcon';
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,11 @@ describe('SidebarNavLink', () => {
expect(html).not.toContain('navlink-label mono');
});

it('indents by level via margin (chevron + gap step of 19px) with a uniform 8px inner pad', () => {
it('indents by level via margin (one 19px chevron+gap step per level) with a uniform 8px inner pad', () => {
const html = renderToStaticMarkup(<SidebarNavLink label="Nested" level={2} />);
// level*19 = 38px -> 38/16 = 2.375rem margin; the 8px (0.5rem) pad is uniform
// across folders and leaves so glyphs line up under their parent.
// Each level is one 19px chevron+gap step: level*19 = 38px -> 38/16 = 2.375rem margin.
// The 8px (0.5rem) pad is uniform across folders and leaves so glyphs line up
// under their parent.
expect(html).toContain('margin-left:2.375rem');
expect(html).toContain('padding-left:0.5rem');
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ const CollectionSettings: React.FC<CollectionSettingsProps> = ({ collection }) =

<div className="collection-settings-tabs">
<Tabs
variant="responsive"
tabs={tabs.map((tab) => ({
...tab,
content: <div className="collection-settings-tab-content">{tab.content}</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,16 @@ describe('HeadersTab', () => {
expect(values).toContain('text/html');
});

it('renders a provided title and description', () => {
it('renders the provided description with no standalone title heading', () => {
const root = useRenderToDom(
<HeadersTab
headers={[]}
onHeadersChange={noop}
title="Headers"
description="Request headers sent with the call"
/>
);
expect(query(root, '.title').text.trim()).toBe('Headers');
// With no title prop, the tab shows only the description — no separate title heading.
expect(root.querySelector('.title')).toBeNull();
expect(query(root, '.description').text.trim()).toBe('Request headers sent with the call');
expect(root.querySelector('[data-testid="bulk-edit-toggle"]')).toBeTruthy();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ describe('ScriptsTab', () => {
expect(query(root, '[data-testid="scripts-tabs-tab-post-response"]').text.trim()).toBe('Post response');
});

it('renders the Tests section by default', () => {
it('renders the Tests section with no standalone title heading', () => {
const root = useRenderToDom(<ScriptsTab scripts={{}} onScriptChange={noop} />);
expect(root.querySelector('.title')).toBeNull();
expect(query(root, '.label').text.trim()).toBe('Tests');
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ const EnvironmentsView: React.FC<EnvironmentsViewProps> = ({ collection, compact

<div className="env-tabs-area">
{selectedEnvironment ? (
<Tabs defaultActiveTab="variables" tabs={tabs} />
<Tabs variant="responsive" testId="environment-tabs" defaultActiveTab="variables" tabs={tabs} />
) : (
<div className="env-message">
<p>Select an environment to view its variables</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,6 @@ describe('ExampleView', () => {
const root = useRenderToDom(<ExampleView request={request} example={{ ...example, request: {} }} />);
const empty = root.querySelector('[data-testid="example-view-request-empty"]');
expect(empty).not.toBeNull();
expect(empty!.textContent).toContain('No headers, body, auth, variables, or scripts configured for this request.');
expect(empty!.textContent).toContain('No params, headers, or body configured for this request.');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export const ExampleView: React.FC<ExampleViewProps> = ({ request, example, orie
)}
{!hasRequestData && (
<div className="example-view-empty" data-testid="example-view-request-empty">
No header, body, params, or auth configured for the request.
No params, headers, or body configured for this request.
</div>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ const FolderSettings: React.FC<FolderSettingsProps> = ({ folder, onFolderChange
</div>

<div className="flex-1 min-h-0 overflow-y-auto mt-4">
<Tabs tabs={tabs} activeTab={activeTab} onTabChange={setActiveTab} />
<Tabs variant="responsive" testId="folder-settings-tabs" tabs={tabs} activeTab={activeTab} onTabChange={setActiveTab} />
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ const RequestPane: React.FC<RequestPaneProps> = ({ item, onItemChange }) => {
return (
<StyledWrapper>
<Tabs
variant="responsive"
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,8 @@ const ResponsePane: React.FC<ResponsePaneProps> = ({ response, isLoading }) => {
return (
<StyledWrapper>
<Tabs
variant="responsive"
testId="response-tabs"
className='h-full'
tabs={tabs}
activeTab={activeTab}
Expand Down
85 changes: 83 additions & 2 deletions packages/oc-docs/src/ui/Tabs/StyledWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,22 @@ export const StyledWrapper = styled.div`
border-radius: 2px;
}
.tab-count {
margin-left: 0.1875rem;
display: inline-flex;
align-items: center;
vertical-align: baseline;
margin-left: 0.125rem;
font-size: 0.625rem;
font-weight: 500;
line-height: 1;
letter-spacing: 0;
color: var(--text-secondary);
color: inherit;
}
.tab-status-dot {
display: inline-flex;
align-items: center;
margin-left: 0.125rem;
line-height: 0;
opacity: 0.8;
}

&.tabs-variant-button .tabs {
Expand Down Expand Up @@ -105,6 +116,76 @@ export const StyledWrapper = styled.div`
margin-top: 0.75rem;
}

&.tabs-variant-responsive .tabs-header {
align-items: center;
justify-content: flex-start;
gap: 0.5rem;
min-width: 0;
}
&.tabs-variant-responsive .tabs {
position: relative;
flex: 1 1 auto;
flex-wrap: nowrap;
align-items: center;
gap: 1.2rem;
min-width: 0;
overflow: hidden;
}
&.tabs-variant-responsive .tabs-measure {
position: absolute;
top: 0;
left: 0;
display: flex;
gap: 1.2rem;
visibility: hidden;
pointer-events: none;
}
&.tabs-variant-responsive .tab {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
gap: 0.25rem;
white-space: nowrap;
padding: 0.375rem 0;
border-bottom: 0.125rem solid transparent;
color: var(--oc-colors-text-subtext0);
font-size: var(--oc-font-size-sm);
font-weight: 400;
line-height: 1.125rem;
transition: color 0.15s ease, border-color 0.15s ease;
}
&.tabs-variant-responsive .tab::after {
display: none;
}
&.tabs-variant-responsive .tab:hover:not(.is-active):not(:disabled) {
color: var(--text-primary);
}
&.tabs-variant-responsive .tab.is-active {
color: var(--text-primary);
font-weight: 400;
border-bottom-color: var(--primary-color);
}
&.tabs-variant-responsive .tabs-more {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.125rem;
border: none;
background: none;
color: var(--oc-colors-text-subtext0);
cursor: pointer;
line-height: 0;
}
&.tabs-variant-responsive .tabs-more:hover {
color: var(--text-primary);
}
.tabs-more-item {
display: inline-flex;
align-items: center;
gap: 0.25rem;
}

.tabs-right {
display: flex;
align-items: center;
Expand Down
Loading
Loading