feat: library ui changes and searchbar removal

This commit is contained in:
Moyasee
2025-11-06 18:26:56 +02:00
parent 754e9c14b8
commit 3bef0c9269
14 changed files with 82 additions and 192 deletions

View File

@@ -93,6 +93,7 @@
},
"header": {
"search": "Search games",
"search_library": "Search library",
"home": "Home",
"catalogue": "Catalogue",
"library": "Library",

View File

@@ -5,7 +5,7 @@
}
::-webkit-scrollbar {
width: 4px;
width: 9px;
background-color: globals.$dark-background-color;
}
@@ -90,6 +90,7 @@ img {
progress[value] {
-webkit-appearance: none;
appearance: none;
}
.container {

View File

@@ -24,7 +24,7 @@
background-color: globals.$background-color;
display: inline-flex;
transition: all ease 0.2s;
width: 300px;
width: 200px;
align-items: center;
border-radius: 8px;
border: solid 1px globals.$border-color;
@@ -35,7 +35,7 @@
}
&--focused {
width: 350px;
width: 250px;
border-color: #dadbe1;
}
}

View File

@@ -7,7 +7,7 @@ import { useAppDispatch, useAppSelector } from "@renderer/hooks";
import "./header.scss";
import { AutoUpdateSubHeader } from "./auto-update-sub-header";
import { setFilters } from "@renderer/features";
import { setFilters, setLibrarySearchQuery } from "@renderer/features";
import cn from "classnames";
const pathTitle: Record<string, string> = {
@@ -28,10 +28,20 @@ export function Header() {
(state) => state.window
);
const searchValue = useAppSelector(
const catalogueSearchValue = useAppSelector(
(state) => state.catalogueSearch.filters.title
);
const librarySearchValue = useAppSelector(
(state) => state.library.searchQuery
);
const isOnLibraryPage = location.pathname.startsWith("/library");
const searchValue = isOnLibraryPage
? librarySearchValue
: catalogueSearchValue;
const dispatch = useAppDispatch();
const [isFocused, setIsFocused] = useState(false);
@@ -63,18 +73,29 @@ export function Header() {
};
const handleSearch = (value: string) => {
dispatch(setFilters({ title: value.slice(0, 255) }));
if (isOnLibraryPage) {
dispatch(setLibrarySearchQuery(value.slice(0, 255)));
} else {
dispatch(setFilters({ title: value.slice(0, 255) }));
if (!location.pathname.startsWith("/catalogue")) {
navigate("/catalogue");
}
}
};
if (!location.pathname.startsWith("/catalogue")) {
navigate("/catalogue");
const handleClearSearch = () => {
if (isOnLibraryPage) {
dispatch(setLibrarySearchQuery(""));
} else {
dispatch(setFilters({ title: "" }));
}
};
useEffect(() => {
if (!location.pathname.startsWith("/catalogue") && searchValue) {
if (!location.pathname.startsWith("/catalogue") && catalogueSearchValue) {
dispatch(setFilters({ title: "" }));
}
}, [location.pathname, searchValue, dispatch]);
}, [location.pathname, catalogueSearchValue, dispatch]);
return (
<>
@@ -123,7 +144,7 @@ export function Header() {
ref={inputRef}
type="text"
name="search"
placeholder={t("search")}
placeholder={isOnLibraryPage ? t("search_library") : t("search")}
value={searchValue}
className="header__search-input"
onChange={(event) => handleSearch(event.target.value)}
@@ -134,7 +155,7 @@ export function Header() {
{searchValue && (
<button
type="button"
onClick={() => dispatch(setFilters({ title: "" }))}
onClick={handleClearSearch}
className="header__action-button"
>
<XIcon />

View File

@@ -5,10 +5,12 @@ import type { LibraryGame } from "@types";
export interface LibraryState {
value: LibraryGame[];
searchQuery: string;
}
const initialState: LibraryState = {
value: [],
searchQuery: "",
};
export const librarySlice = createSlice({
@@ -18,7 +20,10 @@ export const librarySlice = createSlice({
setLibrary: (state, action: PayloadAction<LibraryState["value"]>) => {
state.value = action.payload;
},
setLibrarySearchQuery: (state, action: PayloadAction<string>) => {
state.searchQuery = action.payload;
},
},
});
export const { setLibrary } = librarySlice.actions;
export const { setLibrary, setLibrarySearchQuery } = librarySlice.actions;

View File

@@ -12,12 +12,12 @@
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit);
padding: 8px 16px;
padding: 8px 12px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.9);
cursor: pointer;
font-size: 13px;
font-size: 12px;
font-weight: 500;
transition: all ease 0.2s;
white-space: nowrap; /* prevent label and count from wrapping */

View File

@@ -11,7 +11,6 @@
cursor: pointer;
display: flex;
align-items: center;
padding: 0;
text-align: left;
&:before {
@@ -25,7 +24,7 @@
35deg,
rgba(0, 0, 0, 0.1) 0%,
rgba(0, 0, 0, 0.07) 51.5%,
rgba(255, 255, 255, 0.15) 74%,
rgba(255, 255, 255, 0.15) 64%,
rgba(255, 255, 255, 0.1) 100%
);
transition: all ease 0.3s;
@@ -63,12 +62,7 @@
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(
0deg,
rgba(0, 0, 0, 0.1) 0%,
rgba(0, 0, 0, 0.2) 50%,
rgba(0, 0, 0, 0.3) 100%
);
background: linear-gradient(0deg, rgba(0, 0, 0, 0.7) 20%, transparent 100%);
z-index: 1;
}
@@ -80,7 +74,7 @@
display: flex;
flex-direction: column;
justify-content: space-between;
padding: calc(globals.$spacing-unit * 2);
padding: calc(globals.$spacing-unit * 1.5);
}
&__top-section {
@@ -97,8 +91,8 @@
-webkit-backdrop-filter: blur(8px);
border: solid 1px rgba(255, 255, 255, 0.15);
border-radius: 4px;
width: 36px;
height: 36px;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
@@ -168,9 +162,9 @@
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
padding: 6px 12px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
font-size: 14px;
font-size: 12px;
}
&__playtime-text {
@@ -185,7 +179,6 @@
display: flex;
flex-direction: column;
gap: 6px;
padding: 6px 12px;
flex: 1 1 auto;
min-width: 0;
}
@@ -193,7 +186,6 @@
&__achievement-header {
display: flex;
align-items: center;
gap: 8px;
justify-content: space-between;
}
&__achievements-gap {
@@ -241,7 +233,7 @@
}
&__achievement-percentage {
font-size: 12px;
font-size: 14px;
color: rgba(255, 255, 255, 0.85);
white-space: nowrap;
}

View File

@@ -54,7 +54,7 @@
justify-content: space-between;
height: 100%;
width: 100%;
background: linear-gradient(0deg, rgba(0, 0, 0, 0.5) 5%, transparent 100%);
background: linear-gradient(0deg, rgba(0, 0, 0, 0.7) 20%, transparent 100%);
padding: 8px;
z-index: 2;
}
@@ -109,25 +109,26 @@
&__achievements {
display: flex;
flex-direction: column;
gap: 4px;
padding: 6px 8px;
opacity: 0;
transform: translateY(8px);
transition: all ease 0.2s;
pointer-events: none;
width: 100%;
}
&__achievements-gap {
display: flex;
align-items: center;
gap: 6px;
}
&__achievement-header {
display: flex;
align-items: center;
gap: 6px;
justify-content: space-between;
margin-bottom: 8px;
color: globals.$muted-color;
overflow: hidden;
height: 18px;
}
&__achievements-gap {
display: flex;
align-items: center;
gap: 8px;
}
&__achievement-trophy {
@@ -136,16 +137,15 @@
}
&__achievement-progress {
margin-top: 8px;
width: 100%;
height: 4px;
transition: all ease 0.2s;
background-color: rgba(255, 255, 255, 0.08);
background-color: rgba(255, 255, 255, 0.15);
border-radius: 4px;
overflow: hidden;
&::-webkit-progress-bar {
background-color: transparent;
background-color: rgba(255, 255, 255, 0.15);
border-radius: 4px;
}
@@ -164,15 +164,10 @@
}
&__achievement-count {
font-size: 12px;
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
white-space: nowrap;
}
&__achievement-percentage {
font-size: 11px;
color: rgba(255, 255, 255, 0.7);
white-space: nowrap;
}
@@ -281,9 +276,9 @@
}
}
/* Force fixed size for compact grid cells so cards render at 220x320 */
/* Responsive sizing for compact grid cells */
.library__games-grid--compact .library-game-card__wrapper {
width: 215px;
height: 320px;
aspect-ratio: unset;
width: 100%;
height: auto;
aspect-ratio: 215 / 320;
}

View File

@@ -147,7 +147,7 @@ export function LibraryGameCard({
<div className="library-game-card__achievement-header">
<div className="library-game-card__achievements-gap">
<TrophyIcon
size={14}
size={13}
className="library-game-card__achievement-trophy"
/>
<span className="library-game-card__achievement-count">

View File

@@ -2,7 +2,7 @@
.library {
&__content {
padding: calc(globals.$spacing-unit * 3);
padding: calc(globals.$spacing-unit * 2);
height: 100%;
width: 100%;
overflow-y: auto;
@@ -38,7 +38,6 @@
align-items: center;
justify-content: space-between;
width: 100%;
gap: calc(globals.$spacing-unit * 2);
}
&__controls-left {
@@ -159,31 +158,28 @@
}
}
// Compact view - smaller cards
// Compact view - smaller cards with responsive design
&--compact {
grid-template-columns: repeat(auto-fill, 215px);
grid-auto-rows: 320px;
justify-content: start;
grid-template-columns: repeat(3, 1fr);
@container #{globals.$app-container} (min-width: 900px) {
grid-template-columns: repeat(auto-fill, 215px);
grid-template-columns: repeat(5, 1fr);
}
@container #{globals.$app-container} (min-width: 1300px) {
grid-template-columns: repeat(auto-fill, 215px);
grid-template-columns: repeat(7, 1fr);
}
/* keep same pattern for very large screens */
@container #{globals.$app-container} (min-width: 2000px) {
grid-template-columns: repeat(auto-fill, 215px);
grid-template-columns: repeat(9, 1fr);
}
@container #{globals.$app-container} (min-width: 2600px) {
grid-template-columns: repeat(auto-fill, 215px);
grid-template-columns: repeat(12, 1fr);
}
@container #{globals.$app-container} (min-width: 3000px) {
grid-template-columns: repeat(auto-fill, 210px);
grid-template-columns: repeat(14, 1fr);
}
}
}

View File

@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from "react";
import { useLibrary, useAppDispatch } from "@renderer/hooks";
import { useLibrary, useAppDispatch, useAppSelector } from "@renderer/hooks";
import { setHeaderTitle } from "@renderer/features";
import { TelescopeIcon } from "@primer/octicons-react";
import { useTranslation } from "react-i18next";
@@ -8,7 +8,6 @@ import { LibraryGameCard } from "./library-game-card";
import { LibraryGameCardLarge } from "./library-game-card-large";
import { ViewOptions, ViewMode } from "./view-options";
import { FilterOptions, FilterOption } from "./filter-options";
import { SearchBar } from "./search-bar";
import "./library.scss";
export default function Library() {
@@ -20,7 +19,7 @@ export default function Library() {
const [viewMode, setViewMode] = useState<ViewMode>("compact");
const [filterBy, setFilterBy] = useState<FilterOption>("all");
const [searchQuery, setSearchQuery] = useState<string>("");
const searchQuery = useAppSelector((state) => state.library.searchQuery);
const dispatch = useAppDispatch();
const { t } = useTranslation("library");
@@ -133,7 +132,6 @@ export default function Library() {
</div>
<div className="library__controls-right">
<SearchBar value={searchQuery} onChange={setSearchQuery} />
<ViewOptions viewMode={viewMode} onViewModeChange={setViewMode} />
</div>
</div>

View File

@@ -1,75 +0,0 @@
.search-bar {
display: flex;
align-items: center;
&__container {
height: 32px;
display: flex;
align-items: center;
gap: 8px;
padding: 4px 12px;
background-color: transparent;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 6px;
transition: all 0.2s ease;
width: 250px;
&:focus-within {
width: 300px;
background: rgba(255, 255, 255, 0.02);
border-color: rgba(255, 255, 255, 0.2);
}
}
&__icon {
color: rgba(255, 255, 255, 0.75);
flex-shrink: 0;
transition: color 0.2s ease;
}
&__input {
flex: 1;
background: transparent;
border: none;
outline: none;
color: rgba(255, 255, 255, 0.9);
font-size: 14px;
font-family: inherit;
min-width: 0;
&::placeholder {
color: rgba(255, 255, 255, 0.6);
}
&:focus ~ .search-bar__icon {
color: rgba(255, 255, 255, 0.7);
}
}
&__clear {
flex-shrink: 0;
background: transparent;
border: none;
color: rgba(255, 255, 255, 0.65);
font-size: 18px;
font-weight: bold;
cursor: pointer;
padding: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
transition: all 0.2s ease;
&:hover {
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.8);
}
&:active {
background: rgba(255, 255, 255, 0.15);
}
}
}

View File

@@ -1,44 +0,0 @@
import { SearchIcon } from "@primer/octicons-react";
import { FC, useRef } from "react";
import { useTranslation } from "react-i18next";
import "./search-bar.scss";
interface SearchBarProps {
value: string;
onChange: (value: string) => void;
}
export const SearchBar: FC<SearchBarProps> = ({ value, onChange }) => {
const { t } = useTranslation();
const inputRef = useRef<HTMLInputElement>(null);
const handleClear = () => {
onChange("");
inputRef.current?.focus();
};
return (
<div className="search-bar">
<div className="search-bar__container">
<SearchIcon size={16} className="search-bar__icon" />
<input
ref={inputRef}
type="text"
className="search-bar__input"
placeholder={t("Search library", { defaultValue: "Search library" })}
value={value}
onChange={(e) => onChange(e.target.value)}
/>
{value && (
<button
className="search-bar__clear"
onClick={handleClear}
aria-label="Clear search"
>
×
</button>
)}
</div>
</div>
);
};

View File

@@ -26,7 +26,7 @@
display: flex;
align-items: center;
gap: calc(globals.$spacing-unit);
padding: 8px 10px;
padding: 10px;
border-radius: 6px;
background: rgba(255, 255, 255, 0.04);
border: none;
@@ -38,8 +38,8 @@
white-space: nowrap;
&:hover {
color: rgba(255, 255, 255, 0.95);
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.9);
background: rgba(255, 255, 255, 0.08);
}
&.active {