Add curator roles/permissions with Users admin, and fix lineage branch joins.

Staff accounts use admin/curator roles and fine-grained flags; transitions connect source-to-target with color gradients and stream cutout masks so overlaps stay seamless.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila KhodjaefandCursor committed 2026-07-27 18:04:17 +03:00
1 parent bfa21989c9
commit 0466b77328
31 files changed
+1492 -269

No files matched your search

+12 -8
View File
@@ -19,6 +19,7 @@ interface Props {
artist: Artist & { movement_name?: string };
debugMode?: boolean;
debugShowMore?: boolean;
canCheckup?: boolean;
portraitRevision?: number;
onBack: () => void;
onEnterGallery: () => void;
@@ -36,6 +37,7 @@ export default function ArtistBio({
artist,
debugMode = false,
debugShowMore = false,
canCheckup = true,
portraitRevision = 0,
onBack,
onEnterGallery,
@@ -322,14 +324,16 @@ export default function ArtistBio({
<p className="debug-image-status">No portrait image result found.</p>
)}
<div className="debug-action-buttons">
<button
type="button"
className="debug-checked-btn"
onClick={handleMarkChecked}
disabled={!!artist.checkup_checked || markingChecked || uploading}
>
{markingChecked ? '…' : 'Checked'}
</button>
{canCheckup && (
<button
type="button"
className="debug-checked-btn"
onClick={handleMarkChecked}
disabled={!!artist.checkup_checked || markingChecked || uploading}
>
{markingChecked ? '…' : 'Checked'}
</button>
)}
<button
type="button"
className="debug-fix-btn"
+2 -1
View File
@@ -96,8 +96,9 @@
}
.movement-branch-fast {
stroke-opacity: 0.32;
stroke-opacity: 0.36;
stroke-width: calc(var(--stream-stroke) * 0.45);
stroke-linecap: round;
}
.movement-stream-fast {
+64 -39
View File
@@ -377,13 +377,12 @@ function branchCorridorXRange(
parentXStart: number,
parentXEnd: number,
childXStart: number,
childXEnd: number,
childIndex: number,
childCount: number
): { x0: number; x1: number } {
const t = childCount === 1 ? 0.5 : (childIndex + 1) / (childCount + 1);
const originX = parentXStart + t * (parentXEnd - parentXStart);
const targetX = (childXStart + childXEnd) / 2;
const targetX = childXStart;
const x0 = Math.min(originX, targetX);
const x1 = Math.max(originX, targetX);
// Near-vertical transitions still have a wide SVG stroke — pad so overlapping streams register.
@@ -574,7 +573,6 @@ function refineLanesForLineageCorridors(
parentSpan.xStart,
parentSpan.xEnd,
childSpan.xStart,
childSpan.xEnd,
childIndex,
children.length
);
@@ -928,7 +926,7 @@ function buildLabelPlacements(
}
function branchTargetOnChild(layout: MovementLayout): { x: number; y: number } {
const x = (layout.xStart + layout.xEnd) / 2;
const x = layout.xStart;
return { x, y: yOnStream(layout, x) };
}
@@ -1491,9 +1489,35 @@ export default function MovementBands({
preserveAspectRatio="none"
aria-hidden
>
{!fastGraphics && (
<defs>
{branches.map((branch) => (
<defs>
{/* Hide branch ink under streams so translucent overlaps do not brighten movements */}
<mask
id="movement-branch-cutout"
maskUnits="userSpaceOnUse"
x={0}
y={0}
width={100}
height={layoutHeight}
>
<rect x={0} y={0} width={100} height={layoutHeight} fill="white" />
{layouts.map((layout) => (
<path
key={`branch-cutout-${layout.movement.id}`}
d={streamPath(layout.xStart, layout.xEnd, layout.y, layout.yEnd, streamCurveOffset)}
fill="none"
stroke="black"
strokeLinecap="round"
strokeLinejoin="round"
style={{
strokeWidth: streamStrokePx * 1.12,
vectorEffect: 'non-scaling-stroke',
}}
/>
))}
</mask>
{!fastGraphics &&
branches.map((branch) => (
<linearGradient
key={`branch-grad-${branch.key}`}
id={`branch-grad-${branch.key}`}
@@ -1503,15 +1527,13 @@ export default function MovementBands({
x2={branch.x2}
y2={branch.y2}
>
<stop offset="0%" stopColor={branch.colorFrom} stopOpacity={0} />
<stop offset="18%" stopColor={branch.colorFrom} stopOpacity={0.34} />
<stop offset="50%" stopColor={branch.colorTo} stopOpacity={0.34} />
<stop offset="82%" stopColor={branch.colorTo} stopOpacity={0.34} />
<stop offset="100%" stopColor={branch.colorTo} stopOpacity={0} />
<stop offset="0%" stopColor={branch.colorFrom} stopOpacity={0.36} />
<stop offset="100%" stopColor={branch.colorTo} stopOpacity={0.36} />
</linearGradient>
))}
{layouts.map((layout) => {
{!fastGraphics &&
layouts.map((layout) => {
const primaryParent =
layout.parentIds.length > 0 ? layoutById.get(layout.parentIds[0]) : null;
const hasChildren = (childIdsByParent.get(layout.movement.id)?.length ?? 0) > 0;
@@ -1557,20 +1579,21 @@ export default function MovementBands({
</linearGradient>
);
})}
</defs>
)}
</defs>
{fastGraphics ? (
<>
{branches.map((branch) => (
<path
key={branch.key}
d={branch.d}
className="movement-branch movement-branch-fast"
stroke={branch.colorTo}
fill="none"
/>
))}
<g mask="url(#movement-branch-cutout)">
{branches.map((branch) => (
<path
key={branch.key}
d={branch.d}
className="movement-branch movement-branch-fast"
stroke={branch.colorTo}
fill="none"
/>
))}
</g>
{layouts.map((layout) => {
const d = streamPath(layout.xStart, layout.xEnd, layout.y, layout.yEnd, streamCurveOffset);
return (
@@ -1586,21 +1609,23 @@ export default function MovementBands({
</>
) : (
<>
{branches.map((branch) => {
const [fromId, toId] = branch.key.split('-').map(Number);
const branchHighlighted =
hoveredMovementId != null &&
(fromId === hoveredMovementId || toId === hoveredMovementId);
return (
<path
key={branch.key}
d={branch.d}
className={`movement-branch${branchHighlighted ? ' movement-branch-highlighted' : ''}`}
stroke={`url(#branch-grad-${branch.key})`}
fill="none"
/>
);
})}
<g mask="url(#movement-branch-cutout)">
{branches.map((branch) => {
const [fromId, toId] = branch.key.split('-').map(Number);
const branchHighlighted =
hoveredMovementId != null &&
(fromId === hoveredMovementId || toId === hoveredMovementId);
return (
<path
key={branch.key}
d={branch.d}
className={`movement-branch${branchHighlighted ? ' movement-branch-highlighted' : ''}`}
stroke={`url(#branch-grad-${branch.key})`}
fill="none"
/>
);
})}
</g>
{layouts.map((layout) => {
const d = streamPath(layout.xStart, layout.xEnd, layout.y, layout.yEnd, streamCurveOffset);
+13 -8
View File
@@ -22,6 +22,8 @@ interface Props {
onArtistBio: () => void;
onInfluenceArtistClick?: (artistId: number) => void;
isCurator?: boolean;
/** When false, hide the Checked action (needs checkup permission). Defaults to true when debugMode is on. */
canCheckup?: boolean;
debugMode?: boolean;
debugShowMore?: boolean;
onPaintingImageFixed?: (
@@ -206,6 +208,7 @@ export default function PaintingDetailView({
onArtistBio,
onInfluenceArtistClick,
isCurator = false,
canCheckup = true,
debugMode = false,
debugShowMore = false,
onPaintingImageFixed,
@@ -734,14 +737,16 @@ export default function PaintingDetailView({
<p className="debug-image-status">No Google image result found.</p>
)}
<div className="debug-action-buttons">
<button
type="button"
className="debug-checked-btn"
onClick={handleMarkChecked}
disabled={!!painting.checkup_checked || markingChecked || uploading}
>
{markingChecked ? '…' : 'Checked'}
</button>
{canCheckup && (
<button
type="button"
className="debug-checked-btn"
onClick={handleMarkChecked}
disabled={!!painting.checkup_checked || markingChecked || uploading}
>
{markingChecked ? '…' : 'Checked'}
</button>
)}
<button
type="button"
className="debug-fix-btn"