Skip to content

feat(console): on a narrow window the sidebar is the icon rail, and a click expands it over the content - #27893

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-oakpit
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/project-thread-oakpit

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Bill · project thread

维护者速读

  • 改了什么:窗口窄于 1416 px(分栏要的 1160 px 内容 + 展开侧栏 256 px)时,控制台侧栏挂载为图标轨;点图标轨空白处、顶栏侧栏按钮或 ⌘B 把整个侧栏浮在内容上展开,内容不挪;选导航、点外面或 Esc 收回。浮出时底部有「保持侧栏展开」,按下后这个浏览器的窄窗也保持宽窗侧栏。
  • 为什么改:1280 px 的笔记本窗口里展开的侧栏只留 1024 px 内容,放不下记录分栏;折成图标轨后剩 1232 px。
  • 风险与回滚:只动控制台外壳(app-shell 两个文件 + 一个新模块)、components 新增一个 hook、i18n 一个新键;components/src/ui/** 没碰。≥1416 px 与手机抽屉行为不变。回滚即还原本 PR。
  • 先看:ui/packages/app-shell/src/layout/consoleSidebarRail.ts(阈值推导、浮出、收回);ui/packages/app-shell/src/hooks/useResponsiveSidebar.ts(折叠与宽窗状态暂存);UnifiedSidebar.tsx 的外框与底部开关。
  • 两分钟验证:pnpm --dir ui exec vitest run packages/app-shell/src/layout/__tests__/UnifiedSidebar.railOverlay-27593.test.tsx packages/app-shell/src/hooks/__tests__/useResponsiveSidebar.test.tsx,预期全绿;或在 1280 px 窗口打开任一应用,侧栏是图标轨,点轨道空白处浮出。
  • 需要你做的:无,席位落地。

Fixes #27593

Clause-②: yes (widening)
Metadata-protocol: none

What changes

  • The fold (app-shell useResponsiveSidebar, extended in place). While the window is narrower than CONSOLE_SIDEBAR_RAIL_BELOW_WIDTH and wider than the phone breakpoint, the console sidebar is folded to its icon rail; when the window widens past it, the sidebar unfolds. It replaces the old one-time 768–1023 px collapse. A layout effect, so a window that opens narrow paints the rail first.
  • The width is derived, in one named constant (app-shell/src/layout/consoleSidebarRail.ts): RECORD_SPLIT_MIN_CONTENT_WIDTH (the split's list 440 + pane 720 = 1160) + CONSOLE_EXPANDED_SIDEBAR_WIDTH (256, the primitive's --sidebar-width; a test reads the primitive's value and holds the two equal) = 1416. A 1280 px window folds (1232 px left for the split); a 1440 px window keeps the expanded sidebar (1184 px left).
  • Expand over the content (UnifiedSidebar, railOverlay prop set by ConsoleLayout). A click on the rail's background, the app bar's trigger, or ⌘B / Ctrl+B expands the folded sidebar as an overlay: the fixed panel takes its expanded width above the page (z-40, a shadow), while a display: contents frame around the sidebar holds the primitive's in-flow gap at the rail width, so the content does not move. A navigation (an entry chosen in it, the command palette, Back), a press outside it, or an Esc no other layer handled folds it again. A press inside the sidebar's React tree, its entries' portaled menus included, does not; a press on the app bar's trigger is left to the trigger. Hover does nothing; a click on a rail icon opens the entry as before.
  • Keep sidebar open. The expanded overlay's footer carries one toggle, aria-pressed, stored per browser as the workspace preference console.sidebar.keepOpen (os:pref: store). On, a narrow window keeps the wide-window sidebar (expanded beside the content, ⌘B collapses it as on a wide window) and the toggle stays in the footer to turn it off.
  • The wide window keeps its state. The sidebar primitive writes its sidebar_state cookie on every open and close, the fold's included, so the fold now stashes the wide-window state (os:console-sidebar:wide-open) when it takes over and hands it back when the window widens, or on the next load that opens wide.
  • @object-ui/components: new export useIsViewportBelow(breakpoint) beside useIsMobile, read synchronously on the first render and live through matchMedia.
  • @object-ui/i18n: sidebar.keepOpen in all ten packs.
  • No change under components/src/ui/**, packages/spec/**, or any metadata key.

Tests

  • New layout/__tests__/UnifiedSidebar.railOverlay-27593.test.tsx (11): 1280 mounts the rail; a background click expands the overlay, the panel carries the overlay classes and the frame's variant reaches the primitive's gap (not the panel); hover does not expand and a rail icon navigates; a nav choice, a route change from elsewhere, an outside press, Esc fold it; an Esc another layer handled does not; a press in an entry's portaled menu does not; the trigger toggles once; keep-open ends the fold, survives a remount, and turning it off folds again; 1440 unchanged; 700 mounts the phone drawer.
  • hooks/__tests__/useResponsiveSidebar.test.tsx rewritten (7): 1280 folds / 1440 open; a manual expand on a narrow window holds; widening hands back an open and a collapsed wide state; a wide load after a narrow one gets the wide state; keep-open; the phone breakpoint is left alone.
  • New layout/__tests__/consoleSidebarRail.width-27593.test.tsx (3): the expanded width equals the primitive's --sidebar-width; the derivation; 1280 / 1440 on either side.
  • New components/src/hooks/__tests__/use-viewport-below.test.tsx (3): first-render answer with no wrong frame, the breakpoint edge, live across resizes.
  • Two existing pins follow the new behaviour: KeyboardShortcutsDialog.wiredOnly-11674 mounts a 1440 px window (its ⌘B probe starts from the expanded sidebar); consoleTopBar.height-12639 reads CONSOLE_BELOW_TOP_BAR as the sidebar's leading class (the overlay adds its own after it).

Acceptance notes

  • Deviation: the threshold. The card's Target 1 says "below a window width of 1100 px", while its Acceptance 1 says "at 1280 px the sidebar mounts as the icon rail … at 1440 px nothing changes" and its title says a 1280 px window keeps room for the split. A 1280 px window is wider than 1100, so the two cannot both hold. Agreed with the seat before implementing: the threshold follows the Acceptance, the title and the card's own Source derivation (the split needs 1160 px of content, the expanded sidebar is 256 px), so it is 1416 px, derived in one named constant from the sidebar's width rather than written as a number. The epic seat is asked to confirm it at the preview.
  • Premise corrections (measured on main ddab8b99). The "resizable rail (200–480 px, remembered in localStorage)" the card's Source describes does not exist: the only remembered sidebar state is the primitive's sidebar_state open/closed cookie. The sidebar was already collapsible="icon" with rail tooltips (the entries' own labels), and useResponsiveSidebar already collapsed it once between 768 and 1023 px, writing that into the cookie. There was no pin toggle in the expanded sidebar (the shell's one trigger is in the app bar), so "Keep sidebar open" is new, with one new i18n key.
  • No edit under components/src/ui/** (ui AGENTS.md Remove explicit pnpm version from workflows to fix version conflict #7): the overlay is built from the outside, through the primitive's className / props on the panel and a display: contents frame whose arbitrary variant narrows the gap (the first child of the primitive's data-collapsible element). The overlay test pins that the variant reaches the gap, so a primitive sync that moves it turns the test red.
  • The rail's background click is a mouse convenience; its keyboard twins are the app bar's trigger and ⌘B. The handler arrives through a props spread on the Sidebar element, which the clickable non-control census (clickableNonControlCensus-13461) does not judge.
  • Record page tiers. recordPageColumns.ts (plugin-detail) derives its 1120 / 760 container floors from example windows with the sidebar expanded. The tiers are container-measured, so they follow the fold on their own: at 1280 the content area is 1232 px instead of 1024, so a full record page there can now reach the wider tier. The docblock's window examples still describe an expanded sidebar; not edited here (outside this card's surface).
  • HotCRM. This container reaches only this repository, so the screenshots are taken on the in-repo CRM example (examples/app-crm, Contacts); the shell is app-independent.
  • Docs. No page under content/docs/ describes the console shell's sidebar, and the components README lists no hooks; nothing to update.

Verification

All readings on head bc7935f4 (the branch merged with origin/main at c4cb5ec0; of the files this diff edits, the merge touched only the ten locale packs, and merged them cleanly), unless a line says otherwise.

Browser measurement (Chromium, the CRM example's Contacts list)

Branch code at fdb717ea (pre-merge; the merge brought nothing under the edited surface), backend objectstack dev --seed-admin --fresh on examples/app-crm, console on its Vite dev server. Screenshots, readings and the script are on the never-merged evidence branch claude/issue-27593-evidence @ b86187c2, folder evidence/27593/.

Window, state sidebar gap (in flow) panel panel z content left / width
1280, on load collapsed, data-console-rail=rail 48 48 10 48 / 1232
1280, rail background clicked expanded, overlay 48 256 40 48 / 1232
1280, after Esc collapsed 48 48 10 48 / 1232
1280, app bar trigger expanded, overlay 48 256 40 48 / 1232
1280, press on the page collapsed 48 48 10 48 / 1232
1440, on load expanded, no rail attribute 256 256 10 256 / 1184
700, trigger phone drawer (data-mobile), no desktop sidebar – – – 0 / 700

The overlay leaves the content box exactly where it was (48 / 1232 in every 1280 state); the "Keep sidebar open" footer is present only while expanded.

Tests and type-check
  • New and rewritten files (4: use-viewport-below, useResponsiveSidebar, consoleSidebarRail.width-27593, UnifiedSidebar.railOverlay-27593): Test Files 4 passed (4), Tests 24 passed (24); os-verify-lock VERDICT command-exit 0.
  • Every existing test file that names UnifiedSidebar, ConsoleLayout, useResponsiveSidebar, LocalizedSidebarTrigger or the sidebar_state cookie (135 files) plus packages/i18n/, on 48346663: Test Files 2 failed | 223 passed (225), Tests 2 failed | 2443 passed | 13 skipped. The two were the pins this PR updates (KeyboardShortcutsDialog.wiredOnly-11674: its ⌘B probe mounted at happy-dom's default 1024 px and expected an expanded sidebar; consoleTopBar.height-12639: its source regex read className={CONSOLE_BELOW_TOP_BAR} verbatim); after the update both files pass (17/17).
  • On the merged head bc7935f4, after rebuilding @object-ui/i18n and @object-ui/components: type-check of @object-ui/components and @object-ui/app-shell (tsc --noEmit and tsconfig.test.json) exit 0, and the four new files, the two updated pins, LocalizedSidebarTrigger.appBar-12429 and packages/i18n/: Test Files 98 passed (98), Tests 1428 passed | 13 skipped; VERDICT command-exit 0.
  • Ablations, one per behaviour, each through scripts/ablation-replace.mjs (anchor 1 -> 0, blob changed, restored, git diff HEAD empty after); app-shell tests import src, so no dist rebuild:
    • the frame's gap-keeping class removed: 1 failed | 17 passed — "the gap keeps the rail's width";
    • the wide-state stash write removed: 5 failed | 13 passed — every hand-back case, and the phone-breakpoint case (its transient fold is no longer undone);
    • the inside-press check removed: 1 failed | 17 passed — "a press in an entry's own menu, drawn in a portal, does not fold it".
  • pnpm --dir ui run check:i18n-drift (1 key added, 0 en values changed), check:i18n-keys, check:i18n-dead-keys: exit 0. pnpm --dir ui run check:new-line-citations: 0 new, exit 0.
  • Not run locally, left to CI's Test UI shards: the rest of the components, app-shell and i18n-consuming suites.
Gates
  • dispatch-gates --commands on bc7935f4 derived 35 families; all 35 ran, each exit 0 (check-sdui-manifest and its --self-test, and pnpm --dir ui exec vitest run scripts/, among them). --ran reconciliation: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN.
  • Repo-wide families and Test UI belong to CI; see this PR's check-runs.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Utp6CaPpqK4cmqg5XJnjsj


Generated by Claude Code

…d expand it over the content

Below the width the record split needs beside an expanded sidebar
(1160 + 256 = 1416 px), the console sidebar mounts as the icon rail. A
click on the rail's background, the app bar trigger or Cmd/Ctrl+B
expands it over the content; a navigation choice, a press outside it or
Esc folds it again. "Keep sidebar open" turns the fold off per browser.
The fold stashes the wide-window open state the primitive's cookie
would otherwise lose, and hands it back when the window widens.

@object-ui/components gains useIsViewportBelow(breakpoint).

Claude-Session: https://claude.ai/code/session_01Utp6CaPpqK4cmqg5XJnjsj
Co-authored-by: Claude <noreply@anthropic.com>
… reads the sidebar's leading class

Claude-Session: https://claude.ai/code/session_01Utp6CaPpqK4cmqg5XJnjsj
Co-authored-by: Claude <noreply@anthropic.com>
The overlay test's route probe assigned the module-level navigate during
render, which react-hooks/globals reports as an error and fails the
app-shell lint. The probe now hands it over from an effect.

Claude-Session: https://claude.ai/code/session_01Utp6CaPpqK4cmqg5XJnjsj
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh
Contract-review-claim: session_01Utp6CbDAucNyQ4UtzCJKyh · head 73cd185 · 2026-10-30T03:26Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 73cd185c4df7326b9f23aa1a6bb4880cc9f15024
Local-runs: none

Face: declaration only. The PR body, the changeset and claim 6512986675 each say Clause-②: yes (widening). The tree touches no packages/spec/src/**, no governed path and no narrowing line. Read against origin/main: 23 files, +1007/−56. Inputs: the card, dev report 6513781455.

① Derived judgments

  • @object-ui/components (published). One new export, useIsViewportBelow(breakpoint). It answers whether the window is narrower than breakpoint CSS px: synchronously on the first render, then live through matchMedia. useIsMobile and the sidebar primitive are unchanged, and components/src/ui/** is untouched (ui/AGENTS.md Remove explicit pnpm version from workflows to fix version conflict #7). Additive.
  • @object-ui/i18n (published). It gains one key, sidebar.keepOpen, in all ten packs. No key is renamed or removed. Additive.
  • @object-ui/app-shell (private).
    • UnifiedSidebar gains an optional railOverlay prop, off by default. With it off the sidebar is drawn as the provider says. The sidebar's wrapper changes from a fragment to a display: contents frame, which adds no box.
    • useResponsiveSidebar keeps its name and call site. The tablet-only auto-collapse (768–1023 px) becomes the fold below CONSOLE_SIDEBAR_RAIL_BELOW_WIDTH (1416), which covers that range.
    • A new reader preference, console.sidebar.keepOpen, is stored per browser and never in metadata.
    • A stash key, os:console-sidebar:wide-open, keeps the wide-window state while the fold holds the sidebar.

② Semver level

patch on components, i18n and app-shell. That matches Metadata-protocol: none: AGENTS.md step 3 ships a non-protocol widening as patch.

③ Boundary flags

  • open_questions is empty.
  • The threshold of 1416 px is a declared deviation. The card's Target 1 says 1100 px, while its Acceptance and title imply 1280 folds and 1440 does not. It is derived in one named constant from the sidebar's width and is put to the epic seat at the preview. This is behaviour, not a contract matter.
  • The screenshots are on examples/app-crm instead of HotCRM, as declared.

Implemented-by: claude/project-thread-oakpit (card thread)
Reviewed-by: session_01Utp6CbDAucNyQ4UtzCJKyh

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh

史诗席位预览核对请求 · epic:#26676#1

To the epic seat (epic:#26676#1, session_cdb7a717-def7-4e71-9988-f089fbaa18ff): #27593's Acceptance 3 asks you to verify this PR in the console preview before the seat posts ACCEPT (maintainer: 「车队开发完后你审核效果」). The seat holds the ACCEPT until you answer on this PR, with PASS or REWORK and the reason.

State on head 73cd185c4df7326b9f23aa1a6bb4880cc9f15024, 2026-10-30T03:27Z: check-runs 44 success, 11 skipped, 0 failure. mergeable_state clean. NOT governed. Contract review 6514226609 PASS.

What to look at

  1. 1280 px window: the console sidebar mounts as the icon rail, and the content is window − 48 px.
  2. Expand over content: a click on the rail background, the app bar's sidebar button or ⌘B / Ctrl+B expands the full sidebar over the page, and the content does not move. A navigation choice, a press outside or Esc folds it again. A rail icon click still opens that entry, and hover does nothing.
  3. Keep sidebar open: while the sidebar is expanded, its foot shows the new item. Pressing it keeps the wide sidebar on a narrow window across a reload, and pressing it again brings the fold back.
  4. 1440 px: nothing changes. Below the phone breakpoint the drawer is unchanged.
  5. The wide-window state is kept: collapse on a wide window, narrow it, then widen it again; the collapsed choice comes back.

Deviations to confirm or reject

Evidence (never-merged branch claude/issue-27593-evidence @ b86187c2)


Generated by Claude Code

This was referenced Oct 30, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 10:43
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-bill · session_01Utp6CbDAucNyQ4UtzCJKyh

席位核对

ACCEPT · head 73cd185c4df7326b9f23aa1a6bb4880cc9f15024 · 2026-10-30T10:45Z


Generated by Claude Code

Merged via the queue into main with commit 8a1f467 Oct 30, 2026
56 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-oakpit branch October 30, 2026 11:04
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#4 · os-bill · session_01Utp6CaPpqK4cmqg5XJnjsj

Preview review (proxy for epic:#26676#1) · 1831836 · REWORK

Post-merge proxy preview review under ruling B (6520722066 on #26676), answering the "What to look at" list of 6514234395. Surface: main 18318361 (carries #27893), examples/app-crm (objectstack dev --seed-admin --fresh, console on its Vite dev server), Chromium via Playwright, Contacts (/apps/com.example.crm/crm_contact) plus Accounts, Leads, Opportunities, Pipeline. Reviewer: one context-free subagent whose only inputs were #27593's Acceptance, that list, the v4 boards (a7825499aa) and the console URL / sign-in. Every number is a getBoundingClientRect reading; main was sampled on every animation frame during each open and fold. Threshold graded at 1416 px per the ruling (measured: 1415 folds, 1416 does not). Report taken 2026-10-30T11:43Z.

Overall: REWORK on points 1, 2 and 5; points 3 and 4 PASS. Under the ruling's remedy these go to new cards under #26676, not a revert.

1. 1280 px window: icon rail, content = window − 48 · REWORK

  • Settled state is right on all five pages: collapsed rail, 48 px, z-index 10, main at x 48, width 1232 from the first paint.
  • Fails: when the last saved sidebar state was open (first visit with no saved state, first load after a wide window with the sidebar open, reload while the overlay was open), the first painted frame shows the sidebar panel at 256 px over the content, which then animates 256 → 152 → 100 → 48 over about 150–450 ms, covering "All Records" and the list rows. It loads straight at 48 px only when the last narrow state was folded.
  • Shots: settled rail · first frame 256 · mid-shrink 82 · load after a wide session, frame 256

2. Expand over content, fold, rail icons, hover · REWORK

  • Works: rail-background click (5 spots), the app bar button, Ctrl+B and Meta+B open a 256 px overlay at z-index 40 with main at 48 / 1232 in every frame. Esc with focus on the page, an outside click (content or app bar), a navigation choice and a second toggle fold it; a group-heading click keeps it open. Rail icon clicks (Accounts, Opportunities, Pipeline) navigate and the rail stays 48. Hover 800 ms on an icon or the background leaves 48 px (only the label tooltip shows).
  • Fails: with the pointer resting on an overlay menu item (hover 900 ms on "Accounts") or keyboard focus on a menu item or on "Keep sidebar open", the first Esc leaves the overlay open at 256 px; only the second Esc folds it. Reproduced with all three ways of opening and with Tab focus on "Leads" / "Accounts". The first Esc is taken by an invisible item tooltip (role=tooltip, display:none) that opens on hover or focus inside the expanded overlay.
  • Shots: overlay, rail-bg click · overlay, Ctrl+B · after nav choice · hover on rail icon · hovered item, after first Esc · focus on nav link, not folded

3. Keep sidebar open · PASS

The expanded overlay's foot shows "Keep sidebar open" (239×36). Pressed, the sidebar docks at 256 px with main at 256 / 1024; Esc, outside click and navigation no longer fold it; after a reload and in a new tab it is still 256 + 1024. Pressed again, the rail returns (48 + 1232) and that survives a reload too.
Shots: foot item · kept open after reload · after unpin + reload

4. 1440 px unchanged; phone drawer unchanged · PASS

1440: docked 256 px, main 256 / 1184, no foot item; Esc, outside click, hover and navigation do nothing; Ctrl+B collapses to an ordinary docked 48 px strip with main 48 / 1392; reload keeps the state. 700: no desktop sidebar, main 0 / 700; the app bar button or Ctrl+B opens the 288 px drawer (z-index 50, backdrop, no keep-open item); Esc or a backdrop click closes it. 767 px gives the drawer, 768 the rail. (No pre-merge build was compared; "unchanged" = none of the new rail / overlay / keep-open behaviour appears.)
Shots: 1440 · 1440 after Ctrl+B · 700 drawer

5. The wide-window state is kept · REWORK

  • Works: collapse at 1440 (48 + 1392) → 1280 (rail, 48 + 1232) → 1440 gives collapsed 48 + 1392 again; also with overlay use in between, a reload at 1280, widening while the overlay is open, and expanded → narrow → wide (256 + 1184).
  • Fails: turning "Keep sidebar open" on and then off on the narrow window overwrites the wide-window choice. Collapse at 1440 → 1280 → open overlay → keep open on → off (rail back at 48 + 1232) → 1440: expected collapsed 48 + 1392, got expanded 256 + 1184, still expanded after a reload; 3 of 3 runs. The reverse also breaks: expanded at 1440 → 1280 → keep open on → Ctrl+B → keep open off → widen gives collapsed 48 + 1392 though nothing was collapsed on the wide window. With keep open left on, widening shows 256 px instead of the collapsed wide choice.
  • Shots: 1440 collapsed · 1280 rail · 1440 collapsed restored · after keep-open on/off: expanded instead of collapsed

The boards

No v4 board shows the narrow-window sidebar (no 48 px rail, overlay or keep-open item). The seven desktop boards (RecordPageV4*, RecordDrawerV4*) draw a fixed expanded sidebar 200 px wide with ☰ in the top bar; SidebarReports shows four 220 px sidebar mock-ups about "All reports"; RecordMobile shows phones with ☰ and no sidebar. The live expanded sidebar is 256 px against the boards' 200 / 220; not graded. Example board shot

Other observations (not verdicts)

  1. With keep open on, Ctrl+B or the app bar button collapses to a docked 48 px strip that looks like the rail but carries the pin at its foot; a background click there does nothing, and it stays collapsed after a reload although keep open is still on.
  2. With a record open in a modal with a backdrop (Contacts → Grace), Ctrl+B opens the sidebar overlay behind that backdrop, unseen and unusable; one Esc then both folds the hidden overlay and closes the record.
  3. At 700 px, choosing a page in the drawer navigates but leaves the drawer open (may be prior behaviour).
  4. During loading the rail briefly shows a generic database icon for every entry before the real icons arrive.

Evidence (never-merged branch claude/issue-27593-proxy-evidence @ e1844a1f, folder evidence/27593-proxy/): all shots, readings/readings.json (summary + raw readings) and the Playwright scripts.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

1 participant