Skip to content
Open
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
2f87065
Route the reading-log JSON writes to FastAPI in dev
lokesh Aug 22, 2026
6aac29c
Add /reading-state.json for a batch of works
lokesh Aug 22, 2026
661e2de
Add ol-shelf-button and ol-book-actions
lokesh Aug 22, 2026
801d151
Give the design gallery a Books section
lokesh Aug 22, 2026
3cb19f5
Add ol-book-cover
lokesh Aug 22, 2026
1bc774d
Lighten and tighten the blank cover
lokesh Aug 22, 2026
0e2704f
Centre the actions popover under its trigger
lokesh Aug 22, 2026
a25ad17
Make bottom-start the popover default
lokesh Aug 22, 2026
e2763a3
Stop the overlay demo bouncing to login
lokesh Aug 22, 2026
b4024b0
Drop the shelf-button demo that showed a shelf it did not have
lokesh Aug 22, 2026
c1be2d5
Fix four things the components got wrong
lokesh Aug 22, 2026
cad8535
Ask when the reader finished the book
lokesh Aug 22, 2026
8b8d94d
Make the cover hover card always on and trim the design notes
lokesh Aug 24, 2026
deb72c6
Give the split shelf button the raised secondary treatment
lokesh Aug 24, 2026
66ee78e
Bring the check-in editing and popover polish over from #13400
lokesh Aug 24, 2026
f781673
Route book-component API calls through /_fast on testing
lokesh Aug 24, 2026
c587178
Drop /reading-state.json and server-render the demo state
lokesh Aug 25, 2026
bd4009d
Keep the shelf button's hover blue when it is selected
lokesh Aug 25, 2026
e6a7393
Reuse the webpack-side helpers and build popover panes on demand
lokesh Aug 25, 2026
478d97a
Show the check-in date on the Already Read row
lokesh Aug 25, 2026
1a1dd17
Move date helpers to a shared module and centralize lists state
lokesh Aug 25, 2026
30f2d88
Merge remote-tracking branch 'upstream/master' into feat/shelf-button…
lokesh Aug 26, 2026
b392e37
Roll optimistic writes back from a snapshot
lokesh Aug 26, 2026
a49c25c
Merge remote-tracking branch 'upstream/master' into feat/shelf-button…
lokesh Aug 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,300 changes: 1,300 additions & 0 deletions openlibrary/components/lit/OlBookActions.js

Large diffs are not rendered by default.

212 changes: 212 additions & 0 deletions openlibrary/components/lit/OlBookCover.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import { LitElement, css, html, nothing } from 'lit';
import { translate } from './utils/labels.js';
import './OlTooltip.js';

export const DEFAULT_LABELS = {
by: 'by %(name)s',
};

/**
* A book cover at a fixed 2:3 ratio: the artwork when there is one, a generated
* title/author panel when there isn't, and a corner for whatever the surface
* wants to float over it.
*
* Knows nothing about shelves, availability or search — give it a URL and a
* title. `overlay` is the slot the save button goes in; the component owns the
* corner position so a consumer never has to.
*
* A pointer gets a hover card carrying the title, year and author.
* `ol-tooltip` arms on the same media query a cover-card layout uses to
* hide that text below the cover, so exactly one of the two shows.
*
* @element ol-book-cover
*
* @prop {String} src - Cover image URL; empty draws the generated blank cover
* @prop {String} bookTitle - The book's title. Named `book-title` because a
* `title` attribute would draw a native browser tooltip over the whole host
* @prop {String} authors - Author names, already joined for display
* @prop {String} year - First publication year, shown in the hover card
* @prop {String} href - Link target; empty renders the cover unlinked
* @prop {String} size - "medium" (default) or "small"; small drops the author
* from the blank cover, which has no room for it
* @prop {Object} labels - Translated strings, merged over DEFAULT_LABELS
*
* @slot overlay - Pinned to the cover's top-right corner, over the artwork
*
* @fires ol-book-cover-click - The cover link was clicked. detail: { href }
*/
export class OlBookCover extends LitElement {
static properties = {
src: { type: String },
bookTitle: { type: String, attribute: 'book-title' },
authors: { type: String },
year: { type: String },
href: { type: String },
size: { type: String, reflect: true },
labels: { type: Object },
};

static styles = css`
:host {
position: relative;
display: block;
aspect-ratio: 2 / 3;
border-radius: var(--border-radius-thumbnail);
overflow: hidden;
background: var(--color-surface-sunken);
font-family: var(--font-family-body);
}

.link {
display: block;
height: 100%;
}

/* Wraps the cover link only, keeping the overlay out of the trigger area. */
ol-tooltip {
display: block;
height: 100%;
}

.img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}

.blank {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
box-sizing: border-box;
padding: var(--spacing-inset-sm);
background: linear-gradient(160deg, var(--neutral-600), var(--neutral-800));
color: var(--color-text-inverse);
text-align: center;
}

.blank__title {
font-family: var(--font-family-heading);
font-size: var(--font-size-title-medium);
font-weight: 500;
line-height: var(--line-height-tight);
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
}

/* A 72px cover has room for neither the padding nor the type of a
full-size one. */
:host([size="small"]) .blank {
padding: var(--spacing-inset-xs);
}

:host([size="small"]) .blank__title {
font-size: var(--font-size-label-medium);
}

.blank__author {
font-size: var(--font-size-label-small);
letter-spacing: 0.08em;
text-transform: uppercase;
opacity: 0.85;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

/* The corner is the cover's to own. Whatever is slotted in goes static
inside it — a wrapper like <ol-book-actions> takes the corner and its
own trigger sits inside that. */
slot[name="overlay"]::slotted(*) {
position: absolute;
top: 4px;
right: 4px;
}

/* Panel content: styled here because it is a light child of this
component; only the panel chrome comes from ol-tooltip. */
.tip {
font-size: var(--font-size-body-medium);
}

.tip__title {
font-weight: 600;
}

.tip__year,
.tip__byline {
color: var(--neutral-300);
}

.tip__byline {
font-size: var(--font-size-label-medium);
}
`;

constructor() {
super();
this.src = '';
this.bookTitle = '';
this.authors = '';
this.year = '';
this.href = '';
this.size = 'medium';
this.labels = {};
}

t(key, vars) {
return translate(this.labels, DEFAULT_LABELS, key, vars);
}

get _alt() {
return this.authors ? `${this.bookTitle} ${this.t('by', { name: this.authors })}` : this.bookTitle;
}

render() {
const art = this.href
? html`<a class="link" href=${this.href} @click=${this._onClick}>${this._renderArt()}</a>`
: this._renderArt();
return html`
<ol-tooltip placement="top" arrow>${art}${this._renderTip()}</ol-tooltip>
<slot name="overlay"></slot>
`;
}

_renderArt() {
if (this.src) {
return html`<img class="img" src=${this.src} alt=${this._alt} loading="lazy" />`;
}
return html`
<span class="blank" role="img" aria-label=${this._alt}>
<span class="blank__title">${this.bookTitle}</span>
${this.authors && this.size !== 'small' ? html`<span class="blank__author">${this.authors}</span>` : nothing}
</span>
`;
}

_renderTip() {
return html`
<div slot="content" class="tip">
<div>
<span class="tip__title">${this.bookTitle}</span>
${this.year ? html`<span class="tip__year">(${this.year})</span>` : nothing}
</div>
${this.authors ? html`<div class="tip__byline">${this.authors}</div>` : nothing}
</div>
`;
}

_onClick() {
this.dispatchEvent(new CustomEvent('ol-book-cover-click', {
bubbles: true,
composed: true,
detail: { href: this.href },
}));
}
}

customElements.define('ol-book-cover', OlBookCover);
10 changes: 10 additions & 0 deletions openlibrary/components/lit/OlIcon.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,22 @@ import { glyphs } from './icons.generated.js';
* @prop {'sm' | 'md' | 'lg'} size - "sm" (16px) | "md" (20px, default) | "lg" (24px).
* @prop {String} label - Accessible name; exposes the icon as role="img". Without
* it the icon is aria-hidden.
* @prop {Boolean} filled - Paint the glyph's interior in currentColor, for
* on/off states like a saved bookmark or a rated star.
*
* @cssprop [--ol-icon-stroke-width] - Stroke weight, overriding the size default.
*
* @example
* <ol-icon name="search"></ol-icon>
* <ol-icon name="globe" size="lg" label="Language"></ol-icon>
* <ol-icon name="bookmark" filled></ol-icon>
*/
export class OlIcon extends LitElement {
static properties = {
name: { type: String, reflect: true },
size: { type: String, reflect: true },
label: { type: String },
filled: { type: Boolean, reflect: true },
};

// The host box is sized here and again in ol-icon.css. The duplication is
Expand Down Expand Up @@ -78,13 +82,19 @@ export class OlIcon extends LitElement {
:host([size='lg']) svg {
stroke-width: var(--ol-icon-stroke-width, var(--icon-stroke-lg));
}

/* The source's fill="none" is a presentation attribute, so CSS wins. */
:host([filled]) svg {
fill: currentColor;
}
`;

constructor() {
super();
this.name = '';
this.size = 'md';
this.label = '';
this.filled = false;
}

// Decorative by default; named only when the caller supplies a label. On the
Expand Down
1 change: 0 additions & 1 deletion openlibrary/components/lit/OlMenuPopover.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,6 @@ export class OlMenuPopover extends LitElement {
render() {
return html`
<ol-popover
placement="bottom-start"
aria-label=${ifDefined(this.getAttribute('aria-label') || this.label || undefined)}
@ol-popover-open=${this._onPopoverOpen}
>
Expand Down
3 changes: 1 addition & 2 deletions openlibrary/components/lit/OlOptionsPopover.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export class OlOptionsPopover extends FormAssociatedMixin(LitElement) {
width: 16px;
height: 16px;
margin: 2px 0 0;
accent-color: var(--primary-blue);
accent-color: var(--color-primary);
cursor: pointer;
}

Expand Down Expand Up @@ -260,7 +260,6 @@ export class OlOptionsPopover extends FormAssociatedMixin(LitElement) {
render() {
return html`
<ol-popover
placement="bottom-start"
aria-label="${ifDefined(this.getAttribute('aria-label') || this.label || undefined)}"
@ol-popover-open=${this._onPopoverOpen}
@ol-popover-close=${this._onPopoverClose}
Expand Down
10 changes: 6 additions & 4 deletions openlibrary/components/lit/OlPopover.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ function _removeFromOverlayStack(el) {
* @prop {Boolean} open - Whether the popover is currently open
* @prop {String} placement - Preferred placement relative to the trigger.
* Format: "{side}-{align}" where side is "top" or "bottom" and align is
* "start", "center", or "end". Default: "bottom-center"
* "start", "center", or "end". Default: "bottom-start" — a panel is
* usually wider than the control that opens it, and aligning their leading
* edges keeps it under the trigger instead of straddling it.
* @prop {Number} offset - Gap in px between trigger and popover (default: 4)
* @prop {Boolean} autoClose - Whether outside clicks close the popover.
* Escape always closes for accessibility. Default: true
Expand Down Expand Up @@ -300,7 +302,7 @@ export class OlPopover extends LitElement {
constructor() {
super();
this.open = false;
this.placement = 'bottom-center';
this.placement = 'bottom-start';
this.offset = 4;
this.autoClose = true;
this._position = { top: 0, left: 0 };
Expand Down Expand Up @@ -698,9 +700,9 @@ export class OlPopover extends LitElement {
}

_parsePlacement(placement) {
const parts = (placement || 'bottom-center').split('-');
const parts = (placement || 'bottom-start').split('-');
const side = parts[0] === 'top' ? 'top' : 'bottom';
const align = ['start', 'center', 'end'].includes(parts[1]) ? parts[1] : 'center';
const align = ['start', 'center', 'end'].includes(parts[1]) ? parts[1] : 'start';
return [side, align];
}

Expand Down
3 changes: 1 addition & 2 deletions openlibrary/components/lit/OlSelectPopover.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ export class OlSelectPopover extends FormAssociatedMixin(LitElement) {
width: 16px;
height: 16px;
margin: 0;
accent-color: var(--primary-blue);
accent-color: var(--color-primary);
cursor: pointer;
}

Expand Down Expand Up @@ -360,7 +360,6 @@ export class OlSelectPopover extends FormAssociatedMixin(LitElement) {
render() {
return html`
<ol-popover
placement="bottom-start"
aria-label="${ifDefined(this.getAttribute('aria-label') || this.label || undefined)}"
@ol-popover-open=${this._onPopoverOpen}
@ol-popover-close=${this._onPopoverClose}
Expand Down
Loading