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
4 changes: 3 additions & 1 deletion src/app/shell/AppPanes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -227,14 +227,16 @@ export function openCanvasPath(app: ShellView, path: string | undefined, occurre
export function CanvasCovers({ app }: { readonly app: ShellView }) {
return (
<>
{app.varsGroup && app.leftTab === 'variables' && (
{app.varsGroup && (
<VariablesView
key={app.project.path}
project={app.project}
selected={app.varsGroup}
hidden={app.leftTab !== 'variables'}
showToast={app.showToast}
onRecordUndo={app.pushCommand}
onClose={() => app.setVarsGroup(undefined)}
onDock={() => app.setLeftTab('variables')}
/>
)}
{app.contentName && (
Expand Down
143 changes: 65 additions & 78 deletions src/app/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1965,6 +1965,7 @@ html[data-platform='win32'] .titlebar {
overflow: hidden;
}
.code-window-header {
touch-action: none;
display: flex;
align-items: center;
gap: 8px;
Expand Down Expand Up @@ -1996,67 +1997,6 @@ html[data-platform='win32'] .titlebar {
height: 100%;
}

.cw-rz {
position: absolute;
z-index: 2;
}
.cw-rz-n {
top: -3px;
left: 8px;
right: 8px;
height: 6px;
cursor: ns-resize;
}
.cw-rz-s {
bottom: -3px;
left: 8px;
right: 8px;
height: 6px;
cursor: ns-resize;
}
.cw-rz-e {
right: -3px;
top: 8px;
bottom: 8px;
width: 6px;
cursor: ew-resize;
}
.cw-rz-w {
left: -3px;
top: 8px;
bottom: 8px;
width: 6px;
cursor: ew-resize;
}
.cw-rz-se {
right: -3px;
bottom: -3px;
width: 12px;
height: 12px;
cursor: nwse-resize;
}
.cw-rz-nw {
left: -3px;
top: -3px;
width: 12px;
height: 12px;
cursor: nwse-resize;
}
.cw-rz-sw {
left: -3px;
bottom: -3px;
width: 12px;
height: 12px;
cursor: nesw-resize;
}
.cw-rz-ne {
right: -3px;
top: -3px;
width: 12px;
height: 12px;
cursor: nesw-resize;
}

/* Breadcrumbs (canvas toolbar) */
.crumbs {
flex: 1;
Expand Down Expand Up @@ -6636,11 +6576,19 @@ body > .embed-editor_provenance {
The same sheet the CMS uses, with a table in it. Columns are either the
rules a variable is declared in (a theme and its modes) or the things a
family of names describes (h1…h6) — see electron/project/cssVars.ts. */
.vars-view .cms-detail {
width: 100%;
}
/* Shared column bounds keep each group's headings and values aligned. Values
shrink from their preferred width before wide sets need horizontal scroll. */
.vars-sheet {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
overflow: hidden;
--vars-inset: 24px;
--vars-name-col: 200px;
--vars-col-min: 180px;
--vars-col: 260px;
}
.vars-body {
padding-bottom: 40px;
/* The sheet's own left/right inset. It lives INSIDE the rows (the first grid
Expand All @@ -6649,15 +6597,25 @@ body > .embed-editor_provenance {
has an edge to sit against. */
padding-left: 0;
padding-right: 0;
--vars-inset: 24px;
--vars-name-col: 200px;
--vars-col-min: 180px;
--vars-col: 260px;
/* One group at a time scrolls sideways (each table's value stack is its own
scroller), so the sheet itself never does — otherwise a single wide group
would drag every other group's names off with it. */
/* Value strips share a position; names remain outside those scrollers. */
overflow-x: hidden;
}
.vars-scrollbar-row:not([hidden]) {
display: grid;
grid-template-columns: calc(var(--vars-inset) + 18px + var(--vars-name-col)) minmax(0, 1fr);
flex-shrink: 0;
padding-block: 3px;
border-top: 1px solid var(--border);
background: var(--bg);
}
.vars-horizontal-scroll {
grid-column: 2;
min-width: 0;
height: 14px;
overflow-x: scroll;
overflow-y: hidden;
overscroll-behavior-x: contain;
}
/* A group is two stacks: the labels, which never move, and a scroller holding
its values. Only the right-hand one scrolls, so a row always says what it is
— and nothing has to be held in place against the scroll. */
Expand All @@ -6667,24 +6625,24 @@ body > .embed-editor_provenance {
grid-template-columns: auto minmax(0, 1fr);
align-items: start;
}
/* The horizontal bar lands on the group's "New variable" line, because the
value side has no row of its own down there — see Table. The bar sits at the
bottom of this box, so the padding is what centres it in that line: half a
row (15px) puts the bar's own middle (it is 8px tall) on the row's middle. */
/* The value stack no longer scrolls as one box. A heading has to sit outside a
horizontal scroller to stick to the sheet — a box that scrolls in x is a
scroll container in both axes, and `position: sticky` inside one resolves
against that box, so the headings rode the rows out of sight. The scrolling
moved down to the rows; the heading strip above them is held level with it
from VariablesView. The room for the scrollbar goes with the scroller. */
by the sheet's shared scroll coordinator. */
.vars-scroll {
min-width: 0;
}
.vars-scroll-rows {
overflow-x: auto;
overscroll-behavior-x: contain;
min-width: 0;
padding-bottom: 19px;
/* Keep native trackpad scrolling while presenting only the shared bottom bar. */
scrollbar-width: none;
}
.vars-scroll-rows::-webkit-scrollbar {
display: none;
}
.vars-scroll-head {
overflow: hidden;
Expand Down Expand Up @@ -6726,10 +6684,39 @@ body > .embed-editor_provenance {
.vars-body > .props-empty {
padding-left: var(--vars-inset);
}
.vars-view > .dockable-window-header {
position: relative;
}
.vars-window-heading {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
max-width: calc(50% - 108px);
}
.vars-view .cms-detail-title,
.vars-view .cms-detail-path {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.vars-view .cms-saved {
flex-shrink: 0;
}
.vars-view .cms-detail-path {
max-width: calc(50% - 140px);
}
.vars-view .dockable-window-toggle {
margin-left: 0;
}
.vars-search {
position: absolute;
left: 50%;
transform: translateX(-50%);
min-width: 80px;
height: 24px;
width: 180px;
margin-left: auto;
padding: 0 8px;
font-size: 11px;
}
Expand Down
102 changes: 13 additions & 89 deletions src/features/code/CodeWindow.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,11 @@
import Popup from '../../ui/Popup';
import React, { useState } from 'react';
import type { PointerEvent } from 'react';
import type React from 'react';
import CodeEditor from '../../ui/CodeEditor';
import type { CodeEditorProps } from '../../ui/CodeEditor';
import { CloseIcon, CodeIcon } from '../../ui/Icons';
import { usePointerDrag } from '../../ui/usePointerDrag';
import { useWindowRectangle } from '../../ui/useWindowRectangle';
import { WindowResizeHandles } from '../../ui/WindowResizeHandles';

const clamp = (value: number, minimum: number, maximum: number): number =>
Math.min(Math.max(value, minimum), maximum);
const WIDTH_PX_MIN = 340;
const HEIGHT_PX_MIN = 220;
const EDGES = ['n', 's', 'e', 'w', 'se', 'sw', 'ne', 'nw'] as const;
type Edge = (typeof EDGES)[number];
interface Rectangle {
readonly x: number;
readonly y: number;
readonly w: number;
readonly h: number;
}
interface CodeWindowProps extends CodeEditorProps {
readonly title: string;
readonly onClose: () => void;
Expand All @@ -27,7 +15,8 @@ interface CodeWindowProps extends CodeEditorProps {
// A single pointer session owns movement and cleanup, including closing mid-drag.
export default function CodeWindow(props: CodeWindowProps) {
const { title, language, value, onChange, onClose, editorKey, revealLine } = props;
const { rectangle, startDrag, startResize } = useWindowRectangle();
const windowState = useWindowRectangle({ width: 340, height: 220 });
const { rectangle } = windowState;
return (
<Popup
mode="popover"
Expand All @@ -36,9 +25,14 @@ export default function CodeWindow(props: CodeWindowProps) {
className="code-window"
role="dialog"
label={title}
style={{ left: rectangle.x, top: rectangle.y, width: rectangle.w, height: rectangle.h }}
style={{
left: rectangle.x,
top: rectangle.y,
width: rectangle.width,
height: rectangle.height,
}}
>
<div className="code-window-header" onPointerDown={startDrag}>
<div className="code-window-header" onPointerDown={(event) => windowState.start(event)}>
<CodeIcon size={13} />
<span className="code-window-title">{title}</span>
<span className="type-tag">{language}</span>
Expand All @@ -56,77 +50,7 @@ export default function CodeWindow(props: CodeWindowProps) {
revealLine={revealLine}
/>
</div>
{EDGES.map((edge) => (
<div key={edge} className={`cw-rz cw-rz-${edge}`} onPointerDown={startResize(edge)} />
))}
<WindowResizeHandles windowState={windowState} />
</Popup>
);
}

function useWindowRectangle() {
const [rectangle, setRectangle] = useState(windowRectangleInitial);
const startPointer = usePointerDrag();
const startDrag = (event: PointerEvent<HTMLDivElement>): void => {
const ElementType = event.currentTarget.ownerDocument.defaultView?.Element;
if (ElementType && event.target instanceof ElementType && event.target.closest('button')) {
return;
}
event.preventDefault();
const { clientX, clientY } = event;
startPointer(event, {
onMove: (next) => {
setRectangle((current) => ({
...current,
x: clamp(rectangle.x + next.clientX - clientX, -current.w + 120, window.innerWidth - 60),
y: clamp(rectangle.y + next.clientY - clientY, 42, window.innerHeight - 48),
}));
},
});
};
const startResize =
(edge: Edge) =>
(event: PointerEvent<HTMLDivElement>): void => {
event.preventDefault();
event.stopPropagation();
const { clientX, clientY } = event;
startPointer(event, {
onMove: (next) => {
setRectangle(
windowRectangleResize(rectangle, edge, next.clientX - clientX, next.clientY - clientY),
);
},
});
};
return { rectangle, startDrag, startResize };
}

function windowRectangleInitial(): Rectangle {
// Proportional dimensions make real code usable while keeping the canvas visible.
const width = clamp(Math.round(window.innerWidth * 0.52), WIDTH_PX_MIN, 1040);
const height = clamp(
Math.round(window.innerHeight * 0.78),
HEIGHT_PX_MIN,
window.innerHeight - 140,
);
return { x: Math.max(60, window.innerWidth - width - 80), y: 96, w: width, h: height };
}

function windowRectangleResize(start: Rectangle, edge: Edge, dx: number, dy: number): Rectangle {
// Mutation stays local to this calculation; the state is replaced atomically.
let { x, y, w: width, h: height } = start;
if (edge.includes('e')) {
width = Math.max(WIDTH_PX_MIN, start.w + dx);
}
if (edge.includes('s')) {
height = Math.max(HEIGHT_PX_MIN, start.h + dy);
}
if (edge.includes('w')) {
width = Math.max(WIDTH_PX_MIN, start.w - dx);
x = start.x + (start.w - width);
}
if (edge.includes('n')) {
height = Math.max(HEIGHT_PX_MIN, start.h - dy);
y = start.y + (start.h - height);
}
return { x, y, w: width, h: height };
}
Loading