1
0
forked from boos/BoosAPI

[codex] restore route priority drag behavior (#376)

* fix: restore route priority drag behavior

* fix: use theme rail highlight surface
This commit is contained in:
Cita
2026-04-01 21:24:25 +08:00
committed by GitHub
parent 5fffcb6d66
commit 1133846998
4 changed files with 26 additions and 83 deletions
+5 -72
View File
@@ -1,7 +1,6 @@
import { memo, useState, type ReactNode } from 'react'; import { memo, useState, type ReactNode } from 'react';
import { import {
DndContext, DndContext,
DragOverlay,
KeyboardSensor, KeyboardSensor,
PointerSensor, PointerSensor,
closestCenter, closestCenter,
@@ -42,13 +41,12 @@ import {
isExplicitGroupRoute, isExplicitGroupRoute,
resolveRouteTitle, resolveRouteTitle,
resolveRouteIcon, resolveRouteIcon,
getPriorityTagStyle,
} from './utils.js'; } from './utils.js';
import { import {
buildPriorityBuckets, buildPriorityBuckets,
} from './priorityBuckets.js'; } from './priorityBuckets.js';
import { import {
buildPriorityDragPreviewStyle, buildPriorityRailNodeStyle,
buildPriorityRailSections, buildPriorityRailSections,
createPriorityRailNewLayerId, createPriorityRailNewLayerId,
isPriorityRailNewLayerId, isPriorityRailNewLayerId,
@@ -159,52 +157,6 @@ function PriorityRailNewLayerRow({
); );
} }
function PriorityDragPreview({
channel,
displayPriority,
activeRowWidth,
}: {
channel: RouteChannel;
displayPriority: number;
activeRowWidth?: number | null;
}) {
return (
<div
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 8,
padding: '8px 12px',
borderRadius: 'var(--radius-md)',
border: '1px solid var(--color-border)',
background: 'var(--color-bg-card)',
boxShadow: 'var(--shadow-sm)',
color: 'var(--color-text-primary)',
fontSize: 12,
...buildPriorityDragPreviewStyle(activeRowWidth),
}}
>
<span
className="badge"
style={{
fontSize: 10,
fontWeight: 700,
letterSpacing: 0.1,
}}
>
{`P${displayPriority}`}
</span>
<span style={{ fontWeight: 600 }}>
{channel.account?.username || `account-${channel.accountId}`}
</span>
<span className="badge badge-muted" style={{ fontSize: 10 }}>
{channel.site?.name || 'unknown'}
</span>
</div>
);
}
function RouteCardInner({ function RouteCardInner({
route, route,
brand, brand,
@@ -281,14 +233,10 @@ function RouteCardInner({
const priorityBuckets = buildPriorityBuckets(channels || []); const priorityBuckets = buildPriorityBuckets(channels || []);
const priorityRailSections = buildPriorityRailSections(channels || []); const priorityRailSections = buildPriorityRailSections(channels || []);
const [activeDragChannelId, setActiveDragChannelId] = useState<number | null>(null); const [activeDragChannelId, setActiveDragChannelId] = useState<number | null>(null);
const [activeDragRowWidth, setActiveDragRowWidth] = useState<number | null>(null);
const [dragOverId, setDragOverId] = useState<string | number | null>(null); const [dragOverId, setDragOverId] = useState<string | number | null>(null);
const activeDragChannel = activeDragChannelId == null const activeDragBucketIndex = activeDragChannelId == null
? null
: (channels || []).find((channel) => channel.id === activeDragChannelId) || null;
const activeDragBucketIndex = activeDragChannel == null
? -1 ? -1
: priorityBuckets.findIndex((bucket) => bucket.channels.some((channel) => channel.id === activeDragChannel.id)); : priorityBuckets.findIndex((bucket) => bucket.channels.some((channel) => channel.id === activeDragChannelId));
const hoveredBucketIndex = typeof dragOverId === 'number' const hoveredBucketIndex = typeof dragOverId === 'number'
? priorityBuckets.findIndex((bucket) => bucket.channels.some((channel) => channel.id === dragOverId)) ? priorityBuckets.findIndex((bucket) => bucket.channels.some((channel) => channel.id === dragOverId))
: -1; : -1;
@@ -298,14 +246,12 @@ function RouteCardInner({
const clearDragState = () => { const clearDragState = () => {
setActiveDragChannelId(null); setActiveDragChannelId(null);
setActiveDragRowWidth(null);
setDragOverId(null); setDragOverId(null);
}; };
const handleDragStart = (event: DragStartEvent) => { const handleDragStart = (event: DragStartEvent) => {
const nextId = Number(event.active.id); const nextId = Number(event.active.id);
setActiveDragChannelId(Number.isFinite(nextId) ? nextId : null); setActiveDragChannelId(Number.isFinite(nextId) ? nextId : null);
setActiveDragRowWidth(event.active.rect.current.initial?.width ?? null);
setDragOverId(event.active.id); setDragOverId(event.active.id);
}; };
@@ -679,7 +625,7 @@ function RouteCardInner({
const hoveredExistingLayer = activeDragChannelId != null && hoveredBucketIndex === bucketIndex; const hoveredExistingLayer = activeDragChannelId != null && hoveredBucketIndex === bucketIndex;
const hoveredCrossLayer = hoveredExistingLayer && activeDragBucketIndex !== bucketIndex; const hoveredCrossLayer = hoveredExistingLayer && activeDragBucketIndex !== bucketIndex;
const hoveredNewLayer = activeDragChannelId != null && hoveredNewLayerBucketIndex === bucketIndex; const hoveredNewLayer = activeDragChannelId != null && hoveredNewLayerBucketIndex === bucketIndex;
const railTone = getPriorityTagStyle(bucketIndex); const railNodeStyle = buildPriorityRailNodeStyle(bucketIndex, hoveredExistingLayer);
return ( return (
<div <div
@@ -739,16 +685,12 @@ function RouteCardInner({
minWidth: 72, minWidth: 72,
padding: '6px 10px', padding: '6px 10px',
borderRadius: 999, borderRadius: 999,
border: `1px solid ${hoveredExistingLayer ? 'var(--color-primary)' : 'color-mix(in srgb, currentColor 24%, transparent)'}`,
background: hoveredExistingLayer
? 'color-mix(in srgb, var(--color-primary) 10%, var(--color-bg))'
: railTone.background,
color: hoveredExistingLayer ? 'var(--color-primary)' : railTone.color,
fontSize: 11, fontSize: 11,
fontWeight: 600, fontWeight: 600,
textAlign: 'center', textAlign: 'center',
lineHeight: 1.2, lineHeight: 1.2,
transition: 'all 0.16s ease', transition: 'all 0.16s ease',
...railNodeStyle,
}} }}
> >
{railSection ? `P${bucketIndex} · ${railSection.channelCount}` : railLabel} {railSection ? `P${bucketIndex} · ${railSection.channelCount}` : railLabel}
@@ -806,15 +748,6 @@ function RouteCardInner({
})} })}
</div> </div>
</SortableContext> </SortableContext>
<DragOverlay>
{activeDragChannel && !compact ? (
<PriorityDragPreview
channel={activeDragChannel}
displayPriority={Math.max(0, activeDragBucketIndex)}
activeRowWidth={activeDragRowWidth}
/>
) : null}
</DragOverlay>
</DndContext> </DndContext>
</div> </div>
) : ( ) : (
@@ -50,7 +50,7 @@ export function SortableChannelRow({
const rowStyle: CSSProperties = { const rowStyle: CSSProperties = {
transform: CSS.Transform.toString(transform), transform: CSS.Transform.toString(transform),
transition, transition,
opacity: isDragging ? (mobile ? 0.72 : 0) : channel.enabled === false ? 0.5 : 1, opacity: isDragging ? 0.72 : channel.enabled === false ? 0.5 : 1,
zIndex: isDragging ? 10 : 1, zIndex: isDragging ? 10 : 1,
display: 'grid', display: 'grid',
gridTemplateColumns: managementLocked || mobile ? 'minmax(0, 1fr)' : 'minmax(0, 1fr) auto auto auto', gridTemplateColumns: managementLocked || mobile ? 'minmax(0, 1fr)' : 'minmax(0, 1fr) auto auto auto',
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { import {
applyPriorityRailDrop, applyPriorityRailDrop,
buildPriorityDragPreviewStyle, buildPriorityRailNodeStyle,
buildPriorityRailDragTargets, buildPriorityRailDragTargets,
buildPriorityRailSections, buildPriorityRailSections,
createPriorityRailNewLayerId, createPriorityRailNewLayerId,
@@ -76,10 +76,15 @@ describe('priorityRail helpers', () => {
]); ]);
}); });
it('uses the active row width for drag preview alignment when available', () => { it('keeps priority color hierarchy even when a rail node is highlighted', () => {
expect(buildPriorityDragPreviewStyle(640)).toMatchObject({ const p0 = buildPriorityRailNodeStyle(0, false);
width: 640, const p0Highlighted = buildPriorityRailNodeStyle(0, true);
maxWidth: 'calc(100vw - 32px)', const p1Highlighted = buildPriorityRailNodeStyle(1, true);
});
expect(p0.background).not.toBe('var(--color-bg)');
expect(String(p0Highlighted.background)).toContain('var(--color-bg)');
expect(String(p0Highlighted.background)).not.toContain('white');
expect(p0Highlighted.color).toBe('var(--color-primary)');
expect(p0Highlighted.background).not.toBe(p1Highlighted.background);
}); });
}); });
+9 -4
View File
@@ -1,5 +1,6 @@
import type { CSSProperties } from 'react'; import type { CSSProperties } from 'react';
import type { PriorityRailDragTarget, PriorityRailSection } from './types.js'; import type { PriorityRailDragTarget, PriorityRailSection } from './types.js';
import { getPriorityTagStyle } from './utils.js';
type PriorityRailChannelLike = { type PriorityRailChannelLike = {
id: number; id: number;
@@ -119,10 +120,14 @@ export function applyPriorityRailDrop<T extends PriorityRailChannelLike>(
); );
} }
export function buildPriorityDragPreviewStyle(activeRowWidth?: number | null): CSSProperties { export function buildPriorityRailNodeStyle(priority: number, highlighted: boolean): CSSProperties {
const resolvedWidth = Number.isFinite(activeRowWidth ?? Number.NaN) ? activeRowWidth ?? undefined : undefined; const tone = getPriorityTagStyle(priority);
return { return {
width: resolvedWidth, border: `1px solid ${highlighted ? 'var(--color-primary)' : 'color-mix(in srgb, currentColor 24%, transparent)'}`,
maxWidth: 'calc(100vw - 32px)', background: highlighted
? `color-mix(in srgb, ${tone.background} 78%, var(--color-bg))`
: tone.background,
color: highlighted ? 'var(--color-primary)' : tone.color,
}; };
} }