Use Bootstrap Icons in clip menu

This commit is contained in:
Codex
2026-04-22 08:31:25 -05:00
parent ec73156966
commit 0a13aedbef
5 changed files with 387 additions and 145 deletions

View File

@@ -10,6 +10,7 @@
"dependencies": {
"@tanstack/react-query": "^5.59.0",
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.28.0",
@@ -2857,6 +2858,22 @@
"@popperjs/core": "^2.11.8"
}
},
"node_modules/bootstrap-icons": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz",
"integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",

View File

@@ -11,6 +11,7 @@
"dependencies": {
"@tanstack/react-query": "^5.59.0",
"bootstrap": "^5.3.8",
"bootstrap-icons": "^1.13.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.28.0",

View File

@@ -6,6 +6,7 @@ import { BrowserRouter } from "react-router-dom";
import App from "./App";
import { queryClient } from "./lib/queryClient";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-icons/font/bootstrap-icons.css";
import "./styles.css";
ReactDOM.createRoot(document.getElementById("root")!).render(

View File

@@ -19,6 +19,14 @@ const TRIM_STEP_MS = 100;
const TRIM_ZOOM_WINDOW_MS = 3_000;
const TRIM_ZOOM_SLIDER_MAX = 100;
function clipsQueryKey(teamId: string | null, playerId: string | null, includeHidden: boolean) {
return ["clips", teamId, playerId, includeHidden ? "all" : "visible"] as const;
}
function clipsQueryPrefix(teamId: string | null, playerId: string | null) {
return ["clips", teamId, playerId] as const;
}
type WalkupClipSourceMode = "upload" | "url" | "existing";
type WalkupClipModalState =
@@ -28,13 +36,15 @@ type WalkupClipModalState =
type BootstrapIconName =
| "play"
| "stop"
| "three-dots-vertical"
| "three-dots"
| "pencil-square"
| "plus-lg"
| "x-lg"
| "chevron-up"
| "chevron-down"
| "pin-fill";
| "pin-fill"
| "eye"
| "eye-slash";
type TrimFocusEdge = "start" | "end";
type SourceCreationProgress = {
label: string;
@@ -47,6 +57,7 @@ export function LibraryPage() {
const teamId = walkup.selectedTeamId;
const playerId = walkup.currentPlayerId;
const [walkupClipModal, setWalkupClipModal] = useState<WalkupClipModalState | null>(null);
const [clipPinModalClipId, setClipPinModalClipId] = useState<number | null>(null);
const [manageMediaOpen, setManageMediaOpen] = useState(false);
const audioRef = useRef<HTMLAudioElement | null>(null);
const previewClipIdRef = useRef<number | null>(null);
@@ -60,8 +71,8 @@ export function LibraryPage() {
enabled: Boolean(teamId && playerId),
});
const clipsQuery = useQuery({
queryKey: ["clips", teamId, playerId],
queryFn: () => api.listClips(teamId, playerId),
queryKey: clipsQueryKey(teamId, playerId, true),
queryFn: () => api.listClips(teamId, playerId, true),
enabled: Boolean(teamId && playerId),
});
const pinsQuery = useQuery({
@@ -82,6 +93,13 @@ export function LibraryPage() {
const pinnedAssignmentsByClipAndGame = useMemo(() => {
return new Map((pinsQuery.data ?? []).map((assignment) => [`${assignment.external_game_id}:${assignment.clip_id}`, assignment]));
}, [pinsQuery.data]);
const pinCountByClipId = useMemo(() => {
const counts = new Map<number, number>();
for (const assignment of pinsQuery.data ?? []) {
counts.set(assignment.clip_id, (counts.get(assignment.clip_id) ?? 0) + 1);
}
return counts;
}, [pinsQuery.data]);
useEffect(() => {
return () => {
@@ -93,7 +111,7 @@ export function LibraryPage() {
mutationFn: (clipId: number) => api.deleteClip(clipId, playerId),
onSuccess: async () => {
stopPreview();
await queryClient.invalidateQueries({ queryKey: ["clips", teamId, playerId] });
await queryClient.invalidateQueries({ queryKey: clipsQueryPrefix(teamId, playerId) });
},
});
@@ -105,7 +123,29 @@ export function LibraryPage() {
clip_ids: clipIds,
}),
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: ["clips", teamId, playerId] });
await queryClient.invalidateQueries({ queryKey: clipsQueryPrefix(teamId, playerId) });
},
});
const toggleHiddenMutation = useMutation({
mutationFn: async ({ clip, hidden }: { clip: AudioClip; hidden: boolean }) =>
api.updateClip(
clip.id,
{
label: clip.label,
start_ms: clip.start_ms,
end_ms: clip.end_ms,
sort_order: clip.sort_order,
hidden,
},
playerId,
),
onSuccess: async () => {
await Promise.all([
queryClient.invalidateQueries({ queryKey: clipsQueryPrefix(teamId, playerId) }),
queryClient.invalidateQueries({ queryKey: ["assignments"] }),
queryClient.invalidateQueries({ queryKey: ["prep"] }),
]);
},
});
@@ -259,7 +299,7 @@ export function LibraryPage() {
return (
<section className="page-grid">
<div className="panel">
<div className="panel walkup-library-panel">
<div className="section-header clip-list-header">
<div>
<h2>My Clips</h2>
@@ -304,11 +344,12 @@ export function LibraryPage() {
}}
canMoveUp={index > 0}
canMoveDown={index < orderedClips.length - 1}
games={walkup.games}
pinnedAssignmentsByClipAndGame={pinnedAssignmentsByClipAndGame}
onTogglePin={(gameId) => {
void togglePinMutation.mutateAsync({ clipId: clip.id, gameId });
pinCount={pinCountByClipId.get(clip.id) ?? 0}
onOpenPinModal={() => setClipPinModalClipId(clip.id)}
onToggleHidden={() => {
void toggleHiddenMutation.mutateAsync({ clip, hidden: !clip.hidden });
}}
isHidden={clip.hidden}
/>
))}
{!clipsQuery.isLoading && !orderedClips.length ? (
@@ -345,6 +386,18 @@ export function LibraryPage() {
isDeletingClip={deleteClipMutation.isPending}
/>
) : null}
{clipPinModalClipId !== null ? (
<PinToGameModal
clip={orderedClips.find((item) => item.id === clipPinModalClipId) ?? null}
games={walkup.games}
pinnedAssignmentsByClipAndGame={pinnedAssignmentsByClipAndGame}
onClose={() => setClipPinModalClipId(null)}
onTogglePin={(gameId, clipId) => {
void togglePinMutation.mutateAsync({ clipId, gameId });
}}
isTogglingPin={togglePinMutation.isPending}
/>
) : null}
{manageMediaOpen ? (
<ManageUploadedMediaModal
assets={assetsQuery.data ?? []}
@@ -359,75 +412,21 @@ export function LibraryPage() {
}
function BootstrapIcon({ name }: { name: BootstrapIconName }) {
if (name === "play") {
return (
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<path d="M10.804 8 5 4.633v6.734zm.792-.696a.802.802 0 0 1 0 1.392l-6.363 3.692C4.713 12.69 4 12.345 4 11.692V4.308c0-.653.713-.998 1.233-.696z" />
</svg>
);
}
const iconNameMap: Record<BootstrapIconName, string> = {
play: "play-fill",
stop: "stop-fill",
"three-dots": "three-dots",
"pencil-square": "pencil-square",
"plus-lg": "plus-lg",
"x-lg": "x-lg",
"chevron-up": "chevron-up",
"chevron-down": "chevron-down",
"pin-fill": "pin-fill",
eye: "eye",
"eye-slash": "eye-slash",
};
if (name === "stop") {
return (
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<path d="M3.5 5A1.5 1.5 0 0 1 5 3.5h6A1.5 1.5 0 0 1 12.5 5v6a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 11zM5 4.5a.5.5 0 0 0-.5.5v6a.5.5 0 0 0 .5.5h6a.5.5 0 0 0 .5-.5V5a.5.5 0 0 0-.5-.5z" />
</svg>
);
}
if (name === "three-dots-vertical") {
return (
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<path d="M9.5 1a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0m0 7a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0m0 7a1.5 1.5 0 1 1-3 0a1.5 1.5 0 0 1 3 0" />
</svg>
);
}
if (name === "plus-lg") {
return (
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<path d="M8 2a.75.75 0 0 1 .75.75v4.5h4.5a.75.75 0 0 1 0 1.5h-4.5v4.5a.75.75 0 0 1-1.5 0v-4.5h-4.5a.75.75 0 0 1 0-1.5h4.5v-4.5A.75.75 0 0 1 8 2" />
</svg>
);
}
if (name === "chevron-up") {
return (
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<path d="M7.646 5.854a.5.5 0 0 1 .708 0l4.5 4.5a.5.5 0 0 1-.708.708L8 7.207 3.854 11.062a.5.5 0 1 1-.708-.708z" />
</svg>
);
}
if (name === "chevron-down") {
return (
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<path d="M1.646 5.854a.5.5 0 0 1 .708 0L8 11.5l5.646-5.646a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708" />
</svg>
);
}
if (name === "pin-fill") {
return (
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<path d="M4.146 1.5a.5.5 0 0 1 .708 0l3.75 3.75 1.646-1.646a1 1 0 0 1 1.414 0l.732.732a1 1 0 0 1 0 1.414L10.75 7.896l3.75 3.75-1.5 1.5-3.75-3.75-2.354 2.354a1 1 0 0 1-1.414 0l-.732-.732a1 1 0 0 1 0-1.414L6.354 7.25l-3.75-3.75a.5.5 0 0 1 0-.708z" />
</svg>
);
}
if (name === "x-lg") {
return (
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<path d="M2.146 2.146a.5.5 0 0 1 .708 0L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8 2.146 2.854a.5.5 0 0 1 0-.708" />
</svg>
);
}
return (
<svg viewBox="0 0 16 16" aria-hidden="true" focusable="false">
<path d="M15.502 1.94a.5.5 0 0 1 0 .706l-.853.853-2-2 .853-.853a.5.5 0 0 1 .707 0l1.293 1.294zm-1.75 1.457-2-2L4.939 8.21a.5.5 0 0 0-.11.168l-1 3a.5.5 0 0 0 .64.64l3-1a.5.5 0 0 0 .168-.11l6.115-6.51zM4.257 11.518l.924-.308 6.563-6.992-1.5-1.5L3.25 9.7l-.308.924 1.315.894z" />
</svg>
);
return <i className={`bi bi-${iconNameMap[name]} bootstrap-icon`} aria-hidden="true" />;
}
function WalkupClipModal({
@@ -507,11 +506,11 @@ function WalkupClipModal({
});
await Promise.all([
queryClient.invalidateQueries({ queryKey: ["assets", teamId, playerId] }),
queryClient.invalidateQueries({ queryKey: ["clips", teamId, playerId] }),
queryClient.invalidateQueries({ queryKey: clipsQueryPrefix(teamId, playerId) }),
]);
const refreshedClips = await queryClient.fetchQuery({
queryKey: ["clips", teamId, playerId],
queryFn: () => api.listClips(teamId, playerId),
queryKey: clipsQueryKey(teamId, playerId, true),
queryFn: () => api.listClips(teamId, playerId, true),
});
const createdClip = refreshedClips.find((clip) => clip.asset_id === assetId);
if (!createdClip) {
@@ -591,7 +590,7 @@ function WalkupClipModal({
start_ms: 0,
end_ms: DEFAULT_CLIP_LENGTH_MS,
});
await queryClient.invalidateQueries({ queryKey: ["clips", teamId, playerId] });
await queryClient.invalidateQueries({ queryKey: clipsQueryPrefix(teamId, playerId) });
return clip;
},
onSuccess: (clip) => {
@@ -839,7 +838,7 @@ function WalkupClipModal({
onSaveComplete={async () => {
await Promise.all([
queryClient.invalidateQueries({ queryKey: ["assets", teamId, playerId] }),
queryClient.invalidateQueries({ queryKey: ["clips", teamId, playerId] }),
queryClient.invalidateQueries({ queryKey: clipsQueryPrefix(teamId, playerId) }),
]);
handleClose();
}}
@@ -903,7 +902,7 @@ function ManageUploadedMediaModal({
stopPreview();
await Promise.all([
queryClient.invalidateQueries({ queryKey: ["assets", teamId, playerId] }),
queryClient.invalidateQueries({ queryKey: ["clips", teamId, playerId] }),
queryClient.invalidateQueries({ queryKey: clipsQueryPrefix(teamId, playerId) }),
]);
},
});
@@ -931,12 +930,11 @@ function ManageUploadedMediaModal({
</div>
<button
type="button"
className="btn btn-outline-secondary btn-sm icon-button"
className="btn-close"
onClick={handleClose}
aria-label="Close modal"
title="Close modal"
>
<BootstrapIcon name="x-lg" />
</button>
</div>
<div className="walkup-modal-body">
@@ -973,9 +971,10 @@ function WalkupClipCard({
onMoveDown,
canMoveUp,
canMoveDown,
games,
pinnedAssignmentsByClipAndGame,
onTogglePin,
pinCount,
onOpenPinModal,
onToggleHidden,
isHidden,
}: {
clip: AudioClip;
isPreviewing: boolean;
@@ -986,18 +985,35 @@ function WalkupClipCard({
onMoveDown: () => void;
canMoveUp: boolean;
canMoveDown: boolean;
games: TeamSnapEvent[];
pinnedAssignmentsByClipAndGame: Map<string, { id: number }>;
onTogglePin: (gameId: string) => void;
pinCount: number;
onOpenPinModal: () => void;
onToggleHidden: () => void;
isHidden: boolean;
}) {
const [menuOpen, setMenuOpen] = useState(false);
const [menuDirection, setMenuDirection] = useState<"down" | "up">("down");
const menuRef = useRef<HTMLDivElement | null>(null);
const menuButtonRef = useRef<HTMLButtonElement | null>(null);
const menuPanelRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!menuOpen) {
return;
}
const frame = window.requestAnimationFrame(() => {
const buttonRect = menuButtonRef.current?.getBoundingClientRect();
const menuHeight = menuPanelRef.current?.getBoundingClientRect().height ?? 0;
if (!buttonRect || !menuHeight) {
setMenuDirection("down");
return;
}
const openDownSpace = window.innerHeight - buttonRect.bottom;
const openUpSpace = buttonRect.top;
setMenuDirection(openDownSpace < menuHeight + 16 && openUpSpace > menuHeight ? "up" : "down");
});
function handlePointerDown(event: PointerEvent) {
if (!menuRef.current?.contains(event.target as Node)) {
setMenuOpen(false);
@@ -1013,6 +1029,7 @@ function WalkupClipCard({
document.addEventListener("pointerdown", handlePointerDown);
document.addEventListener("keydown", handleKeyDown);
return () => {
window.cancelAnimationFrame(frame);
document.removeEventListener("pointerdown", handlePointerDown);
document.removeEventListener("keydown", handleKeyDown);
};
@@ -1033,6 +1050,7 @@ function WalkupClipCard({
</button>
<div className="clip-summary-title-row">
<strong>{clip.label}</strong>
{isHidden ? <span className="pill">Hidden</span> : null}
</div>
<div className="clip-summary-order-controls">
<button
@@ -1059,46 +1077,61 @@ function WalkupClipCard({
<div className="clip-summary-menu-wrap" ref={menuRef}>
<button
type="button"
className="icon-button-bare icon-button-menu"
className="btn btn-sm btn-outline-secondary d-inline-flex align-items-center justify-content-center px-2"
ref={menuButtonRef}
onClick={() => setMenuOpen((current) => !current)}
aria-label="Clip menu"
aria-expanded={menuOpen}
aria-haspopup="menu"
title="Clip menu"
>
<BootstrapIcon name="three-dots-vertical" />
<BootstrapIcon name="three-dots" />
</button>
{menuOpen ? (
<div className="clip-summary-menu" role="menu">
<button type="button" className="clip-summary-menu-item" role="menuitem" onClick={onEdit}>
<div ref={menuPanelRef} className={`clip-summary-menu${menuDirection === "up" ? " is-up" : ""}`} role="menu">
<div className="clip-summary-menu-label">Pinned to {pinCount} game{pinCount === 1 ? "" : "s"}</div>
<button
type="button"
className="btn btn-sm btn-outline-secondary clip-summary-menu-action"
role="menuitem"
onClick={() => {
setMenuOpen(false);
onOpenPinModal();
}}
>
<span className="clip-summary-menu-icon">
<BootstrapIcon name="pin-fill" />
</span>
<span>Pin to game</span>
</button>
<button
type="button"
className="btn btn-sm btn-outline-secondary clip-summary-menu-action"
role="menuitem"
onClick={() => {
setMenuOpen(false);
onToggleHidden();
}}
>
<span className="clip-summary-menu-icon">
<BootstrapIcon name={isHidden ? "eye" : "eye-slash"} />
</span>
<span>{isHidden ? "Show in gameday" : "Hide from gameday"}</span>
</button>
<button
type="button"
className="btn btn-sm btn-outline-secondary clip-summary-menu-action"
role="menuitem"
onClick={() => {
setMenuOpen(false);
onEdit();
}}
>
<span className="clip-summary-menu-icon">
<BootstrapIcon name="pencil-square" />
</span>
<span>Edit clip</span>
</button>
<div className="clip-summary-menu-label">Pin to game</div>
{games.map((game) => {
const gameId = String(game.id);
const isPinned = pinnedAssignmentsByClipAndGame.has(`${gameId}:${clip.id}`);
return (
<button
type="button"
key={gameId}
className={`clip-summary-menu-item${isPinned ? " is-active" : ""}`}
role="menuitemcheckbox"
aria-checked={isPinned}
onClick={() => {
onTogglePin(gameId);
setMenuOpen(false);
}}
>
<span className="clip-summary-menu-icon">
<BootstrapIcon name="pin-fill" />
</span>
<span>{isPinned ? "Pinned: " : "Pin: "}{formatGameTitle(game)}</span>
</button>
);
})}
<div className="clip-summary-menu-label">Source: {clip.asset_title}</div>
</div>
) : null}
@@ -1108,6 +1141,107 @@ function WalkupClipCard({
);
}
function PinToGameModal({
clip,
games,
pinnedAssignmentsByClipAndGame,
onClose,
onTogglePin,
isTogglingPin,
}: {
clip: AudioClip | null;
games: TeamSnapEvent[];
pinnedAssignmentsByClipAndGame: Map<string, { id: number }>;
onClose: () => void;
onTogglePin: (gameId: string, clipId: number) => void;
isTogglingPin: boolean;
}) {
useEffect(() => {
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = "hidden";
return () => {
document.body.style.overflow = previousOverflow;
};
}, []);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
onClose();
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [onClose]);
if (!clip) {
return null;
}
return (
<div className="walkup-modal-backdrop" role="presentation" onClick={onClose}>
<section
className="walkup-modal card shadow-lg border-0 w-100"
role="dialog"
aria-modal="true"
aria-labelledby="pin-to-game-modal-title"
onClick={(event) => event.stopPropagation()}
>
<div className="walkup-modal-header">
<div>
<p className="eyebrow mb-1">Pin to game</p>
<h2 id="pin-to-game-modal-title" className="h3 mb-0">
{clip.label}
</h2>
</div>
<button
type="button"
className="btn btn-outline-secondary btn-sm icon-button"
onClick={onClose}
aria-label="Close modal"
title="Close modal"
>
<BootstrapIcon name="x-lg" />
</button>
</div>
<div className="walkup-modal-body stack">
<div className="panel-note">Choose which games should receive this clip. The clip can be pinned to multiple games or none at all.</div>
<div className="clip-pin-game-list">
{games.map((game) => {
const gameId = String(game.id);
const isPinned = pinnedAssignmentsByClipAndGame.has(`${gameId}:${clip.id}`);
return (
<button
type="button"
key={gameId}
className={`clip-pin-game-row btn btn-outline-secondary${isPinned ? " is-active" : ""}`}
onClick={() => onTogglePin(gameId, clip.id)}
disabled={isTogglingPin}
>
<div className="clip-pin-game-copy">
<strong>{formatGameTitle(game)}</strong>
<span className="muted">{isPinned ? "Pinned to this game" : "Not pinned"}</span>
</div>
<span className={`pill${isPinned ? " is-active" : ""}`}>{isPinned ? "Pinned" : "Pin"}</span>
</button>
);
})}
{!games.length ? <div className="muted">No games are available for this team yet.</div> : null}
</div>
<div className="row walkup-modal-actions">
<button type="button" className="btn btn-outline-secondary" onClick={onClose}>
Close
</button>
</div>
</div>
</section>
</div>
);
}
function SourceProgressPanel({ progress }: { progress: SourceCreationProgress }) {
const progressValue = progress.percent ?? 100;
return (
@@ -1598,7 +1732,7 @@ function UploadedMediaCard({
setIsEditing(false);
await Promise.all([
queryClient.invalidateQueries({ queryKey: ["assets", teamId, playerId] }),
queryClient.invalidateQueries({ queryKey: ["clips", teamId, playerId] }),
queryClient.invalidateQueries({ queryKey: clipsQueryPrefix(teamId, playerId) }),
]);
},
});

View File

@@ -43,6 +43,8 @@ select {
.shell {
min-height: 100vh;
position: relative;
isolation: isolate;
}
.operator-page {
@@ -117,10 +119,15 @@ select {
padding: 0;
}
.clip-list-add-button svg {
width: 1.2rem;
height: 1.2rem;
fill: currentColor;
.bootstrap-icon {
display: inline-block;
line-height: 1;
font-size: 1rem;
font-style: normal;
}
.clip-list-add-button .bootstrap-icon {
font-size: 1.2rem;
}
.walkup-modal-backdrop {
@@ -132,7 +139,7 @@ select {
padding: 1rem;
background: rgba(11, 18, 28, 0.76);
backdrop-filter: blur(10px);
z-index: 3000;
z-index: 10000;
}
.walkup-modal {
@@ -140,7 +147,7 @@ select {
max-height: 92vh;
overflow: auto;
position: relative;
z-index: 3001;
z-index: 10001;
}
.walkup-modal-header {
@@ -247,6 +254,16 @@ select {
backdrop-filter: blur(12px);
}
.walkup-library-panel {
position: relative;
z-index: 2;
}
.walkup-library-panel + .panel {
position: relative;
z-index: 1;
}
.panel h2,
.panel h3 {
margin-top: 0;
@@ -280,7 +297,10 @@ select {
.clip-summary-title-row {
flex: 1 1 auto;
min-width: 0;
display: block;
display: flex;
align-items: center;
gap: 0.45rem;
flex-wrap: wrap;
line-height: 1.15;
}
@@ -331,11 +351,8 @@ select {
overflow: visible;
}
.icon-button-menu svg {
width: 1rem;
height: 1rem;
display: block;
fill: currentColor;
.icon-button-menu .bootstrap-icon {
font-size: 1rem;
}
.operator-panel-header {
@@ -363,7 +380,7 @@ select {
position: absolute;
top: calc(100% + 0.4rem);
right: 0;
z-index: 10;
z-index: 11000;
min-width: 8.5rem;
padding: 0.35rem;
border: 1px solid var(--panel-border);
@@ -403,12 +420,9 @@ select {
outline-offset: 2px;
}
.icon-button svg {
width: 1em;
height: 1em;
.icon-button .bootstrap-icon {
font-size: 1em;
vertical-align: -0.125em;
fill: currentColor;
display: block;
}
.icon-button {
@@ -438,7 +452,7 @@ select {
position: absolute;
top: calc(100% + 0.45rem);
right: 0;
z-index: 10;
z-index: 11000;
min-width: 12rem;
padding: 0.45rem;
border: 1px solid var(--panel-border);
@@ -449,6 +463,11 @@ select {
gap: 0.35rem;
}
.clip-summary-menu.is-up {
top: auto;
bottom: calc(100% + 0.45rem);
}
.clip-summary-menu-item {
width: 100%;
border: 0;
@@ -462,6 +481,20 @@ select {
gap: 0.45rem;
}
.clip-summary-menu-action {
width: 100%;
justify-content: flex-start;
}
.clip-summary-menu-action .clip-summary-menu-icon {
color: inherit;
}
.clip-summary-menu-item.is-active {
background: rgba(217, 79, 4, 0.08);
color: var(--accent);
}
.clip-summary-menu-icon {
display: inline-flex;
align-items: center;
@@ -471,12 +504,9 @@ select {
flex: 0 0 auto;
}
.clip-summary-menu-icon svg {
width: 1em;
height: 1em;
.clip-summary-menu-icon .bootstrap-icon {
font-size: 1em;
vertical-align: -0.125em;
fill: currentColor;
display: block;
}
.clip-summary-menu-label {
@@ -487,6 +517,65 @@ select {
overflow-wrap: anywhere;
}
.clip-details-stats {
display: flex;
align-items: center;
gap: 0.65rem;
flex-wrap: wrap;
}
.clip-details-actions {
display: flex;
align-items: center;
gap: 0.65rem;
flex-wrap: wrap;
}
.clip-details-actions .btn {
display: inline-flex;
align-items: center;
gap: 0.45rem;
}
.clip-details-game-list,
.clip-pin-game-list {
display: grid;
gap: 0.5rem;
}
.clip-details-game-row,
.clip-pin-game-row {
width: 100%;
border: 1px solid var(--line);
border-radius: 0.75rem;
background: rgba(255, 255, 255, 0.74);
padding: 0.75rem 0.85rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}
.clip-pin-game-row {
text-align: left;
}
.clip-pin-game-row.is-active {
border-color: rgba(217, 79, 4, 0.32);
background: rgba(217, 79, 4, 0.08);
}
.clip-pin-game-copy {
display: grid;
gap: 0.18rem;
min-width: 0;
}
.clip-pin-game-row .pill.is-active {
background: rgba(217, 79, 4, 0.12);
color: var(--accent);
}
.icon-button {
min-width: 2.1rem;
padding-inline: 0.5rem;