diff --git a/src/components.tsx b/src/components.tsx
index 54b7be3..dda6517 100644
--- a/src/components.tsx
+++ b/src/components.tsx
@@ -744,15 +744,21 @@ export function ViewToggle({
export function CardTile({
card,
corner,
+ cornerLeft,
footer,
+ selected,
}: {
card: CardRef;
corner?: ReactNode;
+ /** Rendered top-left, e.g. a selection checkbox (see `selected`). */
+ cornerLeft?: ReactNode;
footer?: ReactNode;
+ /** Highlights the tile and keeps `cornerLeft` visible without hovering. */
+ selected?: boolean;
}) {
const { open } = useClickPreview();
return (
-
+
{card.image_uri ? (
![]()
{card.name}
)}
+ {cornerLeft &&
{cornerLeft}
}
{corner &&
{corner}
}
{footer &&
{footer}
}
diff --git a/src/styles.css b/src/styles.css
index 92eb438..c9d401b 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -203,6 +203,13 @@ select option {
color: var(--text);
}
+input[type="checkbox"] {
+ width: 15px;
+ height: 15px;
+ accent-color: var(--accent);
+ cursor: pointer;
+}
+
/* ============================================================
App shell: nav rail + main
============================================================ */
@@ -950,6 +957,39 @@ tbody tr:hover {
margin-right: 4px;
}
+/* ============================================================
+ Bulk-selection action bar (Library tab)
+ ============================================================ */
+
+.bulk-bar {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ flex-wrap: wrap;
+ background: var(--accent-subtle);
+ border: 1px solid var(--accent-glow);
+ border-radius: var(--radius-lg);
+ padding: 10px 16px;
+ margin-bottom: 16px;
+}
+.bulk-bar-count {
+ font-weight: 700;
+ font-size: 13px;
+ color: var(--accent-hover);
+ white-space: nowrap;
+}
+.bulk-bar-actions {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-wrap: wrap;
+}
+.bulk-bar-delta {
+ width: 48px;
+ text-align: center;
+ padding: 8px 4px;
+}
+
/* ============================================================
Filters panel
============================================================ */
@@ -1158,6 +1198,34 @@ tbody tr:hover {
.tile:focus-within .tile-corner {
opacity: 1;
}
+.tile-corner-left {
+ position: absolute;
+ top: 8px;
+ left: 8px;
+ /* Same reasoning as .tile-corner: don't cover the card name unless the
+ tile is being interacted with — except once selected, where the
+ checkbox should stay visible so the selection is visible at a glance. */
+ opacity: 0;
+ transition: opacity 0.12s;
+}
+.tile:hover .tile-corner-left,
+.tile:focus-within .tile-corner-left,
+.tile-selected .tile-corner-left {
+ opacity: 1;
+}
+.tile-selected .tile-img {
+ box-shadow: 0 0 0 3px var(--accent), var(--shadow-md);
+}
+.tile-select {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 26px;
+ height: 26px;
+ border-radius: 999px;
+ background: rgba(0, 0, 0, 0.78);
+ backdrop-filter: blur(2px);
+}
.tile-badge {
background: rgba(0, 0, 0, 0.78);
color: #fff;
diff --git a/src/views/LibraryBrowser.tsx b/src/views/LibraryBrowser.tsx
index f0223ed..27c4167 100644
--- a/src/views/LibraryBrowser.tsx
+++ b/src/views/LibraryBrowser.tsx
@@ -8,6 +8,7 @@ import {
GroupBySelect,
GroupMode,
LocationField,
+ PromptDialog,
QtyStepper,
ViewToggle,
groupCards,
@@ -41,6 +42,13 @@ export function LibraryBrowser() {
const [pendingRemoval, setPendingRemoval] = useState
>(new Set());
const removalTimers = useRef