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
20 changes: 15 additions & 5 deletions apps/mapgen-studio/src/app/LeftDock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,30 @@ import type { ReactNode } from "react";
export type LeftDockProps = {
/** Top offset (px) so the dock clears the floating header. */
top: number;
/** Bottom offset (px) so the dock clears the floating footer. */
bottom: number;
/** The recipe/config authoring panel rendered inside the dock. */
children: ReactNode;
};

/**
* `LeftDock` — the left-anchored floating dock that hosts the recipe authoring
* panel (architecture/10 §4). It owns only the absolute positioning + z-index of
* the left rail; the panel content is composed in by `StudioShell`. Extracting the
* positioning frame from the authoring closure keeps the shell layout declarative
* without changing the rendered DOM (same `absolute left-4 z-10` placement).
* the left rail; the panel content is composed in by `StudioShell`. The dock is
* pinned between the measured header (`top`) and the footer reserve (`bottom`)
* so the panel inside can claim the full working column (`max-h-full`) while
* still shrinking to fit short content.
*
* `pointer-events-none`: the dock spans header→footer even when its panel is
* short; the empty remainder must pass map drag/zoom through. The panel restores
* `pointer-events-auto` on itself.
*/
export function LeftDock({ top, children }: LeftDockProps) {
export function LeftDock({ top, bottom, children }: LeftDockProps) {
return (
<aside aria-label="Recipe and configuration" className="absolute left-4 z-10" style={{ top }}>
<aside
aria-label="Recipe and configuration"
className="absolute left-4 z-10 flex flex-col items-stretch pointer-events-none"
style={{ top, bottom }}>
{children}
</aside>
);
Expand Down
20 changes: 15 additions & 5 deletions apps/mapgen-studio/src/app/RightDock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,30 @@ import type { ReactNode } from "react";
export type RightDockProps = {
/** Top offset (px) so the dock clears the floating header. */
top: number;
/** Bottom offset (px) so the dock clears the floating footer. */
bottom: number;
/** The explore/inspection panel rendered inside the dock. */
children: ReactNode;
};

/**
* `RightDock` — the right-anchored floating dock that hosts the explore panel
* (architecture/10 §4). It owns only the absolute positioning + z-index of the
* right rail; the panel content is composed in by `StudioShell`. Extracting the
* positioning frame keeps the shell layout declarative without changing the
* rendered DOM (same `absolute right-4 z-10` placement).
* right rail; the panel content is composed in by `StudioShell`. The dock is
* pinned between the measured header (`top`) and the footer reserve (`bottom`)
* so the panel inside caps at the working column instead of underlapping the
* footer on short viewports.
*
* `pointer-events-none`: the dock spans header→footer even when its panel is
* short; the empty remainder must pass map drag/zoom through. The panel restores
* `pointer-events-auto` on itself.
*/
export function RightDock({ top, children }: RightDockProps) {
export function RightDock({ top, bottom, children }: RightDockProps) {
return (
<aside aria-label="Explore and inspect" className="absolute right-4 z-10" style={{ top }}>
<aside
aria-label="Explore and inspect"
className="absolute right-4 z-10 flex flex-col items-stretch pointer-events-none"
style={{ top, bottom }}>
{children}
</aside>
);
Expand Down
8 changes: 6 additions & 2 deletions apps/mapgen-studio/src/app/StudioShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2176,6 +2176,10 @@ export function StudioShell(props: StudioShellProps) {
setHeaderHeight((prev) => prev === height ? prev : height);
}, []);
const panelTop = LAYOUT.SPACING + headerHeight + LAYOUT.SPACING;
// The docks are pinned between the measured header and the footer bar (which
// sits `bottom-4` and is FOOTER_HEIGHT tall), keeping one SPACING of air on
// each side of the footer.
const panelBottom = LAYOUT.FOOTER_HEIGHT + 2 * LAYOUT.SPACING;

const backgroundGridEnabled = useMemo(() => {
if (!showGrid) return false;
Expand Down Expand Up @@ -2427,8 +2431,8 @@ export function StudioShell(props: StudioShellProps) {
onChange={handleImportFileChange}
/>

<LeftDock top={panelTop}>{leftPanel}</LeftDock>
<RightDock top={panelTop}>{rightPanel}</RightDock>
<LeftDock top={panelTop} bottom={panelBottom}>{leftPanel}</LeftDock>
<RightDock top={panelTop} bottom={panelBottom}>{rightPanel}</RightDock>

{header}
{footer}
Expand Down
7 changes: 1 addition & 6 deletions apps/mapgen-studio/src/ui/components/AppHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,11 @@ import {
type Civ7StudioSetupConfig
} from '../../features/civ7Setup/setupConfig';
import {
LAYOUT,
MAP_SIZE_OPTIONS,
PLAYER_COUNT_OPTIONS,
RESOURCE_MODE_OPTIONS } from
'../constants';
import type { ThemePreference, WorldSettings } from '../types';
export const HEADER_HEIGHT = LAYOUT.HEADER_HEIGHT;
export interface AppHeaderProps {
themePreference: ThemePreference;
onThemeCycle: () => void;
Expand Down Expand Up @@ -101,10 +99,7 @@ export const AppHeader: React.FC<AppHeaderProps> = ({
return (
<header
ref={headerRef}
className="absolute top-4 left-4 right-4 z-20 grid grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-3"
style={{
minHeight: HEADER_HEIGHT
}}>
className="absolute top-4 left-4 right-4 z-20 grid grid-cols-[auto_minmax(0,1fr)_auto] items-start gap-3">

{/* Left: App Brand */}
<div className="shrink-0">
Expand Down
8 changes: 7 additions & 1 deletion apps/mapgen-studio/src/ui/components/ExplorePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
Flame } from
'lucide-react';
import { Tooltip, TooltipContent, TooltipTrigger } from '../../components/ui';
import { LAYOUT } from '../constants';
import type {
StageOption,
StepOption,
Expand Down Expand Up @@ -317,9 +318,14 @@ export const ExplorePanel: React.FC<ExplorePanelProps> = ({
// ==========================================================================
// Render
// ==========================================================================
// Width comes from the LAYOUT geometry authority; the panel caps at the
// dock's header→footer column (`max-h-full`) and scrolls internally instead
// of underlapping the footer on short viewports. pointer-events-auto restores
// interactivity inside the pass-through dock.
return (
<div
className={`flex flex-col w-[260px] rounded-lg border overflow-hidden shadow-lg backdrop-blur-sm ${panelBg} ${panelBorder}`}>
style={{ width: LAYOUT.EXPLORE_PANEL_WIDTH }}
className={`flex flex-col max-h-full rounded-lg border overflow-y-auto overflow-x-hidden custom-scrollbar shadow-lg backdrop-blur-sm pointer-events-auto ${panelBg} ${panelBorder}`}>

{/* 1. STAGE SECTION */}
<div className={`flex-shrink-0 border-b ${borderSubtle}`}>
Expand Down
19 changes: 16 additions & 3 deletions apps/mapgen-studio/src/ui/components/RecipePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
Play } from
'lucide-react';
import { SchemaConfigForm } from '../../features/configOverrides/SchemaConfigForm';
import { LAYOUT } from '../constants';
import {
formatMapConfigSaveDeployPhaseLabel,
type MapConfigSaveDeployStatus,
Expand Down Expand Up @@ -219,8 +220,13 @@ export const RecipePanel: React.FC<RecipePanelProps> = ({
// ==========================================================================
return (
<>
{/* Width comes from the LAYOUT geometry authority (Pass-2: 340px); height
shrinks to fit but is capped by the dock's header→footer column
(`max-h-full`). pointer-events-auto restores interactivity inside the
pass-through dock. */}
<div
className={`flex flex-col w-[280px] max-h-[calc(100vh-180px)] rounded-lg border overflow-hidden shadow-lg backdrop-blur-sm ${panelBg} ${panelBorder}`}>
style={{ width: LAYOUT.PANEL_WIDTH }}
className={`flex flex-col max-h-full rounded-lg border overflow-hidden shadow-lg backdrop-blur-sm pointer-events-auto ${panelBg} ${panelBorder}`}>

{/* Header */}
<div className={`flex-shrink-0 border-b ${borderSubtle}`}>
Expand Down Expand Up @@ -393,9 +399,11 @@ export const RecipePanel: React.FC<RecipePanelProps> = ({
</Tooltip>
</div>

{/* Config Form / JSON */}
{/* Config Form / JSON. pb-6 matches the h-6 scroll-edge fade below:
at full scroll the fade overlays only this padding, never the
last field row. */}
<div
className={`px-3 pb-3 ${overridesDisabled ? 'opacity-40 pointer-events-none select-none' : ''}`}>
className={`px-3 pb-6 ${overridesDisabled ? 'opacity-40 pointer-events-none select-none' : ''}`}>

{showJson ?
<div className="border border-border-subtle rounded p-2.5 max-h-[240px] overflow-auto bg-surface-sunken">
Expand All @@ -417,6 +425,11 @@ export const RecipePanel: React.FC<RecipePanelProps> = ({

}
</div>
{/* Scroll-edge fade: sticky inside the scroll container so mid-scroll
cuts read as "more below" instead of the end of the form. The
negative margin keeps it from adding scroll height; it fades to
the panel surface (popover) and never intercepts the pointer. */}
<div aria-hidden="true" className="sticky bottom-0 -mt-6 h-6 shrink-0 pointer-events-none bg-gradient-to-t from-popover to-transparent" />
</div>
}

Expand Down
2 changes: 1 addition & 1 deletion apps/mapgen-studio/src/ui/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

// Layout components
export { AppBrand } from './AppBrand';
export { AppHeader, HEADER_HEIGHT, type AppHeaderProps } from './AppHeader';
export { AppHeader, type AppHeaderProps } from './AppHeader';
export { AppFooter, FOOTER_HEIGHT, type AppFooterProps } from './AppFooter';

// Panel components
Expand Down
20 changes: 13 additions & 7 deletions apps/mapgen-studio/src/ui/constants/layout.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
// ============================================================================
// LAYOUT
// ============================================================================
// Layout constants for consistent spacing and sizing.
// The single geometry authority for the studio chrome (Pass-2 amendment in
// .interface-design/system.md). Panels consume these via inline style so a
// geometry change is a one-line edit here — no arbitrary-value width classes.
// ============================================================================

export const LAYOUT = {
/** Floating top toolbar reserve in pixels. */
HEADER_HEIGHT: 40,
/**
* Initial estimate of the measured header height (single-row bar). The
* ResizeObserver in `AppHeader` is the authority — this only seeds
* `StudioShell`'s state for the first paint and is NOT a reserved band.
*/
HEADER_HEIGHT: 48,
/** Footer bar height in pixels */
FOOTER_HEIGHT: 56,
/** Default panel width in pixels */
PANEL_WIDTH: 280,
/** Explore panel width in pixels */
EXPLORE_PANEL_WIDTH: 240,
/** Recipe (left) dock panel width in pixels */
PANEL_WIDTH: 340,
/** Explore (right) dock panel width in pixels */
EXPLORE_PANEL_WIDTH: 260,
/** Standard spacing unit in pixels */
SPACING: 16,
/** Border radius presets */
Expand Down
9 changes: 6 additions & 3 deletions openspec/changes/mapgen-studio-layout-geometry/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,12 @@ wraps) and gives the explore panel its first overflow constraint.

### 4. Fade affordance inside the scroll container

A `pointer-events-none sticky bottom-0` gradient (`bg-gradient-to-t from-card`)
inside the scrollable body. Sticky-in-scroll means it disappears naturally at the
end of content (nothing below it to overlay) without any scroll-position JS.
A `pointer-events-none sticky bottom-0` gradient (`bg-gradient-to-t from-popover`)
inside the scrollable body, with a negative top margin so it adds no scroll
height. Because of that margin the fade is always painted at the scroll edge —
including at full scroll — so the form wrapper carries bottom padding equal to
the fade height (`pb-6` vs `h-6`): at the end of content the fade overlays only
padding, never the last field row. No scroll-position JS.

## Risks

Expand Down
18 changes: 9 additions & 9 deletions openspec/changes/mapgen-studio-layout-geometry/tasks.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
## 1. Constants

- [ ] 1.1 `layout.ts`: `PANEL_WIDTH` 280 → 340; `EXPLORE_PANEL_WIDTH` 240 → 260;
- [x] 1.1 `layout.ts`: `PANEL_WIDTH` 280 → 340; `EXPLORE_PANEL_WIDTH` 240 → 260;
`HEADER_HEIGHT` 104 → 48 with doc comment re-stated as "initial estimate of
the measured header height (ResizeObserver is the authority), not a reserve".

## 2. Header

- [ ] 2.1 `AppHeader.tsx`: remove the `style={{ minHeight: HEADER_HEIGHT }}` reserve;
- [x] 2.1 `AppHeader.tsx`: remove the `style={{ minHeight: HEADER_HEIGHT }}` reserve;
keep the ResizeObserver reporting pipeline untouched.

## 3. Docks and panels

- [ ] 3.1 `LeftDock.tsx` / `RightDock.tsx`: accept and apply a `bottom` constraint
- [x] 3.1 `LeftDock.tsx` / `RightDock.tsx`: accept and apply a `bottom` constraint
(`FOOTER_HEIGHT + 2×SPACING` supplied by `StudioShell`); document the new
single-responsibility (top+bottom positioning).
- [ ] 3.2 `RecipePanel.tsx`: root drops `w-[280px]` + `max-h-[calc(100vh-180px)]`
- [x] 3.2 `RecipePanel.tsx`: root drops `w-[280px]` + `max-h-[calc(100vh-180px)]`
for `style` width from `LAYOUT.PANEL_WIDTH` + `max-h-full`.
- [ ] 3.3 `ExplorePanel.tsx`: root drops `w-[260px]` for `LAYOUT.EXPLORE_PANEL_WIDTH`,
- [x] 3.3 `ExplorePanel.tsx`: root drops `w-[260px]` for `LAYOUT.EXPLORE_PANEL_WIDTH`,
gains `max-h-full` and internal overflow scrolling.

## 4. Scroll affordance

- [ ] 4.1 Recipe panel scroll body: add the sticky bottom token-gradient fade
- [x] 4.1 Recipe panel scroll body: add the sticky bottom token-gradient fade
(`pointer-events-none`).

## 5. Verification

- [ ] 5.1 `bun run openspec -- validate mapgen-studio-layout-geometry --strict`
- [ ] 5.2 tsc + mapgen-studio vitest project green
- [ ] 5.3 Visual: DOM-measure dock boxes on :5173 (left 340 wide, top ≈ header+16,
- [x] 5.1 `bun run openspec -- validate mapgen-studio-layout-geometry --strict`
- [x] 5.2 tsc + mapgen-studio vitest project green
- [x] 5.3 Visual: DOM-measure dock boxes on :5173 (left 340 wide, top ≈ header+16,
bottom clear of footer); screenshot dark + light; squint-test the column.