mirror of
https://github.com/HananoshikaYomaru/openfreebooks.git
synced 2026-11-12 20:45:52 +00:00
Refactor CatalogApp component to integrate CatalogFiltersMenu for improved filtering functionality, update layout structure in catalog-canvas-view for better readability, and enhance SASS styles for catalog header and filter menu. Remove unused functions and streamline canvas rendering logic in catalog-to-canvas.
This commit is contained in:
parent
81b316e08d
commit
9ddcce79c6
15 changed files with 2351 additions and 294 deletions
|
|
@ -2,6 +2,7 @@ import { createMemo, createSignal, For, lazy, onCleanup, onMount, Show, Suspense
|
|||
import { subjectToCanvas } from "../lib/catalog-to-canvas";
|
||||
import { watchSiteTheme } from "../lib/catalog-canvas-theme";
|
||||
import { CatalogChapterCard } from "./catalog-chapter-card";
|
||||
import { CatalogFiltersMenu } from "./catalog-filters-menu";
|
||||
import type {
|
||||
CatalogChapter,
|
||||
CatalogData,
|
||||
|
|
@ -151,7 +152,10 @@ export function CatalogApp() {
|
|||
});
|
||||
|
||||
return (
|
||||
<div class="catalog-shell">
|
||||
<div
|
||||
class="catalog-shell"
|
||||
classList={{ "catalog-shell--tree": viewMode() === "tree" }}
|
||||
>
|
||||
<aside class="catalog-sidebar" aria-label="Subjects">
|
||||
<div class="catalog-sidebar__header">
|
||||
<p class="catalog-sidebar__kicker">{data.title}</p>
|
||||
|
|
@ -175,11 +179,11 @@ export function CatalogApp() {
|
|||
|
||||
<div class="catalog-main">
|
||||
<header class="catalog-header">
|
||||
<div class="catalog-header__intro">
|
||||
<h1 class="catalog-header__title">{currentSubject().name}</h1>
|
||||
<p class="catalog-header__stats">{statsLabel()}</p>
|
||||
</div>
|
||||
<div class="catalog-header__controls">
|
||||
<div class="catalog-header__head">
|
||||
<div class="catalog-header__intro">
|
||||
<h1 class="catalog-header__title">{currentSubject().name}</h1>
|
||||
<p class="catalog-header__stats">{statsLabel()}</p>
|
||||
</div>
|
||||
<div class="catalog-view-toggle" role="tablist" aria-label="Chapter layout">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -200,31 +204,13 @@ export function CatalogApp() {
|
|||
Map
|
||||
</button>
|
||||
</div>
|
||||
<div class="catalog-filters-wrap">
|
||||
<p class="catalog-filters__label">Filter by curriculum</p>
|
||||
<div class="catalog-filters" role="group" aria-label="Curriculum filters">
|
||||
<button
|
||||
type="button"
|
||||
class="catalog-filter-btn"
|
||||
classList={{ "is-active": activeFilters().size === 0 }}
|
||||
onClick={() => toggleFilter("ALL")}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
<For each={data.curriculums}>
|
||||
{(curriculum) => (
|
||||
<button
|
||||
type="button"
|
||||
class="catalog-filter-btn"
|
||||
classList={{ "is-active": activeFilters().has(curriculum) }}
|
||||
onClick={() => toggleFilter(curriculum)}
|
||||
>
|
||||
{curriculum}
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="catalog-header__filters">
|
||||
<CatalogFiltersMenu
|
||||
curriculums={data.curriculums}
|
||||
activeFilters={activeFilters}
|
||||
onToggle={toggleFilter}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
|
|
|||
|
|
@ -13,14 +13,14 @@ import {
|
|||
} from "json-canvas-viewer";
|
||||
import type { CatalogChapter } from "../../../data/catalog.types";
|
||||
import { renderCatalogChapterCardElement } from "./catalog-chapter-card";
|
||||
import { chapterSlugFromNodeId, relayoutCanvasChapterHeights } from "../lib/catalog-to-canvas";
|
||||
import { measureRenderedMapChapterCards } from "../lib/catalog-chapter-card-layout";
|
||||
import { chapterSlugFromNodeId } from "../lib/catalog-to-canvas";
|
||||
import {
|
||||
syncViewerTheme,
|
||||
VIEWER_THEME_COLORS,
|
||||
siteCanvasTheme,
|
||||
watchSiteTheme,
|
||||
} from "../lib/catalog-canvas-theme";
|
||||
import { focusReadableCatalogView } from "../lib/catalog-canvas-viewport";
|
||||
|
||||
type CatalogCanvasViewProps = {
|
||||
canvas: JSONCanvas;
|
||||
|
|
@ -54,21 +54,6 @@ function bindPageScrollWheel(container: HTMLElement): () => void {
|
|||
return () => container.removeEventListener("wheel", onWheel, { capture: true });
|
||||
}
|
||||
|
||||
function waitForFrames(count = 2): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
let remaining = count;
|
||||
const step = () => {
|
||||
remaining -= 1;
|
||||
if (remaining <= 0) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
requestAnimationFrame(step);
|
||||
};
|
||||
requestAnimationFrame(step);
|
||||
});
|
||||
}
|
||||
|
||||
function buildNodeComponents(
|
||||
subjectId: string,
|
||||
chapters: Record<string, CatalogChapter>
|
||||
|
|
@ -115,20 +100,7 @@ export function CatalogCanvasView(props: CatalogCanvasViewProps) {
|
|||
lastSerialized = cacheKey;
|
||||
viewer.load({ canvas });
|
||||
syncViewerTheme(viewer);
|
||||
|
||||
await waitForFrames();
|
||||
if (generation !== loadGeneration || !viewer) return;
|
||||
|
||||
const measured = measureRenderedMapChapterCards(container);
|
||||
const fitted = relayoutCanvasChapterHeights(canvas, measured);
|
||||
if (!fitted) return;
|
||||
|
||||
const fittedKey = `${serializeCanvas(fitted)}|${chaptersKey(chapters)}`;
|
||||
if (fittedKey === lastSerialized) return;
|
||||
|
||||
lastSerialized = fittedKey;
|
||||
viewer.load({ canvas: fitted });
|
||||
syncViewerTheme(viewer);
|
||||
focusReadableCatalogView(viewer, container);
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
|
|
|
|||
94
frontend/src/components/catalog-filters-menu.tsx
Normal file
94
frontend/src/components/catalog-filters-menu.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
import { createMemo, createSignal, For, onCleanup, onMount, Show } from "solid-js";
|
||||
|
||||
type CatalogFiltersMenuProps = {
|
||||
curriculums: string[];
|
||||
activeFilters: () => Set<string>;
|
||||
onToggle: (curriculum: string) => void;
|
||||
};
|
||||
|
||||
function filtersSummary(active: Set<string>): string {
|
||||
if (active.size === 0) return "All";
|
||||
return Array.from(active).join(", ");
|
||||
}
|
||||
|
||||
export function CatalogFiltersMenu(props: CatalogFiltersMenuProps) {
|
||||
const [open, setOpen] = createSignal(false);
|
||||
let root!: HTMLDivElement;
|
||||
|
||||
const summary = createMemo(() => filtersSummary(props.activeFilters()));
|
||||
|
||||
const close = () => setOpen(false);
|
||||
|
||||
onMount(() => {
|
||||
const onDocumentClick = (event: MouseEvent) => {
|
||||
if (!open() || !root.contains(event.target as Node)) {
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === "Escape") {
|
||||
close();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("click", onDocumentClick);
|
||||
document.addEventListener("keydown", onKeyDown);
|
||||
onCleanup(() => {
|
||||
document.removeEventListener("click", onDocumentClick);
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="catalog-filter-menu" ref={root}>
|
||||
<button
|
||||
type="button"
|
||||
class="catalog-filter-menu__trigger"
|
||||
aria-expanded={open() ? "true" : "false"}
|
||||
aria-haspopup="true"
|
||||
aria-controls="catalog-filter-menu-panel"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
setOpen((isOpen) => !isOpen);
|
||||
}}
|
||||
>
|
||||
<span class="catalog-filter-menu__trigger-kicker">Filter by curriculum</span>
|
||||
<span class="catalog-filter-menu__trigger-value">{summary()}</span>
|
||||
<span class="catalog-filter-menu__chevron" aria-hidden="true" />
|
||||
</button>
|
||||
<Show when={open()}>
|
||||
<div
|
||||
id="catalog-filter-menu-panel"
|
||||
class="catalog-filter-menu__panel"
|
||||
role="group"
|
||||
aria-label="Curriculum filters"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<label class="catalog-filter-menu__option">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="catalog-filter-menu__checkbox"
|
||||
checked={props.activeFilters().size === 0}
|
||||
onChange={() => props.onToggle("ALL")}
|
||||
/>
|
||||
<span class="catalog-filter-menu__option-label">All</span>
|
||||
</label>
|
||||
<For each={props.curriculums}>
|
||||
{(curriculum) => (
|
||||
<label class="catalog-filter-menu__option">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="catalog-filter-menu__checkbox"
|
||||
checked={props.activeFilters().has(curriculum)}
|
||||
onChange={() => props.onToggle(curriculum)}
|
||||
/>
|
||||
<span class="catalog-filter-menu__option-label">{curriculum}</span>
|
||||
</label>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
31
frontend/src/lib/catalog-canvas-viewport.ts
Normal file
31
frontend/src/lib/catalog-canvas-viewport.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { internal, type JSONCanvasViewerInterface } from "json-canvas-viewer";
|
||||
import { CHAPTER_NODE_WIDTH, STRAND_GAP_X } from "./catalog-to-canvas";
|
||||
|
||||
/** Roughly one strand column plus peek of the next — readable card text, not full board. */
|
||||
const READABLE_COLUMNS_IN_VIEW = 1.15;
|
||||
const MIN_READABLE_SCALE = 0.78;
|
||||
const MAX_READABLE_SCALE = 1.08;
|
||||
|
||||
export function focusReadableCatalogView(
|
||||
viewer: JSONCanvasViewerInterface,
|
||||
container: HTMLElement
|
||||
): void {
|
||||
const dm = viewer.container.get(internal.DataManager);
|
||||
const bounds = dm.data.nodeBounds;
|
||||
if (bounds.width <= 0 || bounds.height <= 0) return;
|
||||
|
||||
const viewportW = container.clientWidth;
|
||||
const viewportH = container.clientHeight;
|
||||
if (viewportW < 1 || viewportH < 1) return;
|
||||
|
||||
const columnSpan = CHAPTER_NODE_WIDTH + STRAND_GAP_X;
|
||||
const scaleByWidth = viewportW / (columnSpan * READABLE_COLUMNS_IN_VIEW);
|
||||
const scale = Math.min(MAX_READABLE_SCALE, Math.max(MIN_READABLE_SCALE, scaleByWidth));
|
||||
|
||||
const focusX = bounds.minX + Math.min(bounds.width * 0.28, columnSpan * 0.65);
|
||||
const focusY = bounds.minY + Math.min(bounds.height * 0.22, 280);
|
||||
|
||||
dm.data.scale = scale;
|
||||
dm.data.offsetX = viewportW / 2 - focusX * scale;
|
||||
dm.data.offsetY = viewportH / 2 - focusY * scale;
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ import { measureAllMapChapterCardHeights } from "./catalog-chapter-card-layout";
|
|||
|
||||
export const CHAPTER_NODE_WIDTH = 300;
|
||||
/** Horizontal gap between strand columns (groups must not overlap). */
|
||||
const STRAND_GAP_X = 112;
|
||||
export const STRAND_GAP_X = 112;
|
||||
const GAP_Y = 48;
|
||||
const GROUP_PAD = 28;
|
||||
|
||||
|
|
@ -327,7 +327,12 @@ export function relayoutCanvasChapterHeights(
|
|||
for (const node of chapterNodes) {
|
||||
const slug = chapterSlugFromNodeId(node.id);
|
||||
if (!slug) continue;
|
||||
mergedHeights.set(slug, Math.max(node.height ?? 0, heightBySlug.get(slug) ?? 0));
|
||||
const measured = heightBySlug.get(slug);
|
||||
if (measured !== undefined) {
|
||||
mergedHeights.set(slug, measured);
|
||||
} else {
|
||||
mergedHeights.set(slug, node.height ?? 0);
|
||||
}
|
||||
}
|
||||
|
||||
const slots: ChapterLayoutSlot[] = chapterNodes.map((node, index) => {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,41 @@
|
|||
// -----------------------------------------------------------------------------
|
||||
// Shared
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@mixin catalog-chapter-title-link {
|
||||
display: inline;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
background: linear-gradient(var(--accent), var(--accent)) 0 calc(100% - 2px) / 0% 1px no-repeat;
|
||||
transition: background-size 180ms ease;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background-size: 100% 1px;
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin catalog-chapter-title-arrow {
|
||||
display: inline-block;
|
||||
margin-left: 0.2em;
|
||||
color: var(--accent);
|
||||
transition: transform 180ms ease;
|
||||
}
|
||||
|
||||
@mixin catalog-chapter-title-link-with-arrow {
|
||||
@include catalog-chapter-title-link;
|
||||
|
||||
&:hover .catalog-chapter-card__title-arrow,
|
||||
&:focus-visible .catalog-chapter-card__title-arrow {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Page & shell
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
.catalog-page {
|
||||
padding: 24px 0 64px;
|
||||
min-height: calc(100vh - 120px);
|
||||
|
|
@ -23,6 +61,10 @@
|
|||
box-shadow: 0 12px 40px var(--shadow-soft);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Sidebar
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
.catalog-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -73,24 +115,28 @@
|
|||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: background 200ms ease, color 200ms ease, box-shadow 200ms ease;
|
||||
}
|
||||
|
||||
.catalog-subject-btn:hover,
|
||||
.catalog-subject-btn:focus-visible {
|
||||
background: var(--card-bg);
|
||||
color: var(--ink);
|
||||
}
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background: var(--card-bg);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.catalog-subject-btn.is-active {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 14px rgba(154, 107, 46, 0.35);
|
||||
&.is-active {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
box-shadow: 0 4px 14px rgba(154, 107, 46, 0.35);
|
||||
}
|
||||
}
|
||||
|
||||
[data-theme="dark"] .catalog-subject-btn.is-active {
|
||||
box-shadow: 0 4px 14px rgba(196, 146, 74, 0.25);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Main column
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
.catalog-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -98,15 +144,38 @@
|
|||
background: var(--card-bg);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Header
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
.catalog-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
gap: 20px 32px;
|
||||
padding: 28px 32px 24px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: var(--card-bg);
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.catalog-header__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.catalog-header__intro {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.catalog-header__title {
|
||||
|
|
@ -119,20 +188,15 @@
|
|||
}
|
||||
|
||||
.catalog-header__stats {
|
||||
margin: 8px 0 0;
|
||||
margin: 0;
|
||||
font-size: 0.9rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.catalog-header__controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.catalog-view-toggle {
|
||||
display: inline-flex;
|
||||
align-self: flex-end;
|
||||
height: 48px;
|
||||
padding: 4px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
|
|
@ -150,65 +214,128 @@
|
|||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: background 200ms ease, color 200ms ease;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
&[aria-selected="true"] {
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
}
|
||||
}
|
||||
|
||||
.catalog-view-toggle__btn:hover,
|
||||
.catalog-view-toggle__btn:focus-visible {
|
||||
color: var(--ink);
|
||||
.catalog-header__filters {
|
||||
flex: 0 1 auto;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.catalog-view-toggle__btn[aria-selected="true"] {
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
// -----------------------------------------------------------------------------
|
||||
// Filter menu (glass dropdown — not --card-bg; see panel background)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
.catalog-filter-menu {
|
||||
position: relative;
|
||||
width: min(100%, 18rem);
|
||||
}
|
||||
|
||||
.catalog-filters-wrap {
|
||||
.catalog-filter-menu__trigger {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
font-family: var(--font-body);
|
||||
color: var(--ink);
|
||||
background: var(--paper);
|
||||
cursor: pointer;
|
||||
transition: border-color 200ms ease, background 200ms ease;
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
border-color: var(--accent);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.catalog-filters__label {
|
||||
margin: 0;
|
||||
.catalog-filter-menu__trigger-kicker {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.65rem;
|
||||
font-size: 0.62rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.catalog-filters {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.catalog-filter-btn {
|
||||
padding: 6px 16px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.82rem;
|
||||
.catalog-filter-menu__trigger-value {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
color: var(--soft-ink);
|
||||
background: var(--paper);
|
||||
cursor: pointer;
|
||||
transition: background 200ms ease, color 200ms ease, border-color 200ms ease;
|
||||
}
|
||||
|
||||
.catalog-filter-btn:hover,
|
||||
.catalog-filter-btn:focus-visible {
|
||||
border-color: var(--accent);
|
||||
.catalog-filter-menu__chevron {
|
||||
width: 0.45rem;
|
||||
height: 0.45rem;
|
||||
border-right: 2px solid var(--soft-ink);
|
||||
border-bottom: 2px solid var(--soft-ink);
|
||||
transform: rotate(45deg) translateY(-2px);
|
||||
}
|
||||
|
||||
.catalog-filter-menu__panel {
|
||||
position: absolute;
|
||||
top: calc(100% + 6px);
|
||||
right: 0;
|
||||
z-index: 30;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 100%;
|
||||
padding: 6px;
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 10px;
|
||||
background: var(--header-bg-scrolled);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
box-shadow: 0 10px 28px var(--shadow-soft);
|
||||
}
|
||||
|
||||
.catalog-filter-menu__option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
cursor: pointer;
|
||||
transition: background 150ms ease;
|
||||
|
||||
&:hover {
|
||||
background: color-mix(in srgb, var(--paper) 55%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.catalog-filter-btn.is-active {
|
||||
background: var(--ink);
|
||||
border-color: var(--ink);
|
||||
color: var(--paper);
|
||||
.catalog-filter-menu__checkbox {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin: 0;
|
||||
accent-color: var(--accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.catalog-filter-menu__option-label {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Content area
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
.catalog-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
|
@ -216,14 +343,25 @@
|
|||
}
|
||||
|
||||
.catalog-content--tree {
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 0 560px;
|
||||
height: 560px;
|
||||
flex: 1 1 0%;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.catalog-canvas-loading {
|
||||
margin: auto;
|
||||
padding: 48px;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Map canvas viewer
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
.catalog-canvas-view {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
|
|
@ -242,43 +380,42 @@
|
|||
--jcv-control-fg: var(--ink);
|
||||
}
|
||||
|
||||
/* Override json-canvas-viewer default markdown accent colors */
|
||||
:global(.JCV-markdown-content) {
|
||||
color: var(--ink);
|
||||
font-family: var(--font-body);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
:global(.JCV-markdown-content p) {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
:global(.JCV-markdown-content p + p) {
|
||||
margin-top: 0.35rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
p + p {
|
||||
margin-top: 0.35rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
:global(.JCV-markdown-content strong) {
|
||||
color: var(--ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
strong {
|
||||
color: var(--ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:global(.JCV-markdown-content em) {
|
||||
color: var(--muted);
|
||||
font-style: italic;
|
||||
}
|
||||
em {
|
||||
color: var(--muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
:global(.JCV-markdown-content a) {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
|
||||
:global(.JCV-markdown-content a:hover) {
|
||||
text-decoration: underline;
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(.JCV-minimap-container) {
|
||||
|
|
@ -306,32 +443,11 @@
|
|||
}
|
||||
|
||||
:global(.catalog-chapter-card--map .catalog-chapter-card__title-link) {
|
||||
display: inline;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
background: linear-gradient(var(--accent), var(--accent)) 0 calc(100% - 2px) / 0% 1px
|
||||
no-repeat;
|
||||
transition: background-size 180ms ease;
|
||||
}
|
||||
|
||||
:global(.catalog-chapter-card--map .catalog-chapter-card__title-link:hover),
|
||||
:global(.catalog-chapter-card--map .catalog-chapter-card__title-link:focus-visible) {
|
||||
background-size: 100% 1px;
|
||||
outline: none;
|
||||
@include catalog-chapter-title-link-with-arrow;
|
||||
}
|
||||
|
||||
:global(.catalog-chapter-card--map .catalog-chapter-card__title-arrow) {
|
||||
display: inline-block;
|
||||
margin-left: 0.2em;
|
||||
color: var(--accent);
|
||||
transition: transform 180ms ease;
|
||||
}
|
||||
|
||||
:global(.catalog-chapter-card--map .catalog-chapter-card__title-link:hover .catalog-chapter-card__title-arrow),
|
||||
:global(
|
||||
.catalog-chapter-card--map .catalog-chapter-card__title-link:focus-visible .catalog-chapter-card__title-arrow
|
||||
) {
|
||||
transform: translateX(2px);
|
||||
@include catalog-chapter-title-arrow;
|
||||
}
|
||||
|
||||
:global(.catalog-chapter-card--map .catalog-chapter-card__meta) {
|
||||
|
|
@ -346,20 +462,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
.catalog-canvas-loading {
|
||||
margin: auto;
|
||||
padding: 48px;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
// List layout (strands, tree, cards)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
.catalog-strand {
|
||||
margin-bottom: 48px;
|
||||
animation: catalog-fade-in 0.4s ease-out forwards;
|
||||
}
|
||||
|
||||
.catalog-strand:last-child {
|
||||
margin-bottom: 0;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes catalog-fade-in {
|
||||
|
|
@ -401,44 +514,44 @@
|
|||
margin: 0;
|
||||
padding: 0 0 0 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.catalog-tree::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
bottom: 8px;
|
||||
left: 11px;
|
||||
width: 2px;
|
||||
background: var(--line);
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
bottom: 8px;
|
||||
left: 11px;
|
||||
width: 2px;
|
||||
background: var(--line);
|
||||
}
|
||||
}
|
||||
|
||||
.catalog-tree__item {
|
||||
position: relative;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.catalog-tree__item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.catalog-tree__item::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -19px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--line);
|
||||
box-shadow: 0 0 0 4px var(--card-bg);
|
||||
transition: background 200ms ease;
|
||||
}
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -19px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background: var(--line);
|
||||
box-shadow: 0 0 0 4px var(--card-bg);
|
||||
transition: background 200ms ease;
|
||||
}
|
||||
|
||||
.catalog-tree__item:hover::before,
|
||||
.catalog-tree__item:focus-within::before {
|
||||
background: var(--accent);
|
||||
&:hover::before,
|
||||
&:focus-within::before {
|
||||
background: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
.catalog-chapter-card {
|
||||
|
|
@ -453,30 +566,24 @@
|
|||
text-decoration: none;
|
||||
color: var(--ink);
|
||||
transition: border-color 200ms ease, box-shadow 200ms ease;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.catalog-chapter-card:not(.catalog-chapter-card--map) {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.catalog-chapter-card:not(.catalog-chapter-card--map) .catalog-chapter-card__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
&--planned {
|
||||
opacity: 0.85;
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
a.catalog-chapter-card:hover,
|
||||
a.catalog-chapter-card:focus-visible {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 6px 20px var(--shadow-soft);
|
||||
}
|
||||
a.catalog-chapter-card {
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 6px 20px var(--shadow-soft);
|
||||
}
|
||||
|
||||
.catalog-chapter-card--planned {
|
||||
opacity: 0.85;
|
||||
cursor: default;
|
||||
&:hover .catalog-chapter-card__number,
|
||||
&:focus-visible .catalog-chapter-card__number {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
.catalog-chapter-card__main {
|
||||
|
|
@ -495,11 +602,6 @@ a.catalog-chapter-card:focus-visible {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
a.catalog-chapter-card:hover .catalog-chapter-card__number,
|
||||
a.catalog-chapter-card:focus-visible .catalog-chapter-card__number {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.catalog-chapter-card__title {
|
||||
margin: 0;
|
||||
font-family: var(--font-heading);
|
||||
|
|
@ -509,29 +611,11 @@ a.catalog-chapter-card:focus-visible .catalog-chapter-card__number {
|
|||
}
|
||||
|
||||
.catalog-chapter-card__title-link {
|
||||
display: inline;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
background: linear-gradient(var(--accent), var(--accent)) 0 calc(100% - 2px) / 0% 1px no-repeat;
|
||||
transition: background-size 180ms ease;
|
||||
}
|
||||
|
||||
.catalog-chapter-card__title-link:hover,
|
||||
.catalog-chapter-card__title-link:focus-visible {
|
||||
background-size: 100% 1px;
|
||||
outline: none;
|
||||
@include catalog-chapter-title-link-with-arrow;
|
||||
}
|
||||
|
||||
.catalog-chapter-card__title-arrow {
|
||||
display: inline-block;
|
||||
margin-left: 0.2em;
|
||||
color: var(--accent);
|
||||
transition: transform 180ms ease;
|
||||
}
|
||||
|
||||
.catalog-chapter-card__title-link:hover .catalog-chapter-card__title-arrow,
|
||||
.catalog-chapter-card__title-link:focus-visible .catalog-chapter-card__title-arrow {
|
||||
transform: translateX(2px);
|
||||
@include catalog-chapter-title-arrow;
|
||||
}
|
||||
|
||||
.catalog-map-measure-shell {
|
||||
|
|
@ -560,11 +644,9 @@ a.catalog-chapter-card:focus-visible .catalog-chapter-card__number {
|
|||
padding-left: 48px;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.catalog-chapter-card__meta {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
// Badges & empty state
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
.catalog-badge {
|
||||
padding: 4px 8px;
|
||||
|
|
@ -575,48 +657,48 @@ a.catalog-chapter-card:focus-visible .catalog-chapter-card__number {
|
|||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.catalog-badge--dse {
|
||||
color: #1d4ed8;
|
||||
background: rgba(59, 130, 246, 0.12);
|
||||
border-color: rgba(59, 130, 246, 0.25);
|
||||
}
|
||||
&--dse {
|
||||
color: #1d4ed8;
|
||||
background: rgba(59, 130, 246, 0.12);
|
||||
border-color: rgba(59, 130, 246, 0.25);
|
||||
}
|
||||
|
||||
.catalog-badge--ib {
|
||||
color: #6d28d9;
|
||||
background: rgba(139, 92, 246, 0.12);
|
||||
border-color: rgba(139, 92, 246, 0.25);
|
||||
}
|
||||
&--ib {
|
||||
color: #6d28d9;
|
||||
background: rgba(139, 92, 246, 0.12);
|
||||
border-color: rgba(139, 92, 246, 0.25);
|
||||
}
|
||||
|
||||
.catalog-badge--a-level {
|
||||
color: #047857;
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
border-color: rgba(16, 185, 129, 0.25);
|
||||
}
|
||||
&--a-level {
|
||||
color: #047857;
|
||||
background: rgba(16, 185, 129, 0.12);
|
||||
border-color: rgba(16, 185, 129, 0.25);
|
||||
}
|
||||
|
||||
.catalog-badge--ap {
|
||||
color: #b45309;
|
||||
background: rgba(245, 158, 11, 0.12);
|
||||
border-color: rgba(245, 158, 11, 0.25);
|
||||
}
|
||||
&--ap {
|
||||
color: #b45309;
|
||||
background: rgba(245, 158, 11, 0.12);
|
||||
border-color: rgba(245, 158, 11, 0.25);
|
||||
}
|
||||
|
||||
.catalog-badge--igcse {
|
||||
color: #be123c;
|
||||
background: rgba(244, 63, 94, 0.12);
|
||||
border-color: rgba(244, 63, 94, 0.25);
|
||||
}
|
||||
&--igcse {
|
||||
color: #be123c;
|
||||
background: rgba(244, 63, 94, 0.12);
|
||||
border-color: rgba(244, 63, 94, 0.25);
|
||||
}
|
||||
|
||||
.catalog-badge--extension {
|
||||
color: #9a3412;
|
||||
background: rgba(234, 88, 12, 0.1);
|
||||
border-color: rgba(234, 88, 12, 0.28);
|
||||
}
|
||||
&--extension {
|
||||
color: #9a3412;
|
||||
background: rgba(234, 88, 12, 0.1);
|
||||
border-color: rgba(234, 88, 12, 0.28);
|
||||
}
|
||||
|
||||
.catalog-badge--default {
|
||||
color: var(--soft-ink);
|
||||
background: var(--paper);
|
||||
border-color: var(--line);
|
||||
&--default {
|
||||
color: var(--soft-ink);
|
||||
background: var(--paper);
|
||||
border-color: var(--line);
|
||||
}
|
||||
}
|
||||
|
||||
.catalog-empty {
|
||||
|
|
@ -644,6 +726,48 @@ a.catalog-chapter-card:focus-visible .catalog-chapter-card__number {
|
|||
color: var(--soft-ink);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Responsive
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.catalog-chapter-card:not(.catalog-chapter-card--map) {
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
|
||||
.catalog-chapter-card__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.catalog-chapter-card__meta {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 769px) {
|
||||
.catalog-shell--tree {
|
||||
max-width: min(1680px, calc(100% - 48px));
|
||||
height: calc(100vh - 160px);
|
||||
max-height: calc(100vh - 160px);
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.catalog-shell--tree .catalog-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.catalog-shell--tree .catalog-header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.catalog-shell {
|
||||
grid-template-columns: 1fr;
|
||||
|
|
@ -669,11 +793,48 @@ a.catalog-chapter-card:focus-visible .catalog-chapter-card__number {
|
|||
|
||||
.catalog-header {
|
||||
padding: 20px 20px 16px;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.catalog-header__head {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.catalog-header__intro {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.catalog-view-toggle {
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
justify-content: stretch;
|
||||
|
||||
&__btn {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.catalog-header__filters {
|
||||
width: 100%;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.catalog-filter-menu {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.catalog-content {
|
||||
padding: 24px 20px;
|
||||
}
|
||||
|
||||
.catalog-content--tree {
|
||||
flex: 1 1 0%;
|
||||
min-height: min(70vh, 560px);
|
||||
max-height: min(70vh, 560px);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
2
themes/openfreebooks/static/js/catalog-app-CvFENXwo.js
Normal file
2
themes/openfreebooks/static/js/catalog-app-CvFENXwo.js
Normal file
File diff suppressed because one or more lines are too long
2
themes/openfreebooks/static/js/catalog-app-D-Pa7m8K.js
Normal file
2
themes/openfreebooks/static/js/catalog-app-D-Pa7m8K.js
Normal file
File diff suppressed because one or more lines are too long
2
themes/openfreebooks/static/js/catalog-app-DydLfc7y.js
Normal file
2
themes/openfreebooks/static/js/catalog-app-DydLfc7y.js
Normal file
File diff suppressed because one or more lines are too long
2
themes/openfreebooks/static/js/catalog-app-e24aHQLu.js
Normal file
2
themes/openfreebooks/static/js/catalog-app-e24aHQLu.js
Normal file
File diff suppressed because one or more lines are too long
450
themes/openfreebooks/static/js/catalog-canvas-view-CqwlRFC-.js
Normal file
450
themes/openfreebooks/static/js/catalog-canvas-view-CqwlRFC-.js
Normal file
File diff suppressed because one or more lines are too long
450
themes/openfreebooks/static/js/catalog-canvas-view-CyxXgKFM.js
Normal file
450
themes/openfreebooks/static/js/catalog-canvas-view-CyxXgKFM.js
Normal file
File diff suppressed because one or more lines are too long
450
themes/openfreebooks/static/js/catalog-canvas-view-DBCmUv-a.js
Normal file
450
themes/openfreebooks/static/js/catalog-canvas-view-DBCmUv-a.js
Normal file
File diff suppressed because one or more lines are too long
450
themes/openfreebooks/static/js/catalog-canvas-view-brNOkk1Q.js
Normal file
450
themes/openfreebooks/static/js/catalog-canvas-view-brNOkk1Q.js
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue