mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 19:46:16 +03:00
feat(app): try header separators and spacing from Knobs (#57925)
This commit is contained in:
committed by
GitHub
Unverified
parent
4091a02613
commit
cf562213a3
@@ -229,28 +229,19 @@ test.describe("location", () => {
|
||||
header.evaluate((element) => {
|
||||
const icon = element.querySelector('svg:has(use[href="#opencode-v2-icon-outline-worktree"])')
|
||||
const before = icon?.previousElementSibling?.getBoundingClientRect()
|
||||
const separator = element.querySelector('[data-slot="session-title-separator"]')
|
||||
const after = separator?.getBoundingClientRect()
|
||||
const title = element.querySelector("h1")
|
||||
const avatar = element
|
||||
.querySelector('[data-slot="session-location-trigger"]')
|
||||
?.firstElementChild?.getBoundingClientRect()
|
||||
const bounds = icon?.getBoundingClientRect()
|
||||
|
||||
return bounds && before && after && avatar && separator && title
|
||||
? [
|
||||
before.left - avatar.right,
|
||||
bounds.left - before.right,
|
||||
after.left - bounds.right,
|
||||
title.getBoundingClientRect().left +
|
||||
parseFloat(getComputedStyle(title).paddingLeft) -
|
||||
after.right +
|
||||
parseFloat(getComputedStyle(separator).paddingRight),
|
||||
]
|
||||
: []
|
||||
return bounds && before && avatar ? [before.left - avatar.right, bounds.left - before.right] : []
|
||||
}),
|
||||
)
|
||||
.toEqual([10, 8, 8, 10])
|
||||
.toEqual([10, 8])
|
||||
const separator = header.locator('[data-slot="session-title-separator"]')
|
||||
await expect(separator).toBeVisible()
|
||||
await expect(separator).toHaveText("")
|
||||
await expect(separator).toHaveAttribute("aria-hidden", "true")
|
||||
const location = header.locator('[data-slot="session-location-trigger"]')
|
||||
await location.hover()
|
||||
await expect(location).toHaveCSS("padding-inline-start", "4px")
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { motionTuning } from "@opencode/ui/motion-tuning"
|
||||
import { Match, Switch } from "solid-js"
|
||||
import { createStore } from "solid-js/store"
|
||||
|
||||
const group = "Experiments"
|
||||
|
||||
const glyph = motionTuning.choice("sessionHeader.separator", {
|
||||
label: "Header separator",
|
||||
group,
|
||||
value: "divider",
|
||||
labels: { divider: "Divider", slash: "Slash", chevron: "Chevron", bold: "Bold chevron", dot: "Dot" },
|
||||
})
|
||||
|
||||
const size = motionTuning.number("sessionHeader.separator.size", {
|
||||
label: "Separator size",
|
||||
group,
|
||||
value: 14,
|
||||
min: 6,
|
||||
max: 24,
|
||||
step: 1,
|
||||
unit: "px",
|
||||
})
|
||||
|
||||
const weight = motionTuning.number("sessionHeader.separator.weight", {
|
||||
label: "Separator weight",
|
||||
group,
|
||||
value: 1,
|
||||
min: 0.5,
|
||||
max: 3,
|
||||
step: 0.25,
|
||||
unit: "px",
|
||||
})
|
||||
|
||||
const opacity = motionTuning.number("sessionHeader.separator.opacity", {
|
||||
label: "Separator opacity",
|
||||
group,
|
||||
value: 1,
|
||||
min: 0.2,
|
||||
max: 1,
|
||||
step: 0.05,
|
||||
})
|
||||
|
||||
const gap = motionTuning.number("sessionHeader.separator.gap", {
|
||||
label: "Separator gap (ink to ink)",
|
||||
group,
|
||||
value: 10,
|
||||
min: 4,
|
||||
max: 20,
|
||||
step: 0.5,
|
||||
unit: "px",
|
||||
})
|
||||
|
||||
const read = () => ({ glyph: glyph(), size: size(), weight: weight(), opacity: opacity(), gap: gap() })
|
||||
|
||||
const [tuning, setTuning] = createStore(read())
|
||||
|
||||
motionTuning.subscribe(() => setTuning(read()))
|
||||
|
||||
/**
|
||||
* Sits between breadcrumb segments with `gap` of visible space on both sides. `before` and `after` are the space the
|
||||
* neighbouring segments already leave between their ink and this element, so the margins make up the rest.
|
||||
*/
|
||||
export function SessionHeaderSeparator(props: { before?: number; after?: number }) {
|
||||
// The ink of every glyph fills its box, so the gap is exact; the text beside it keeps ~0.5px of side bearing.
|
||||
const width = () => {
|
||||
if (tuning.glyph === "divider") return tuning.weight
|
||||
|
||||
if (tuning.glyph === "dot") return tuning.size / 4
|
||||
|
||||
return tuning.size * 0.45
|
||||
}
|
||||
|
||||
const stroke = () => tuning.weight * (tuning.glyph === "bold" ? 2 : 1)
|
||||
|
||||
return (
|
||||
<span
|
||||
data-slot="session-title-separator"
|
||||
data-glyph={tuning.glyph}
|
||||
class="session-header-separator text-v2-text-text-faint"
|
||||
style={{
|
||||
"--separator-gap": `${tuning.gap}px`,
|
||||
"--separator-before": props.before === undefined ? undefined : `${props.before}px`,
|
||||
"--separator-after": props.after === undefined ? undefined : `${props.after}px`,
|
||||
opacity: tuning.opacity,
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Switch>
|
||||
<Match when={tuning.glyph === "divider"}>
|
||||
<span class="block rounded-full bg-current" style={{ width: `${width()}px`, height: `${tuning.size}px` }} />
|
||||
</Match>
|
||||
<Match when={tuning.glyph === "dot"}>
|
||||
<span class="block rounded-full bg-current" style={{ width: `${width()}px`, height: `${width()}px` }} />
|
||||
</Match>
|
||||
<Match when={tuning.glyph === "slash"}>
|
||||
<svg
|
||||
class="block fill-none stroke-current"
|
||||
width={tuning.size * 0.4}
|
||||
height={tuning.size}
|
||||
viewBox={`0 0 ${tuning.size * 0.4} ${tuning.size}`}
|
||||
stroke-width={tuning.weight}
|
||||
stroke-linecap="round"
|
||||
>
|
||||
<path
|
||||
d={`M${tuning.weight / 2} ${tuning.size - tuning.weight / 2}L${tuning.size * 0.4 - tuning.weight / 2} ${tuning.weight / 2}`}
|
||||
/>
|
||||
</svg>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<svg
|
||||
class="block fill-none stroke-current"
|
||||
width={width()}
|
||||
height={tuning.size}
|
||||
viewBox={`0 0 ${width()} ${tuning.size}`}
|
||||
stroke-width={stroke()}
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d={`M${stroke() / 2} ${stroke() / 2}L${width() - stroke() / 2} ${tuning.size / 2}L${stroke() / 2} ${tuning.size - stroke() / 2}`}
|
||||
/>
|
||||
</svg>
|
||||
</Match>
|
||||
</Switch>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -42,9 +42,17 @@
|
||||
padding-inline: 0;
|
||||
}
|
||||
|
||||
.session-header-location > .session-location-separator[data-variant="slash"] {
|
||||
/* Together with the row's 2px gap and the title's 4px padding, leave 10px before the title. */
|
||||
padding-inline: 0 4px;
|
||||
/* The icon-only trigger sits 4px from its icon and the row adds a 4px gap; the avatar's 2px margin counts too. */
|
||||
.session-header-location > .session-header-separator {
|
||||
--separator-before: 10px;
|
||||
}
|
||||
|
||||
.session-header-location > .session-header-separator:dir(rtl) {
|
||||
--separator-before: 9px;
|
||||
}
|
||||
|
||||
.session-header-location:has([data-slot="session-location-worktree"]) > .session-header-separator {
|
||||
--separator-before: 9px;
|
||||
}
|
||||
|
||||
/* Keep the names available to assistive tech while the icons leave room for the title. */
|
||||
@@ -85,11 +93,16 @@
|
||||
top: 48px;
|
||||
}
|
||||
|
||||
/* The "/" does not mirror and its side bearings differ, so each direction gets its own padding to centre the ink between the segments. */
|
||||
.session-location-separator {
|
||||
padding-inline: 3.5px 3px;
|
||||
.session-header-separator {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
/* The neighbouring segments' own padding counts toward the gap; 0.5px is the text's side bearing. */
|
||||
margin-inline: calc(var(--separator-gap) - var(--separator-before, 6px) - 0.5px)
|
||||
calc(var(--separator-gap) - var(--separator-after, 6px) - 0.5px);
|
||||
}
|
||||
|
||||
.session-location-separator:dir(rtl) {
|
||||
padding-inline: 3.25px 3.5px;
|
||||
.session-header-separator[data-glyph="chevron"]:dir(rtl) > svg,
|
||||
.session-header-separator[data-glyph="bold"]:dir(rtl) > svg {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { isProjectDirectory, workspaceDirectories } from "@/workspaces/paths"
|
||||
import { sessionHref } from "@/shell/routes/session"
|
||||
import { showToast } from "@/shell/notifications/toast"
|
||||
import { sessionTitle } from "./title"
|
||||
import { SessionHeaderSeparator } from "./header/separator"
|
||||
import { SessionWorkingIndicator } from "./header/session-working-indicator"
|
||||
import { CollapsedSidebarToggleSpace } from "@/shell/sidebar-toggle"
|
||||
import "./session-identity-header.css"
|
||||
@@ -341,13 +342,7 @@ export function SessionLocation(props: {
|
||||
</Menu.Portal>
|
||||
</Menu>
|
||||
<Show when={props.trailing}>
|
||||
<span
|
||||
data-slot="session-title-separator"
|
||||
class="session-location-separator shrink-0 text-[11px] font-medium text-v2-text-text-faint"
|
||||
aria-hidden="true"
|
||||
>
|
||||
/
|
||||
</span>
|
||||
<SessionHeaderSeparator />
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
@@ -408,32 +403,20 @@ export function SessionAncestorTrail(props: {
|
||||
data-session-id={ancestor.id}
|
||||
title={ancestor.title}
|
||||
class="max-w-[min(200px,40vw)] shrink-0 truncate text-[13px] font-[530] leading-4 tracking-[-0.04px] text-v2-text-text-faint transition-colors hover:text-v2-text-text-muted"
|
||||
classList={{ "ps-1": index() === 0, "ps-2": index() > 0 }}
|
||||
classList={{ "ps-1": index() === 0 }}
|
||||
onClick={() => open(props.sessionID, ancestor.id)}
|
||||
>
|
||||
<bdi dir="auto">{ancestor.title}</bdi>
|
||||
</button>
|
||||
<Show when={index() < ancestors().length - 1}>
|
||||
<span
|
||||
data-slot="session-title-separator"
|
||||
class="-translate-y-[0.5px] shrink-0 ps-2 pe-1 text-[11px] font-medium text-v2-text-text-faint"
|
||||
aria-hidden="true"
|
||||
>
|
||||
/
|
||||
</span>
|
||||
<SessionHeaderSeparator before={0} after={0} />
|
||||
</Show>
|
||||
</>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
<Show when={props.trailing}>
|
||||
<span
|
||||
data-slot="session-title-separator"
|
||||
class="-translate-y-[0.5px] shrink-0 ps-2 pe-1 text-[11px] font-medium text-v2-text-text-faint"
|
||||
aria-hidden="true"
|
||||
>
|
||||
/
|
||||
</span>
|
||||
<SessionHeaderSeparator before={0} />
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user