Skip to content

Commit f32d085

Browse files
committed
merge: sync oc-docs updates from opencollection (#139-#146)
2 parents bac6132 + 8be38f7 commit f32d085

66 files changed

Lines changed: 772 additions & 302 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/oc-docs/e2e/components/base.component.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,26 @@ import type { Page, Locator } from '@playwright/test';
22

33
export abstract class BaseComponent {
44
readonly root: Locator;
5+
private dragY = 0;
56

67
constructor(protected readonly page: Page, root?: Locator) {
78
this.root = root ?? page.locator(':root');
89
}
10+
11+
/** Press the pointer on a resize handle; the drag y is kept for later moves. */
12+
protected async grabHandle(handle: Locator): Promise<void> {
13+
const box = await handle.boundingBox();
14+
this.dragY = (box?.y ?? 0) + (box?.height ?? 0) / 2;
15+
await handle.hover();
16+
await this.page.mouse.down();
17+
}
18+
19+
/** Move the held pointer to an absolute x (keeps the grabbed y). */
20+
async movePointerToX(x: number): Promise<void> {
21+
await this.page.mouse.move(x, this.dragY, { steps: 10 });
22+
}
23+
24+
async releasePointer(): Promise<void> {
25+
await this.page.mouse.up();
26+
}
927
}

packages/oc-docs/e2e/components/playground.component.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export class PlaygroundComponent extends BaseComponent {
1515
readonly runner = this.page.getByTestId('playground-runner');
1616
readonly loadError = this.page.getByTestId('playground-load-error');
1717
readonly sidebarPanel = this.page.getByTestId('playground-sidebar-panel');
18+
readonly sidebarResizer = this.page.getByTestId('playground-sidebar-resizer');
1819
readonly sidebarBackdrop = this.page.getByTestId('playground-sidebar-backdrop');
1920
readonly collectionNode = this.page.getByTestId('sidebar-collection-root');
2021
readonly collectionCollapseToggle = this.collectionNode.getByRole('button', {
@@ -121,4 +122,13 @@ export class PlaygroundComponent extends BaseComponent {
121122
async toggleCollapse(): Promise<void> {
122123
await this.collapseButton.click();
123124
}
125+
126+
async sidebarWidth(): Promise<number> {
127+
const box = await this.sidebarPanel.boundingBox();
128+
return box?.width ?? 0;
129+
}
130+
131+
async grabSidebarResizer(): Promise<void> {
132+
await this.grabHandle(this.sidebarResizer);
133+
}
124134
}

packages/oc-docs/e2e/components/request/examples.component.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,6 @@ export class ExamplesComponent extends BaseComponent {
2929
await this.example(name).getByTestId('example-toggle').click();
3030
}
3131

32-
async try(name: string): Promise<void> {
33-
await this.example(name).getByTestId('example-try').click();
34-
}
35-
3632
async selectRequestTab(name: string, tab: string): Promise<void> {
3733
await this.example(name).getByTestId(`example-request-pane-tab-${tab}`).click();
3834
}

packages/oc-docs/e2e/components/sidebar.component.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export class SidebarComponent extends BaseComponent {
88
readonly environments = this.page.getByTestId('sidebar-environments');
99
readonly collapseButton = this.page.getByTestId('sidebar-collapse');
1010
readonly expandButton = this.page.getByTestId('sidebar-expand');
11+
readonly resizer = this.page.getByTestId('sidebar-resizer');
1112
readonly drawer = this.page.getByTestId('sidebar-drawer');
1213
readonly backdrop = this.page.getByTestId('sidebar-backdrop');
1314
readonly hamburger = this.page.getByTestId('topbar-menu');
@@ -42,6 +43,15 @@ export class SidebarComponent extends BaseComponent {
4243
}
4344
}
4445

46+
async width(): Promise<number> {
47+
const box = await this.inline.boundingBox();
48+
return box?.width ?? 0;
49+
}
50+
51+
async grabResizer(): Promise<void> {
52+
await this.grabHandle(this.resizer);
53+
}
54+
4555
async collapse(): Promise<void> {
4656
await this.inline.hover();
4757
await this.collapseButton.click();

packages/oc-docs/e2e/tests/sidebar/sidebar-examples.spec.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,4 @@ test.describe('Sidebar - Examples (docs)', () => {
8484
await expect(requestPage.examples.activeCard).toHaveCount(0);
8585
});
8686

87-
test('Try on an example opens the playground on that example, deep-linked', async ({ requestPage, playground, page }) => {
88-
await requestPage.examples.try(OK_EXAMPLE);
89-
90-
await expect(playground.exampleView).toBeVisible();
91-
await expect(page).toHaveURL(new RegExp(`[?&]pgEx=${OK_EXAMPLE_SLUG}(?:&|$)`));
92-
await expect(playground.exampleViewControls).toHaveCount(0);
93-
});
9487
});
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { test, expect } from '../../playwright';
2+
3+
const FOLDERS = '/?fixture=folders';
4+
const DESKTOP = { width: 1280, height: 900 };
5+
6+
// The resize handle sits at the sidebar's right edge (left: var(--sidebar-width),
7+
// default 260px), and the drag is delta-based from where it is grabbed, so moving
8+
// the pointer to an absolute x lands the width near that x. Collapse fires when the
9+
// pointer is dragged ~100px past the 200px min (i.e. below x=100); re-expand fires
10+
// once it climbs back to the min (x>=200) within the same held gesture.
11+
12+
test.describe('docs sidebar - resize (desktop)', () => {
13+
test.use({ viewport: DESKTOP });
14+
15+
test('widens when the handle is dragged right', async ({ page, sidebar }) => {
16+
await page.goto(FOLDERS);
17+
await expect(sidebar.inline).toBeVisible();
18+
const before = await sidebar.width();
19+
20+
await sidebar.grabResizer();
21+
await sidebar.movePointerToX(400);
22+
await sidebar.releasePointer();
23+
24+
const after = await sidebar.width();
25+
expect(after).toBeGreaterThan(before);
26+
expect(after).toBeGreaterThan(360);
27+
});
28+
29+
test('clamps to the max width (480px)', async ({ page, sidebar }) => {
30+
await page.goto(FOLDERS);
31+
await sidebar.grabResizer();
32+
await sidebar.movePointerToX(900);
33+
await sidebar.releasePointer();
34+
35+
const after = await sidebar.width();
36+
expect(after).toBeGreaterThan(470);
37+
expect(after).toBeLessThanOrEqual(482);
38+
});
39+
40+
test('persists the resized width across a reload (sessionStorage)', async ({ page, sidebar }) => {
41+
await page.goto(FOLDERS);
42+
await sidebar.grabResizer();
43+
await sidebar.movePointerToX(380);
44+
await sidebar.releasePointer();
45+
const resized = await sidebar.width();
46+
expect(resized).toBeGreaterThan(360);
47+
48+
await page.reload();
49+
await expect(sidebar.inline).toBeVisible();
50+
expect(Math.abs((await sidebar.width()) - resized)).toBeLessThan(5);
51+
});
52+
53+
test('collapses when dragged past the min, and can be re-opened', async ({ page, sidebar }) => {
54+
await page.goto(FOLDERS);
55+
await sidebar.grabResizer();
56+
await sidebar.movePointerToX(60);
57+
await sidebar.releasePointer();
58+
59+
await expect(sidebar.inline).toHaveCount(0);
60+
await expect(sidebar.expandButton).toBeVisible();
61+
62+
await sidebar.expand();
63+
await expect(sidebar.inline).toBeVisible();
64+
});
65+
66+
test('re-expands within the same held drag after collapsing', async ({ page, sidebar }) => {
67+
await page.goto(FOLDERS);
68+
await sidebar.grabResizer();
69+
70+
await sidebar.movePointerToX(60);
71+
await expect(sidebar.inline).toHaveCount(0);
72+
73+
await sidebar.movePointerToX(320);
74+
await expect(sidebar.inline).toBeVisible();
75+
76+
await sidebar.releasePointer();
77+
await expect(sidebar.inline).toBeVisible();
78+
});
79+
});
80+
81+
test.describe('playground sidebar - resize (bottom dock)', () => {
82+
test.use({ viewport: DESKTOP });
83+
84+
test('widens when the handle is dragged right', async ({ playground }) => {
85+
await playground.open('bottom');
86+
await expect(playground.sidebarPanel).toBeVisible();
87+
const before = await playground.sidebarWidth();
88+
89+
await playground.grabSidebarResizer();
90+
await playground.movePointerToX(400);
91+
await playground.releasePointer();
92+
93+
expect(await playground.sidebarWidth()).toBeGreaterThan(before);
94+
});
95+
96+
test('collapses when dragged past the min, and re-opens from the toggle', async ({ playground }) => {
97+
await playground.open('bottom');
98+
await expect(playground.sidebarPanel).toBeVisible();
99+
100+
await playground.grabSidebarResizer();
101+
await playground.movePointerToX(60);
102+
await playground.releasePointer();
103+
104+
await expect(playground.sidebarPanel).toHaveCount(0);
105+
106+
await playground.sidebarToggle.click();
107+
await expect(playground.sidebarPanel).toBeVisible();
108+
});
109+
});

packages/oc-docs/src/assets/icons/SendIcon.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import React from 'react';
22
import { baseIconProps } from './baseIconProps';
33

4+
interface SendIconProps {
5+
width?: number;
6+
height?: number;
7+
}
8+
49
/** Paper-plane "send" icon used by the Try action. */
5-
export const SendIcon: React.FC = () => (
6-
<svg {...baseIconProps} width={11} height={11}>
10+
export const SendIcon: React.FC<SendIconProps> = ({ width = 11, height = 11 }) => (
11+
<svg {...baseIconProps} width={width} height={height}>
712
<line x1="22" y1="2" x2="11" y2="13" />
813
<polygon points="22 2 15 22 11 13 2 9 22 2" />
914
</svg>

packages/oc-docs/src/components/AppShell/AppShell.tsx

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,11 @@ import { ChevronLeftIcon, ChevronRightIcon } from '../../assets/icons';
1111
import PageRouter from '../PageRouter/PageRouter';
1212
import Playground from '../Playground/Playground';
1313
import SearchBar from '../Search/SearchBar/SearchBar';
14-
import { useSearchHotkey, usePlaygroundUrlState, useElementWidth } from '../../hooks';
14+
import { useSearchHotkey, usePlaygroundUrlState, useElementWidth, useResizableSidebar } from '../../hooks';
1515
import { useAppSelector } from '../../store/hooks';
1616
import { selectDocsCollection } from '../../store/slices/docs';
1717
import { selectGitCollectionUrl } from '../../store/slices/app';
1818
import { useActiveResolution } from '../../routing/hooks';
19-
import { exampleSlugForIndex } from '../../routing/slug';
20-
import type { HttpRequest } from '@opencollection/types/requests/http';
2119
import { layoutModeForWidth } from '../../hooks/useTopbarLayout';
2220
import { buildFetchInBrunoUrl } from '../../utils/buildFetchInBrunoUrl';
2321
import { StyledWrapper } from './StyledWrapper';
@@ -56,9 +54,14 @@ const AppShell: React.FC<AppShellProps> = ({ logo, testId = 'app-shell' }) => {
5654
const isDesktop = mode === 'desktop';
5755
const [sidebarCollapsed, setSidebarCollapsed] = useState<boolean>(false);
5856
const [drawerOpen, setDrawerOpen] = useState<boolean>(false);
57+
const { width: sidebarWidth, dragging: sidebarDragging, startDrag: startSidebarResize } = useResizableSidebar(
58+
'oc-docs:docsSidebarWidth',
59+
() => setSidebarCollapsed(true),
60+
() => setSidebarCollapsed(false)
61+
);
5962
const { pathname } = useLocation();
6063

61-
const { open: playgroundOpen, dock: playgroundDock, openPlayground, setRequestExample } = usePlaygroundUrlState();
64+
const { open: playgroundOpen, dock: playgroundDock, openPlayground } = usePlaygroundUrlState();
6265
// Bumped on every Try click so the bottom sheet re-expands from collapsed even
6366
// when the requested slug is unchanged (a slug change alone wouldn't signal it).
6467
const [playgroundOpenNonce, setPlaygroundOpenNonce] = useState(0);
@@ -89,22 +92,17 @@ const AppShell: React.FC<AppShellProps> = ({ logo, testId = 'app-shell' }) => {
8992
setPlaygroundOpenNonce((nonce) => nonce + 1);
9093
}, [openPlayground, resolution]);
9194

92-
// Open the playground on a specific example of the current request (its Try
93-
// action): pgReq keeps the request, pgEx the example, so a share/reload lands
94-
// on the same read-only example view.
95-
const handleTryExample = (index: number) => {
96-
const entry = resolution?.entry;
97-
if (!entry) return;
98-
setRequestExample(entry.slug, exampleSlugForIndex(entry.item as HttpRequest | null, index));
99-
};
100-
10195
return (
10296
<StyledWrapper
10397
className="appshell"
10498
data-testid={testId}
10599
data-dock={playgroundOpen ? playgroundDock : 'none'}
106100
>
107-
<div className="appshell-body" ref={bodyRef}>
101+
<div
102+
className="appshell-body"
103+
ref={bodyRef}
104+
style={{ '--sidebar-width': `${sidebarWidth}px` } as React.CSSProperties}
105+
>
108106
<Topbar
109107
layoutMode={mode}
110108
collectionName={collection?.info?.name || 'API Collection'}
@@ -138,6 +136,15 @@ const AppShell: React.FC<AppShellProps> = ({ logo, testId = 'app-shell' }) => {
138136
<aside className="appshell-sidebar" data-testid="app-sidebar">
139137
<Sidebar />
140138
</aside>
139+
<div
140+
className="appshell-sidebar-resizer"
141+
data-testid="sidebar-resizer"
142+
data-dragging={sidebarDragging ? 'true' : undefined}
143+
role="separator"
144+
aria-orientation="vertical"
145+
aria-label="Resize sidebar"
146+
onPointerDown={startSidebarResize}
147+
/>
141148
<IconButton
142149
className="appshell-collapse"
143150
label="Collapse sidebar"
@@ -161,7 +168,7 @@ const AppShell: React.FC<AppShellProps> = ({ logo, testId = 'app-shell' }) => {
161168
</IconButton>
162169
)}
163170
<main className="appshell-content" ref={contentRef}>
164-
<PageRouter onOpenPlayground={handleOpenPlayground} onTryExample={handleTryExample} />
171+
<PageRouter onOpenPlayground={handleOpenPlayground} />
165172
</main>
166173
</div>
167174
</div>

packages/oc-docs/src/components/AppShell/StyledWrapper.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,35 @@ export const StyledWrapper = styled.div`
4444
background-color: var(--oc-background-base);
4545
}
4646
47+
.appshell-sidebar-resizer {
48+
position: absolute;
49+
top: 0;
50+
bottom: 0;
51+
left: var(--sidebar-width);
52+
width: 0.5625rem;
53+
transform: translateX(-0.25rem);
54+
z-index: calc(var(--z-sidebar, 5) + 1);
55+
cursor: col-resize;
56+
touch-action: none;
57+
}
58+
59+
.appshell-sidebar-resizer::before {
60+
content: '';
61+
position: absolute;
62+
top: 0;
63+
bottom: 0;
64+
left: 50%;
65+
width: 0.0625rem;
66+
transform: translateX(-50%);
67+
background-color: transparent;
68+
}
69+
70+
.appshell-sidebar-resizer:hover::before,
71+
.appshell-sidebar-resizer[data-dragging='true']::before {
72+
width: 0.125rem;
73+
background-color: var(--oc-border-border2);
74+
}
75+
4776
.appshell-content {
4877
flex: 1;
4978
min-width: 0;

packages/oc-docs/src/components/Docs/Sidebar/SidebarNavLink/SidebarNavLink.spec.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,11 @@ describe('SidebarNavLink', () => {
5252
expect(html).not.toContain('navlink-label mono');
5353
});
5454

55-
it('indents by level via margin (chevron + gap step of 19px) with a 4px inner pad', () => {
55+
it('indents by level via margin (chevron + gap step of 19px) with a uniform 8px inner pad', () => {
5656
const html = renderToStaticMarkup(<SidebarNavLink label="Nested" level={2} />);
57-
// level*19 + 4 = 42px -> 42/16 = 2.625rem margin; the fixed 4px (0.25rem) pad
58-
// restores the level*19+8 glyph offset.
57+
// level*19 + 4 = 42px -> 42/16 = 2.625rem margin; the 8px (0.5rem) pad is uniform
58+
// across folders and leaves so glyphs line up under their parent.
5959
expect(html).toContain('margin-left:2.625rem');
60-
expect(html).toContain('padding-left:0.25rem');
60+
expect(html).toContain('padding-left:0.5rem');
6161
});
6262
});

0 commit comments

Comments
 (0)