ui: Public Collections UI Nitpicks (#2287)

- Removes share link from the dialogue footer
- Removes stickied collection navigation, replaces with improved
viewport-based scaling!
- Adds a max-width for the collection description in the logged in view.
- Moves the markdown editor buttons to below the editor
- Controls are now In-line with how we handle dialogue options
elsewhere, fixes a minor responsive design issue.
- Minor copy changes

---------

Co-authored-by: emma <hi@emma.cafe>
Co-authored-by: sua yoo <sua@webrecorder.org>
This commit is contained in:
Henry Wilkinson 2025-01-08 19:00:21 -05:00 committed by sua yoo
parent d8655d3bc6
commit 56a634e593
No known key found for this signature in database
GPG Key ID: 5AD1B4C02D4F0567
10 changed files with 190 additions and 116 deletions

View File

@ -27,6 +27,14 @@ export class MarkdownEditor extends BtrixElement {
--ink-block-padding: var(--sl-input-spacing-small); --ink-block-padding: var(--sl-input-spacing-small);
} }
.ink-mde-textarea {
flex-grow: 1;
}
.ink-mde {
height: 100%;
}
.ink-mde { .ink-mde {
border: solid var(--sl-input-border-width) var(--sl-input-border-color); border: solid var(--sl-input-border-width) var(--sl-input-border-color);
} }
@ -56,7 +64,8 @@ export class MarkdownEditor extends BtrixElement {
} }
.cm-line:only-child { .cm-line:only-child {
min-height: 8em; height: 100%;
min-height: 20em;
} }
`; `;
@ -111,7 +120,11 @@ export class MarkdownEditor extends BtrixElement {
render() { render() {
const isInvalid = this.maxlength && this.value.length > this.maxlength; const isInvalid = this.maxlength && this.value.length > this.maxlength;
return html` return html`
<fieldset ?data-invalid=${isInvalid} ?data-user-invalid=${isInvalid}> <fieldset
?data-invalid=${isInvalid}
?data-user-invalid=${isInvalid}
class="flex h-full flex-col"
>
${this.label && html`<label class="form-label">${this.label}</label>`} ${this.label && html`<label class="form-label">${this.label}</label>`}
<textarea id="editor-textarea"></textarea> <textarea id="editor-textarea"></textarea>
<div class="helpText flex items-baseline justify-between"> <div class="helpText flex items-baseline justify-between">

View File

@ -25,14 +25,14 @@ export class CollectionStartPageDialog extends BtrixElement {
{ label: string; icon: NonNullable<SlIcon["name"]>; detail: string } { label: string; icon: NonNullable<SlIcon["name"]>; detail: string }
> = { > = {
[HomeView.Pages]: { [HomeView.Pages]: {
label: msg("Default"), label: msg("List of Pages"),
icon: "list-ul", icon: "list-ul",
detail: `${msg("ReplayWeb.Page default view")}`, detail: `${msg("ReplayWeb.Page default view")}`,
}, },
[HomeView.URL]: { [HomeView.URL]: {
label: msg("Page"), label: msg("Start Page"),
icon: "file-earmark", icon: "file-earmark-richtext",
detail: msg("Load a single page URL"), detail: msg("Show a single URL snapshot"),
}, },
}; };
@ -188,7 +188,7 @@ export class CollectionStartPageDialog extends BtrixElement {
<form @submit=${this.onSubmit}> <form @submit=${this.onSubmit}>
<sl-select <sl-select
name="homeView" name="homeView"
label=${msg("Select View")} label=${msg("Select Initial View")}
value=${this.homeView} value=${this.homeView}
hoist hoist
?disabled=${!this.replayLoaded} ?disabled=${!this.replayLoaded}

View File

@ -63,11 +63,11 @@ export class CollectionsGrid extends BtrixElement {
href=${this.navigate.isPublicPage href=${this.navigate.isPublicPage
? `/${RouteNamespace.PublicOrgs}/${this.slug}/collections/${collection.id}` ? `/${RouteNamespace.PublicOrgs}/${this.slug}/collections/${collection.id}`
: `/${RouteNamespace.PrivateOrgs}/${this.slug}/collections/view/${collection.id}`} : `/${RouteNamespace.PrivateOrgs}/${this.slug}/collections/view/${collection.id}`}
class="group block h-full rounded-lg transition-all hover:scale-[102%]" class="group block h-full rounded-lg"
@click=${this.navigate.link} @click=${this.navigate.link}
> >
<div <div
class="relative mb-4 rounded-lg shadow-md shadow-stone-600/10 ring-1 ring-stone-600/10 transition-shadow group-hover:shadow-sm" class="relative mb-4 rounded-lg shadow-md shadow-stone-600/10 ring-1 ring-stone-600/10 transition group-hover:shadow-stone-800/20 group-hover:ring-stone-800/20"
> >
<btrix-collection-thumbnail <btrix-collection-thumbnail
src=${ifDefined( src=${ifDefined(

View File

@ -252,11 +252,7 @@ export class ShareCollection extends BtrixElement {
<sl-tab-panel name=${Tab.Link}> <sl-tab-panel name=${Tab.Link}>
<div class="px-4 pb-4"> <div class="px-4 pb-4">
${when( ${when(showSettings && this.collection, this.renderSettings)}
showSettings && this.collection,
this.renderSettings,
this.renderShareLink,
)}
</div> </div>
</sl-tab-panel> </sl-tab-panel>
@ -266,7 +262,6 @@ export class ShareCollection extends BtrixElement {
</sl-tab-group> </sl-tab-group>
<div slot="footer"> <div slot="footer">
${when(showSettings, this.renderShareLink)}
<sl-button size="small" @click=${() => (this.showDialog = false)}> <sl-button size="small" @click=${() => (this.showDialog = false)}>
${msg("Done")} ${msg("Done")}
</sl-button> </sl-button>
@ -287,6 +282,10 @@ export class ShareCollection extends BtrixElement {
); );
}} }}
></btrix-select-collection-access> ></btrix-select-collection-access>
${when(
this.collection?.access != CollectionAccess.Private,
this.renderShareLink,
)}
${when( ${when(
this.org && this.org &&
!this.org.enablePublicProfile && !this.org.enablePublicProfile &&
@ -389,7 +388,7 @@ export class ShareCollection extends BtrixElement {
> >
${isSelected ${isSelected
? html`<sl-icon ? html`<sl-icon
class="size-10 text-white drop-shadow-md" class="size-10 stroke-black/50 text-white drop-shadow-md [paint-order:stroke]"
name="check-lg" name="check-lg"
></sl-icon>` ></sl-icon>`
: nothing} : nothing}
@ -410,7 +409,7 @@ export class ShareCollection extends BtrixElement {
private readonly renderShareLink = () => { private readonly renderShareLink = () => {
return html` return html`
<div class="text-left"> <div class="mt-4 text-left">
<div class="form-label">${msg("Link to Share")}</div> <div class="form-label">${msg("Link to Share")}</div>
<btrix-copy-field <btrix-copy-field
class="mb-3" class="mb-3"

View File

@ -15,12 +15,12 @@ export const iconFor = cached(
case "severe": case "severe":
return html`<sl-icon return html`<sl-icon
name="exclamation-triangle-fill" name="exclamation-triangle-fill"
class=${clsx("text-red-600", baseClasses, classList)} class=${clsx("text-red-500", baseClasses, classList)}
></sl-icon>`; ></sl-icon>`;
case "moderate": case "moderate":
return html`<sl-icon return html`<sl-icon
name="dash-square-fill" name="dash-square-fill"
class=${clsx("text-yellow-600", baseClasses, classList)} class=${clsx("text-yellow-500", baseClasses, classList)}
></sl-icon>`; ></sl-icon>`;
case "good": case "good":
return html`<sl-icon return html`<sl-icon
@ -37,7 +37,7 @@ export const iconFor = cached(
case "rejected": case "rejected":
return html`<sl-icon return html`<sl-icon
name="hand-thumbs-down-fill" name="hand-thumbs-down-fill"
class=${clsx("text-red-600", baseClasses, classList)} class=${clsx("text-red-500", baseClasses, classList)}
></sl-icon>`; ></sl-icon>`;
case "commentOnly": case "commentOnly":
// Comment icons are rendered separately // Comment icons are rendered separately

View File

@ -29,9 +29,9 @@ export const textColorFromSeverity = cached((severity: Severity) => {
case "good": case "good":
return tw`text-green-600`; return tw`text-green-600`;
case "moderate": case "moderate":
return tw`text-yellow-600`; return tw`text-yellow-500`;
case "severe": case "severe":
return tw`text-red-600`; return tw`text-red-500`;
default: default:
return ""; return "";
} }

View File

@ -39,7 +39,7 @@ export function page(
></btrix-document-title> ></btrix-document-title>
<div <div
class="mx-auto box-border flex min-h-full w-full max-w-screen-desktop flex-1 flex-col gap-3 p-3 lg:px-10" class="mx-auto box-border flex min-h-full w-full max-w-screen-desktop flex-1 flex-col gap-3 p-3 lg:px-10 lg:pb-10"
> >
${header.breadcrumbs ? html` ${pageNav(header.breadcrumbs)} ` : nothing} ${header.breadcrumbs ? html` ${pageNav(header.breadcrumbs)} ` : nothing}
${pageHeader(header)} ${pageHeader(header)}

View File

@ -206,6 +206,7 @@ export class Collection extends BtrixElement {
`; `;
} }
// TODO Consolidate with collection-detail.ts
private renderAbout(collection: PublicCollection) { private renderAbout(collection: PublicCollection) {
const dateRange = () => { const dateRange = () => {
if (!collection.dateEarliest || !collection.dateLatest) { if (!collection.dateEarliest || !collection.dateLatest) {
@ -243,13 +244,13 @@ export class Collection extends BtrixElement {
return html` return html`
<div class="flex flex-1 flex-col gap-10 lg:flex-row"> <div class="flex flex-1 flex-col gap-10 lg:flex-row">
<section <section
class="flex-1 py-3 leading-relaxed lg:rounded-lg lg:border lg:p-6" class="w-full max-w-4xl py-3 leading-relaxed lg:rounded-lg lg:border lg:p-6"
> >
<btrix-markdown-viewer <btrix-markdown-viewer
value=${collection.description} value=${collection.description}
></btrix-markdown-viewer> ></btrix-markdown-viewer>
</section> </section>
<section class="min-w-60 lg:-mt-8"> <section class="flex-1 lg:-mt-8">
<btrix-section-heading> <btrix-section-heading>
<h3>${msg("Metadata")}</h3> <h3>${msg("Metadata")}</h3>
</btrix-section-heading> </btrix-section-heading>

View File

@ -1,4 +1,5 @@
import { localized, msg, str } from "@lit/localize"; import { localized, msg, str } from "@lit/localize";
import clsx from "clsx";
import { html, nothing, type PropertyValues, type TemplateResult } from "lit"; import { html, nothing, type PropertyValues, type TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators.js"; import { customElement, property, query, state } from "lit/decorators.js";
import { choose } from "lit/directives/choose.js"; import { choose } from "lit/directives/choose.js";
@ -24,6 +25,7 @@ import type { ArchivedItem, Crawl, Upload } from "@/types/crawler";
import type { CrawlState } from "@/types/crawlState"; import type { CrawlState } from "@/types/crawlState";
import { pluralOf } from "@/utils/pluralize"; import { pluralOf } from "@/utils/pluralize";
import { formatRwpTimestamp } from "@/utils/replay"; import { formatRwpTimestamp } from "@/utils/replay";
import { tw } from "@/utils/tailwind";
const ABORT_REASON_THROTTLE = "throttled"; const ABORT_REASON_THROTTLE = "throttled";
const INITIAL_ITEMS_PAGE_SIZE = 20; const INITIAL_ITEMS_PAGE_SIZE = 20;
@ -115,7 +117,7 @@ export class CollectionDetail extends BtrixElement {
if (this.descriptionEditor) { if (this.descriptionEditor) {
// FIXME Focus on editor ready instead of timeout // FIXME Focus on editor ready instead of timeout
window.setTimeout(() => { window.setTimeout(() => {
void this.descriptionEditor?.focus(); this.descriptionEditor && void this.descriptionEditor.focus();
}, 200); }, 200);
} }
} }
@ -148,9 +150,7 @@ export class CollectionDetail extends BtrixElement {
<div class="mt-3 rounded-lg border px-4 py-2"> <div class="mt-3 rounded-lg border px-4 py-2">
${this.renderInfoBar()} ${this.renderInfoBar()}
</div> </div>
<div <div class="flex items-center justify-between py-3">
class="sticky top-0 z-50 -mx-3 mb-3 flex items-center justify-between bg-white px-3 pt-3 shadow-lg shadow-white"
>
${this.renderTabs()} ${this.renderTabs()}
${when(this.isCrawler, () => ${when(this.isCrawler, () =>
choose(this.collectionTab, [ choose(this.collectionTab, [
@ -174,40 +174,6 @@ export class CollectionDetail extends BtrixElement {
</sl-tooltip> </sl-tooltip>
`, `,
], ],
[
Tab.About,
() =>
this.isEditingDescription
? html`
<div>
<sl-button
variant="primary"
size="small"
@click=${() => void this.saveDescription()}
?disabled=${!this.collection}
>
<sl-icon name="check-lg" slot="prefix"></sl-icon>
${msg("Save")}
</sl-button>
<sl-button
size="small"
@click=${() => (this.isEditingDescription = false)}
>
${msg("Cancel")}
</sl-button>
</div>
`
: html`
<sl-button
size="small"
@click=${() => (this.isEditingDescription = true)}
?disabled=${!this.collection}
>
<sl-icon name="pencil" slot="prefix"></sl-icon>
${msg("Edit")}
</sl-button>
`,
],
[ [
Tab.Items, Tab.Items,
() => html` () => html`
@ -230,7 +196,7 @@ export class CollectionDetail extends BtrixElement {
Tab.Items, Tab.Items,
() => guard([this.archivedItems], this.renderArchivedItems), () => guard([this.archivedItems], this.renderArchivedItems),
], ],
[Tab.About, () => this.renderDescription()], [Tab.About, () => this.renderAbout()],
])} ])}
<btrix-dialog <btrix-dialog
@ -544,22 +510,70 @@ export class CollectionDetail extends BtrixElement {
`; `;
} }
private renderDescription() { // TODO Consolidate with collection.ts
private renderAbout() {
const dateRange = (collection: Collection) => {
if (!collection.dateEarliest || !collection.dateLatest) {
return msg("n/a");
}
const format: Intl.DateTimeFormatOptions = {
month: "long",
year: "numeric",
};
const dateEarliest = this.localize.date(collection.dateEarliest, format);
const dateLatest = this.localize.date(collection.dateLatest, format);
if (dateEarliest === dateLatest) return dateLatest;
return msg(str`${dateEarliest} to ${dateLatest}`, {
desc: "Date range formatted to show full month name and year",
});
};
const skeleton = html`<sl-skeleton class="w-24"></sl-skeleton>`;
const metadata = html`
<btrix-desc-list>
<btrix-desc-list-item label=${msg("Collection Period")}>
<span class="font-sans"
>${this.collection ? dateRange(this.collection) : skeleton}</span
>
</btrix-desc-list-item>
</btrix-desc-list>
`;
return html` return html`
<section> <div class="flex flex-1 flex-col gap-10 lg:flex-row">
<section class="flex w-full max-w-4xl flex-col leading-relaxed">
<header class="mb-3 flex min-h-8 items-end justify-between">
<h2 class="text-base font-semibold leading-none">
${msg("Description")}
</h2>
${when(
this.collection?.description && !this.isEditingDescription,
() => html`
<sl-button
size="small"
@click=${() => (this.isEditingDescription = true)}
>
<sl-icon name="pencil" slot="prefix"></sl-icon>
${msg("Edit Description")}
</sl-button>
`,
)}
</header>
${when( ${when(
this.collection, this.collection,
(collection) => (collection) =>
this.isEditingDescription this.isEditingDescription
? html` ? this.renderDescriptionForm()
<btrix-markdown-editor
initialValue=${collection.description || ""}
placeholder=${msg("Tell viewers about this collection")}
maxlength=${4000}
></btrix-markdown-editor>
`
: html` : html`
<div class="rounded-lg border p-4 leading-relaxed"> <div
class=${clsx(
tw`flex-1 rounded-lg border p-3 lg:p-6`,
!collection.description &&
tw`flex flex-col items-center justify-center`,
)}
>
${collection.description ${collection.description
? html` ? html`
<btrix-markdown-viewer <btrix-markdown-viewer
@ -567,15 +581,62 @@ export class CollectionDetail extends BtrixElement {
></btrix-markdown-viewer> ></btrix-markdown-viewer>
` `
: html` : html`
<p class="py-10 text-center text-neutral-500"> <div class="text-center text-neutral-500">
<p class="mb-3">
${msg("No description provided.")} ${msg("No description provided.")}
</p> </p>
<sl-button
size="small"
@click=${() =>
(this.isEditingDescription = true)}
?disabled=${!this.collection}
>
<sl-icon name="pencil" slot="prefix"></sl-icon>
${msg("Add Description")}
</sl-button>
</div>
`} `}
</div> </div>
`, `,
this.renderSpinner, this.renderSpinner,
)} )}
</section> </section>
<section class="flex-1">
<btrix-section-heading>
<h2>${msg("Metadata")}</h2>
</btrix-section-heading>
<div class="mt-5">${metadata}</div>
</section>
</div>
`;
}
private renderDescriptionForm() {
if (!this.collection) return;
return html`
<btrix-markdown-editor
class="flex-1"
initialValue=${this.collection.description || ""}
placeholder=${msg("Tell viewers about this collection")}
maxlength=${4000}
></btrix-markdown-editor>
<div class="flex-column mt-4 flex justify-between border-t pt-4">
<sl-button
size="small"
@click=${() => (this.isEditingDescription = false)}
>
${msg("Cancel")}
</sl-button>
<sl-button
variant="primary"
size="small"
@click=${() => void this.saveDescription()}
?disabled=${!this.collection}
>
${msg("Update Description")}
</sl-button>
</div>
`; `;
} }
@ -708,9 +769,9 @@ export class CollectionDetail extends BtrixElement {
const headers = this.authState?.headers; const headers = this.authState?.headers;
const config = JSON.stringify({ headers }); const config = JSON.stringify({ headers });
return html`<section> return html` <section class="overflow-hidden rounded-lg border">
<div class="aspect-4/3 overflow-hidden rounded-lg border">
<replay-web-page <replay-web-page
class="h-[calc(100vh-6.5rem)]"
source=${replaySource} source=${replaySource}
config="${config}" config="${config}"
coll=${this.collectionId} coll=${this.collectionId}
@ -727,13 +788,12 @@ export class CollectionDetail extends BtrixElement {
} }
}} }}
></replay-web-page> ></replay-web-page>
</div>
</section>`; </section>`;
}; };
private readonly renderSpinner = () => html` private readonly renderSpinner = () => html`
<div <div
class="flex items-center justify-center rounded-lg border py-24 text-3xl" class="flex min-h-full items-center justify-center rounded-lg border py-24 text-3xl"
> >
<sl-spinner></sl-spinner> <sl-spinner></sl-spinner>
</div> </div>

View File

@ -602,6 +602,7 @@ export class Org extends BtrixElement {
if (params.collectionId) { if (params.collectionId) {
return html`<btrix-collection-detail return html`<btrix-collection-detail
class="flex min-h-screen flex-1 flex-col pb-7"
collectionId=${params.collectionId} collectionId=${params.collectionId}
collectionTab=${ifDefined( collectionTab=${ifDefined(
params.collectionTab as CollectionTab | undefined, params.collectionTab as CollectionTab | undefined,