Skip to content
Draft
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
57 changes: 48 additions & 9 deletions src/components/GobanView/GobanView.css
Original file line number Diff line number Diff line change
Expand Up @@ -61,19 +61,39 @@
background: var(--shade5);
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
margin: 1rem 1rem 1rem 0;
/* The 1.25rem on the left creates a consistent gap between the
* goban area and the sidebar across all GobanView consumers (game,
* puzzle, joseki). The Game view places its fullscreen-toggle icon
* inside this gap; on other pages it's just visual breathing room.
*
* Top is 0 because --below-navbar already provides a small (~0.3rem)
* spacer below the navbar — adding more margin doubles up. Bottom
* mirrors that natural top gap via --goban-view-bottom-margin
* (0.3rem normally, 0 below 700px viewport height) so the framing
* stays symmetric and the sidebar fills the available height on
* short viewports. */
margin: 0 1rem var(--goban-view-bottom-margin) 1.25rem;
overflow: hidden;
}

/* Header pinned above the scrollable sidebar content. Hidden via
* :empty when the `header` prop's component renders nothing, so we
* don't leave a stray bordered band at the top. */
.GobanView-header {
flex-shrink: 0;
padding: 0.6rem 0.875rem;
font-size: 1.5rem;
padding: 0.4rem 0.75rem;
font-size: 1.1rem;
font-weight: 600;
text-align: center;
color: var(--fg);
background: var(--shade4);
border-bottom: 1px solid var(--shade3);
border-top-left-radius: inherit;
border-top-right-radius: inherit;

&:empty {
display: none;
}
}

.GobanView-sidebar-content {
Expand All @@ -83,18 +103,28 @@
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 0.75rem;
padding-bottom: 0.75rem;
}

/* Tab panels */
.GobanView-tab-panel {
padding: 0.875rem 0;
padding-bottom: 0.875rem;
position: relative;

&.hidden {
display: none;
}

/* Non-takeover panels (always + toggle) render at their natural
* content height; the sidebar scrolls when their combined height
* exceeds the viewport. Without this, flex-shrink would squash
* them so the chat (which has a fixed inner height) overflows
* visibly into the panel below. */
&.always,
&.toggle {
flex-shrink: 0;
}

&.takeover {
flex-grow: 1;
display: flex;
Expand Down Expand Up @@ -151,7 +181,7 @@
}

.GobanView-sidebar-content {
padding: 0.5rem;
padding-bottom: 0.5rem;
gap: 0.5rem;
}

Expand Down Expand Up @@ -179,18 +209,27 @@
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.5rem;
/* Top breathing room only — consumers run flush to the
* viewport's left/right/bottom edges in portrait. */
padding: 0.5rem 0 0;
background: var(--shade5);

.GobanView-tab-panel {
padding: 0.75rem 0;
padding-bottom: 0.75rem;
}

/* The always panel runs to the viewport bottom on mobile —
* the chat area inside it has its own scroll, so the wrapper
* doesn't need its own bottom breathing room. */
.GobanView-tab-panel.always {
padding-bottom: 0;
}
}

/* Takeover panels overlay the scroll area */
.GobanView-tab-panel.takeover {
display: none;
padding: 0.75rem 0;
padding-bottom: 0.75rem;

&.active {
display: flex;
Expand Down
31 changes: 27 additions & 4 deletions src/components/GobanView/GobanView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export interface GobanViewRef {
/** Open or close a takeover programmatically. Pass null to close any
* currently-active takeover. */
setActiveTakeover: (id: string | null) => void;
/** The current root DOM element of this GobanView. Returns null between
* unmount and remount. Lets consumers scope DOM queries to this
* instance instead of querying document globally. */
getRootElement: () => HTMLDivElement | null;
}

interface GobanViewProps {
Expand All @@ -70,6 +74,14 @@ interface GobanViewProps {
* above the goban (portrait). Stays visible across takeovers so
* consumers can use it to label the current view. */
header?: React.ReactNode;
/** Forwarded to the GobanContainer — fires when the user scrolls the wheel
* over the board. Used by the Game view for scroll-to-navigate. */
onWheel?: React.WheelEventHandler<HTMLDivElement>;
/** Optional content rendered inside the center column above the goban.
* Used by the Game view's "stacked" layout to put a player card on top. */
centerTop?: React.ReactNode;
/** Optional content rendered inside the center column below the goban. */
centerBottom?: React.ReactNode;
ref?: React.Ref<GobanViewRef>;
}

Expand Down Expand Up @@ -111,6 +123,9 @@ function GobanViewComponent({
defaultActiveTakeover,
customSlider,
header,
onWheel,
centerTop,
centerBottom,
ref,
}: GobanViewProps): React.ReactElement {
const { tabs, others } = React.useMemo(() => partitionChildren(children), [children]);
Expand Down Expand Up @@ -183,6 +198,7 @@ function GobanViewComponent({
tabsRef.current = tabs;
const activeTakeoverRef = React.useRef(activeTakeover);
activeTakeoverRef.current = activeTakeover;
const rootRef = React.useRef<HTMLDivElement>(null);

React.useImperativeHandle(
ref,
Expand All @@ -206,6 +222,7 @@ function GobanViewComponent({
opened?.onToggle?.(true);
}
},
getRootElement: () => rootRef.current,
}),
[],
);
Expand Down Expand Up @@ -299,6 +316,7 @@ function GobanViewComponent({
<GobanControllerContext.Provider value={controller}>
<GobanViewStateContext.Provider value={tabState}>
<div
ref={rootRef}
className={
`GobanView portrait` +
(squashed ? " squashed" : "") +
Expand All @@ -307,9 +325,11 @@ function GobanViewComponent({
(className ? ` ${className}` : "")
}
>
{header && <div className="GobanView-header">{header}</div>}
<div className="GobanView-header">{header}</div>
<div className="GobanView-center">
<GobanContainer onResize={onResize} />
{centerTop}
<GobanContainer onResize={onResize} onWheel={onWheel} />
{centerBottom}
</div>
<div className="GobanView-mobile-scroll">
{orderedPanels.map((t) => renderPanel(t, isInlineVisible(t)))}
Expand All @@ -328,6 +348,7 @@ function GobanViewComponent({
<GobanControllerContext.Provider value={controller}>
<GobanViewStateContext.Provider value={tabState}>
<div
ref={rootRef}
className={
`GobanView ${viewMode}` +
(squashed ? " squashed" : "") +
Expand All @@ -337,10 +358,12 @@ function GobanViewComponent({
}
>
<div className="GobanView-center">
<GobanContainer onResize={onResize} />
{centerTop}
<GobanContainer onResize={onResize} onWheel={onWheel} />
{centerBottom}
</div>
<div className="GobanView-sidebar">
{header && <div className="GobanView-header">{header}</div>}
<div className="GobanView-header">{header}</div>
<div className="GobanView-sidebar-content">
{inlinePanels.map((t) => renderPanel(t, isInlineVisible(t)))}
{takeoverPanels.map((t) => renderPanel(t, activeTakeover === t.id))}
Expand Down
18 changes: 10 additions & 8 deletions src/components/GobanView/GobanViewTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,17 @@ export interface GobanViewTabProps {
/** Suppress the tab's button in the bottom tab bar. The panel still
* renders when activated via the imperative setActiveTakeover ref. */
hideFromBar?: boolean;
/** Action-tab click handler. The MouseEvent is forwarded so consumers can
* use `event.currentTarget` to anchor a popover, etc. */
onClick?: (event?: React.MouseEvent<HTMLButtonElement>) => void;
/** For takeover tabs: fires whenever this tab transitions between active
* and inactive. Called with `true` when the user clicks the tab to open
* it, and with `false` when the tab deactivates — whether by the user
* clicking it again to close, another takeover being opened
* (displacement), or GobanView forcibly closing it because the tab has
* been removed from the render tree or gained `disabled`. Consumers
* should treat this as the sole authoritative signal to tear down
* per-tab state. */
/** For takeover and toggle tabs: fires whenever this tab transitions
* between active and inactive. Called with `true` when the user clicks
* the tab to open it, and with `false` when the tab deactivates —
* whether by the user clicking it again to close, another takeover
* being opened (displacement), or GobanView forcibly closing it because
* the tab has been removed from the render tree or gained `disabled`.
* Consumers should treat this as the sole authoritative signal to tear
* down per-tab state, or to persist toggle visibility to a preference. */
onToggle?: (active: boolean) => void;
children?: React.ReactNode;
}
Expand Down
4 changes: 3 additions & 1 deletion src/components/GobanView/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ export function TabBar({ tabs }: TabBarProps): React.ReactElement {

const handleClick = (tab: TabDefinition, event: React.MouseEvent<HTMLButtonElement>) => {
if (tab.type === "toggle") {
state.setToggle(tab.id, !state.toggleVisibility[tab.id]);
const willBeVisible = !state.toggleVisibility[tab.id];
state.setToggle(tab.id, willBeVisible);
tab.onToggle?.(willBeVisible);
} else if (tab.type === "takeover") {
const prevActiveId = state.activeTakeover;
const willBeActive = prevActiveId !== tab.id;
Expand Down
12 changes: 12 additions & 0 deletions src/global_styl/01_variables.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
--private-chat-width: 20rem;
--navbar-height: 3rem;
--below-navbar: 3.3rem;
--goban-view-bottom-margin: 0.3rem;
--goban-view-sidebar-width: 400px;
--goban-view-tab-bar-height: 3rem;

Expand Down Expand Up @@ -115,6 +116,17 @@
}
}

/* The GobanView sidebar flushes against the navbar's natural ~0.3rem
* lower spacer (--below-navbar minus --navbar-height) at the top, and
* mirrors that same 0.3rem at the bottom — visually symmetric framing.
* On short viewports we drop the bottom margin so the sidebar fills the
* available height. */
@media (max-height: 700px) {
:root {
--goban-view-bottom-margin: 0;
}
}

/* Light theme mixin */
@define-mixin light {
/* Build-time variables for color derivation */
Expand Down
3 changes: 3 additions & 0 deletions src/lib/preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ export const defaults = {
"hide-ranks": false,
"label-positioning": "all" as LabelPosition,
"label-positioning-puzzles": "all" as LabelPosition,
"game.layout": "standard" as "standard" | "stacked",
"game.chat-enabled": true,
"moderator.game-moderator-tab-visible": true,
language: "auto",
"move-tree-numbering": "move-number" as "none" | "move-coordinates" | "move-number",
"new-game-board-size": 19,
Expand Down
Loading
Loading