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:
1 parent
bfa21989c9
commit
0466b77328
31 files changed
+1492
-269
No files matched your search
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user