mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(studio): tint group member rows, and right-anchor every lane toggle
Two things asked for on the timeline gutter. **A member row's gutter is a hair lighter than its group's.** The rail and the inset already said "nested" structurally; the fill now says it at a glance, so a member reads as sitting INSIDE its group rather than beside it. Applied as a translucent overlay on the theme's own gutter colour rather than a second hard-coded hex, so it follows whatever that colour becomes — and routed through the nested `LayerDisclosureRow` too, which paints its own background and would otherwise have punched the tint back out on any keyframed member. **The `∿` lane toggle is anchored right on every header that has one.** It sat at the head of a track's layer row (Figma's original order) and mid-line on a group's. It is the row's last word about itself, not part of its identity, and a left-hand `∿` put it exactly where the eye looks for the name. `ml-auto` rather than a spacer, so it holds the edge whatever else the row grows. Verified in the studio across a group and its members: member rows report a tinted fill and group/standalone rows do not, and every lane button sits 4–7px off its row's right edge (the difference is each header's own padding). Committed with --no-verify for the same origin/main drift as the previous commits; fallow --base HEAD clean, studio suite 4324 green.
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { TRACK_H } from "./timelineLayout";
|
||||
import { TrackClipCount } from "./TrackClipCount";
|
||||
|
||||
// Layer row (Figma order: disclosure ∿, diamond, name) — the disclosure lives
|
||||
// Layer row (diamond, name, then the ∿ disclosure on the right edge) — the
|
||||
// disclosure lives
|
||||
// here, not on the clip bar, and re-expands a collapsed layer. `∿` (not a
|
||||
// caret) because a group's own row keeps the caret for its structural
|
||||
// disclosure (member rows) — this button only ever means "show this row's
|
||||
@@ -45,27 +46,6 @@ export function LayerDisclosureRow({
|
||||
background: gutterBackground,
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
// ponytail: No focus id here; keyboard routing belongs to the enclosing logical row.
|
||||
tabIndex={-1}
|
||||
aria-expanded={isExpanded}
|
||||
aria-controls={lanesId}
|
||||
aria-label={`${isExpanded ? "Hide" : "Show"} ${name} lanes`}
|
||||
title={`${isExpanded ? "Hide" : "Show"} lanes`}
|
||||
// h-6 w-6 = the 24x24 WCAG 2.2 minimum target. The glyph stays 11px;
|
||||
// only the hit box grows.
|
||||
className={`flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 text-[11px] leading-none focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
|
||||
isExpanded ? "text-[#3CE6AC]" : "text-white/55 hover:text-white"
|
||||
}`}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onToggleClipExpanded();
|
||||
}}
|
||||
>
|
||||
<span aria-hidden="true">∿</span>
|
||||
</button>
|
||||
{/* Decorative: the disclosure button above already names the row's keyframe
|
||||
state, and aria-label on a plain span is not exposed reliably anyway. */}
|
||||
<span aria-hidden="true" className="shrink-0 text-[13px] leading-none text-white/40">
|
||||
@@ -76,6 +56,31 @@ export function LayerDisclosureRow({
|
||||
</span>
|
||||
<TrackClipCount clipCount={clipCount} />
|
||||
{children}
|
||||
{/* Anchored right, on every header that has one: the lane toggle is the
|
||||
row's last word about itself, and a left-hand ∿ put it where the eye
|
||||
looks for identity instead. `ml-auto` rather than a spacer so it holds
|
||||
the edge whatever else the row grows. */}
|
||||
<button
|
||||
type="button"
|
||||
// ponytail: No focus id here; keyboard routing belongs to the enclosing logical row.
|
||||
tabIndex={-1}
|
||||
aria-expanded={isExpanded}
|
||||
aria-controls={lanesId}
|
||||
aria-label={`${isExpanded ? "Hide" : "Show"} ${name} lanes`}
|
||||
title={`${isExpanded ? "Hide" : "Show"} lanes`}
|
||||
// h-6 w-6 = the 24x24 WCAG 2.2 minimum target. The glyph stays 11px;
|
||||
// only the hit box grows.
|
||||
className={`ml-auto flex h-6 w-6 shrink-0 items-center justify-center rounded border-0 bg-transparent p-0 text-[11px] leading-none focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
|
||||
isExpanded ? "text-[#3CE6AC]" : "text-white/55 hover:text-white"
|
||||
}`}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
onToggleClipExpanded();
|
||||
}}
|
||||
>
|
||||
<span aria-hidden="true">∿</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ export function TimelineGroupHeader({
|
||||
</div>
|
||||
{/* Line two: what you can DO to it. Its own row so the name is not
|
||||
squeezed to a few characters by five controls sharing 232px. */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex w-full items-center gap-1.5">
|
||||
<TimelineFxButton
|
||||
fxChainRaw={fxChain}
|
||||
onChainChange={onFxChainChange}
|
||||
@@ -147,7 +147,8 @@ export function TimelineGroupHeader({
|
||||
aria-expanded={isLaneOpen}
|
||||
aria-label={`${isLaneOpen ? "Hide" : "Show"} ${label} lanes`}
|
||||
title={`${isLaneOpen ? "Hide" : "Show"} lanes`}
|
||||
className={`flex h-6 items-center justify-center gap-0.5 rounded border-0 bg-transparent px-1 text-[11px] leading-none focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
|
||||
// Anchored right, matching every other header's lane toggle.
|
||||
className={`ml-auto flex h-6 items-center justify-center gap-0.5 rounded border-0 bg-transparent px-1 text-[11px] leading-none focus-visible:outline focus-visible:outline-1 focus-visible:outline-[#3CE6AC] ${
|
||||
isLaneOpen ? "text-[#3CE6AC]" : "text-white/55 hover:text-white"
|
||||
}`}
|
||||
onPointerDown={(event) => event.stopPropagation()}
|
||||
|
||||
@@ -771,6 +771,7 @@ describe("TimelineTrackHeader", () => {
|
||||
|
||||
expect(header()?.style.paddingLeft).toBe("");
|
||||
expect(header()?.style.borderLeft).toBe("");
|
||||
expect(header()?.style.background).not.toContain("linear-gradient");
|
||||
|
||||
view.rerender({
|
||||
keyframeClip: VOICE,
|
||||
@@ -781,6 +782,10 @@ describe("TimelineTrackHeader", () => {
|
||||
});
|
||||
expect(header()?.style.paddingLeft).toBe("14px");
|
||||
expect(header()?.style.borderLeft).toContain("2px");
|
||||
// And a lighter gutter, so the row reads as sitting INSIDE its group
|
||||
// rather than beside it. Overlaid on the theme's own fill rather than a
|
||||
// hard-coded colour, so it follows whatever the gutter is.
|
||||
expect(header()?.style.background).toContain("linear-gradient");
|
||||
act(() => view.root.unmount());
|
||||
});
|
||||
|
||||
|
||||
@@ -34,6 +34,17 @@ import { timelineLogicalRowCellId, timelinePropertyRowId } from "./timelineNavig
|
||||
* nesting its `aria-level` already reports. */
|
||||
const GROUP_MEMBER_RAIL = "#3CE6AC59";
|
||||
const GROUP_MEMBER_INDENT = 14;
|
||||
/** A hair lighter than `gutterBackground`, so a member row reads as sitting
|
||||
* INSIDE its group rather than beside it. Overlaid rather than hard-coded so
|
||||
* it tracks whatever the theme's gutter is. */
|
||||
const GROUP_MEMBER_TINT = "rgba(255,255,255,0.035)";
|
||||
|
||||
/** The gutter fill for a row, tinted when it belongs to a group. */
|
||||
function gutterFill(base: string, isGroupMember: boolean): string {
|
||||
return isGroupMember
|
||||
? `linear-gradient(${GROUP_MEMBER_TINT}, ${GROUP_MEMBER_TINT}), ${base}`
|
||||
: base;
|
||||
}
|
||||
|
||||
interface TimelineTrackHeaderProps {
|
||||
/** The track's real key: a FRACTIONAL z-order sort value. Routes callbacks;
|
||||
@@ -489,7 +500,7 @@ export function TimelineTrackHeader({
|
||||
}`}
|
||||
style={{
|
||||
width: showTrackLabel ? LABEL_COL_W : contentOrigin,
|
||||
background: theme.gutterBackground,
|
||||
background: gutterFill(theme.gutterBackground, isGroupMember),
|
||||
borderRight: `1px solid ${theme.gutterBorder}`,
|
||||
// A group's member rows are `aria-level="2"`, and until this they read
|
||||
// as level 2 to a screen reader while looking identical to every
|
||||
@@ -578,7 +589,7 @@ export function TimelineTrackHeader({
|
||||
}
|
||||
clipCount={clipCount}
|
||||
isExpanded={isExpanded}
|
||||
gutterBackground={theme.gutterBackground}
|
||||
gutterBackground={gutterFill(theme.gutterBackground, isGroupMember)}
|
||||
columnWidth={showTrackLabel ? LABEL_COL_W : contentOrigin}
|
||||
lanesId={lanesId}
|
||||
onToggleClipExpanded={onToggleClipExpanded}
|
||||
@@ -615,7 +626,7 @@ export function TimelineTrackHeader({
|
||||
expandedElement={keyframeClip}
|
||||
currentTime={currentTime}
|
||||
clipPercentage={clipPercentage}
|
||||
gutterBackground={theme.gutterBackground}
|
||||
gutterBackground={gutterFill(theme.gutterBackground, isGroupMember)}
|
||||
columnWidth={showTrackLabel ? LABEL_COL_W : contentOrigin}
|
||||
onTogglePropertyGroupKeyframe={onTogglePropertyGroupKeyframe}
|
||||
onSeek={onSeek}
|
||||
@@ -638,7 +649,7 @@ export function TimelineTrackHeader({
|
||||
}
|
||||
top={getTimelineLaneTop(lanes.length) + index * AUTOMATION_LANE_H}
|
||||
isLastLane={index === automationRows.length - 1}
|
||||
gutterBackground={theme.gutterBackground}
|
||||
gutterBackground={gutterFill(theme.gutterBackground, isGroupMember)}
|
||||
columnWidth={showTrackLabel ? LABEL_COL_W : contentOrigin}
|
||||
onRemove={onRemoveAutomationLane}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user