From a1f12d369b71b596bdbe2e290236c1af30dfe816 Mon Sep 17 00:00:00 2001
From: emma <hi@emma.cafe>
Date: Thu, 13 Feb 2025 11:18:07 -0500
Subject: [PATCH 1/9] wip

---
 frontend/src/pages/org/collections-list.ts | 101 ++++++++++++++++++---
 1 file changed, 88 insertions(+), 13 deletions(-)

diff --git a/frontend/src/pages/org/collections-list.ts b/frontend/src/pages/org/collections-list.ts
index e8a788dc42..fc8526cf67 100644
--- a/frontend/src/pages/org/collections-list.ts
+++ b/frontend/src/pages/org/collections-list.ts
@@ -1,5 +1,10 @@
 import { localized, msg } from "@lit/localize";
-import type { SlInput, SlMenuItem } from "@shoelace-style/shoelace";
+import type {
+  SlChangeEvent,
+  SlInput,
+  SlMenuItem,
+  SlRadioGroup,
+} from "@shoelace-style/shoelace";
 import Fuse from "fuse.js";
 import { html, nothing, type PropertyValues } from "lit";
 import { customElement, property, query, state } from "lit/decorators.js";
@@ -79,6 +84,11 @@ const sortableFields: Record<
 };
 const MIN_SEARCH_LENGTH = 2;
 
+enum ListView {
+  List = "list",
+  Grid = "grid",
+}
+
 @customElement("btrix-collections-list")
 @localized()
 export class CollectionsList extends BtrixElement {
@@ -97,6 +107,9 @@ export class CollectionsList extends BtrixElement {
     direction: sortableFields["modified"].defaultDirection!,
   };
 
+  @state()
+  private listView = ListView.List;
+
   @state()
   private filterBy: Partial<Record<keyof Collection, unknown>> = {};
 
@@ -178,8 +191,13 @@ export class CollectionsList extends BtrixElement {
               >
                 ${this.renderControls()}
               </div>
-              <div class="overflow-auto px-2 pb-1">
-                ${guard([this.collections], this.renderList)}
+              <div class="-mx-3 overflow-auto px-3 pb-1">
+                ${guard(
+                  [this.collections, this.listView],
+                  this.listView === ListView.List
+                    ? this.renderList
+                    : this.renderGrid,
+                )}
               </div>
             `
           : this.renderLoading(),
@@ -291,17 +309,45 @@ export class CollectionsList extends BtrixElement {
                 `,
               )}
             </sl-select>
-            <sl-icon-button
-              name="arrow-down-up"
-              label=${msg("Reverse sort")}
-              @click=${() => {
-                this.orderBy = {
-                  ...this.orderBy,
-                  direction: -1 * this.orderBy.direction,
-                };
-              }}
-            ></sl-icon-button>
+            <sl-tooltip content=${msg("Reverse sort")}>
+              <sl-icon-button
+                name="arrow-down-up"
+                label=${msg("Reverse sort")}
+                @click=${() => {
+                  this.orderBy = {
+                    ...this.orderBy,
+                    direction: -1 * this.orderBy.direction,
+                  };
+                }}
+              ></sl-icon-button>
+            </sl-tooltip>
           </div>
+          <label for="viewStyle" class="mx-2 whitespace-nowrap text-neutral-500"
+            >${msg("View:")}</label
+          >
+          <sl-radio-group
+            id="viewStyle"
+            value=${this.listView}
+            size="small"
+            @sl-change=${(e: SlChangeEvent) => {
+              this.listView = (e.target as SlRadioGroup).value as ListView;
+            }}
+          >
+            <sl-tooltip content=${msg("View as List")}>
+              <sl-radio-button pill value=${ListView.List}>
+                <sl-icon
+                  name="view-list"
+                  label=${msg("List")}
+                ></sl-icon> </sl-radio-button
+            ></sl-tooltip>
+            <sl-tooltip content=${msg("View as Grid")}>
+              <sl-radio-button pill value=${ListView.Grid}>
+                <sl-icon
+                  name="grid"
+                  label=${msg("Grid")}
+                ></sl-icon> </sl-radio-button
+            ></sl-tooltip>
+          </sl-radio-group>
         </div>
       </div>
     `;
@@ -386,6 +432,35 @@ export class CollectionsList extends BtrixElement {
     `;
   }
 
+  private readonly renderGrid = () => {
+    return html`<btrix-collections-grid
+      slug=${this.orgSlugState || ""}
+      .collections=${this.collections?.items}
+    >
+      ${this.collections &&
+      this.collections.total > this.collections.items.length
+        ? html`
+            <btrix-pagination
+              page=${this.collections.page}
+              totalCount=${this.collections.total}
+              size=${this.collections.pageSize}
+              @page-change=${async (e: PageChangeEvent) => {
+                await this.fetchCollections({
+                  page: e.detail.page,
+                });
+
+                // Scroll to top of list
+                // TODO once deep-linking is implemented, scroll to top of pushstate
+                this.scrollIntoView({ behavior: "smooth" });
+              }}
+              slot="pagination"
+            >
+            </btrix-pagination>
+          `
+        : nothing}
+    </btrix-collections-grid>`;
+  };
+
   private readonly renderList = () => {
     if (this.collections?.items.length) {
       return html`

From c7eb2e2e50185a973250e320900c459fc1519e6d Mon Sep 17 00:00:00 2001
From: emma <hi@emma.cafe>
Date: Thu, 13 Feb 2025 11:30:29 -0500
Subject: [PATCH 2/9] handle refreshing collections a little better

---
 frontend/src/pages/org/collections-list.ts | 15 +++++++++++++--
 1 file changed, 13 insertions(+), 2 deletions(-)

diff --git a/frontend/src/pages/org/collections-list.ts b/frontend/src/pages/org/collections-list.ts
index fc8526cf67..d570fcd410 100644
--- a/frontend/src/pages/org/collections-list.ts
+++ b/frontend/src/pages/org/collections-list.ts
@@ -128,6 +128,9 @@ export class CollectionsList extends BtrixElement {
   @state()
   private selectedCollection?: Collection;
 
+  @state()
+  collectionRefreshing: string | null = null;
+
   @state()
   private fetchErrorStatusCode?: number;
 
@@ -193,7 +196,7 @@ export class CollectionsList extends BtrixElement {
               </div>
               <div class="-mx-3 overflow-auto px-3 pb-1">
                 ${guard(
-                  [this.collections, this.listView],
+                  [this.collections, this.listView, this.collectionRefreshing],
                   this.listView === ListView.List
                     ? this.renderList
                     : this.renderGrid,
@@ -436,6 +439,14 @@ export class CollectionsList extends BtrixElement {
     return html`<btrix-collections-grid
       slug=${this.orgSlugState || ""}
       .collections=${this.collections?.items}
+      .collectionRefreshing=${this.collectionRefreshing}
+      showVisibility
+      class="mt-8 block"
+      @btrix-collection-saved=${async ({ detail }: CollectionSavedEvent) => {
+        this.collectionRefreshing = detail.id;
+        await this.fetchCollections();
+        this.collectionRefreshing = null;
+      }}
     >
       ${this.collections &&
       this.collections.total > this.collections.items.length
@@ -468,7 +479,7 @@ export class CollectionsList extends BtrixElement {
           class="[--btrix-column-gap:var(--sl-spacing-small)]"
           style="grid-template-columns: min-content [clickable-start] 45em repeat(4, 1fr) [clickable-end] min-content"
         >
-          <btrix-table-head class="mb-2 whitespace-nowrap">
+          <btrix-table-head class="mb-2 mt-1 whitespace-nowrap">
             <btrix-table-header-cell>
               <span class="sr-only">${msg("Collection Access")}</span>
             </btrix-table-header-cell>

From 3d0fed71fab19597507133a692aa2e3c3729a65b Mon Sep 17 00:00:00 2001
From: emma <hi@emma.cafe>
Date: Thu, 13 Feb 2025 11:35:42 -0500
Subject: [PATCH 3/9] make access icons more consistent with list

---
 .../features/collections/collections-grid.ts  | 74 ++++++++++++++++---
 1 file changed, 64 insertions(+), 10 deletions(-)

diff --git a/frontend/src/features/collections/collections-grid.ts b/frontend/src/features/collections/collections-grid.ts
index 989ebce01b..017384758e 100644
--- a/frontend/src/features/collections/collections-grid.ts
+++ b/frontend/src/features/collections/collections-grid.ts
@@ -7,6 +7,7 @@ import {
   queryAssignedNodes,
   state,
 } from "lit/decorators.js";
+import { choose } from "lit/directives/choose.js";
 import { ifDefined } from "lit/directives/if-defined.js";
 import { when } from "lit/directives/when.js";
 
@@ -16,7 +17,7 @@ import { SelectCollectionAccess } from "./select-collection-access";
 import { BtrixElement } from "@/classes/BtrixElement";
 import { textSeparator } from "@/layouts/separator";
 import { RouteNamespace } from "@/routes";
-import type { PublicCollection } from "@/types/collection";
+import { CollectionAccess, type PublicCollection } from "@/types/collection";
 import { pluralOf } from "@/utils/pluralize";
 import { tw } from "@/utils/tailwind";
 
@@ -107,15 +108,68 @@ export class CollectionsGrid extends BtrixElement {
                 </div>
                 <div class="${showActions ? "mr-9" : ""} min-h-9 leading-tight">
                   ${this.showVisibility
-                    ? html`<sl-icon
-                        class="mr-[5px] align-[-1px] text-sm"
-                        name=${SelectCollectionAccess.Options[collection.access]
-                          .icon}
-                        label=${SelectCollectionAccess.Options[
-                          collection.access
-                        ].label}
-                      ></sl-icon>`
-                    : nothing}
+                    ? choose(collection.access, [
+                        [
+                          CollectionAccess.Private,
+                          () => html`
+                            <sl-tooltip
+                              content=${SelectCollectionAccess.Options[
+                                CollectionAccess.Private
+                              ].label}
+                            >
+                              <sl-icon
+                                class="mr-[5px] inline-block align-[-1px] text-neutral-600"
+                                name=${SelectCollectionAccess.Options[
+                                  CollectionAccess.Private
+                                ].icon}
+                              ></sl-icon>
+                            </sl-tooltip>
+                          `,
+                        ],
+                        [
+                          CollectionAccess.Unlisted,
+                          () => html`
+                            <sl-tooltip
+                              content=${SelectCollectionAccess.Options[
+                                CollectionAccess.Unlisted
+                              ].label}
+                            >
+                              <sl-icon
+                                class="mr-[5px] inline-block align-[-1px] text-neutral-600"
+                                name=${SelectCollectionAccess.Options[
+                                  CollectionAccess.Unlisted
+                                ].icon}
+                              ></sl-icon>
+                            </sl-tooltip>
+                          `,
+                        ],
+                        [
+                          CollectionAccess.Public,
+                          () => html`
+                            <sl-tooltip
+                              content=${SelectCollectionAccess.Options[
+                                CollectionAccess.Public
+                              ].label}
+                            >
+                              <sl-icon
+                                class="mr-[5px] inline-block align-[-1px] text-success-600"
+                                name=${SelectCollectionAccess.Options[
+                                  CollectionAccess.Public
+                                ].icon}
+                              ></sl-icon>
+                            </sl-tooltip>
+                          `,
+                        ],
+                      ])
+                    : // ? html`<sl-icon
+                      //     class="mr-[5px] align-[-1px] text-sm"
+                      //     name=${SelectCollectionAccess.Options[collection.access]
+                      //       .icon}
+                      //     label=${SelectCollectionAccess.Options[
+                      //       collection.access
+                      //     ].label}
+                      //   ></sl-icon>`
+                      nothing}
                   <strong
                     class="text-base font-medium leading-tight text-stone-800 transition-colors group-hover:text-cyan-600"
                   >

From 714bc2b624ed9609113c415655c92cbd3575009e Mon Sep 17 00:00:00 2001
From: emma <hi@emma.cafe>
Date: Thu, 17 Apr 2025 15:06:55 -0400
Subject: [PATCH 4/9] refactor collection editing dialog into separate
 component this makes collection grid simpler: it doesn't include extra
 editing dialog when not needed

Extract collection edit dialog into dedicated component

- Move dialog from collections-grid to collections-grid-with-edit-dialog
- Update dashboard page to use new composite component
---
 .../collections-grid-with-edit-dialog.ts      |  52 ++++++++
 .../features/collections/collections-grid.ts  |  28 +----
 frontend/src/features/collections/index.ts    |   1 +
 frontend/src/pages/org/dashboard.ts           | 116 ++++++++++--------
 4 files changed, 121 insertions(+), 76 deletions(-)
 create mode 100644 frontend/src/features/collections/collections-grid-with-edit-dialog.ts

diff --git a/frontend/src/features/collections/collections-grid-with-edit-dialog.ts b/frontend/src/features/collections/collections-grid-with-edit-dialog.ts
new file mode 100644
index 0000000000..3623aae104
--- /dev/null
+++ b/frontend/src/features/collections/collections-grid-with-edit-dialog.ts
@@ -0,0 +1,52 @@
+import { localized } from "@lit/localize";
+import { html } from "lit";
+import { customElement, property, state } from "lit/decorators.js";
+import { when } from "lit/directives/when.js";
+
+import { BtrixElement } from "@/classes/BtrixElement";
+import { type PublicCollection } from "@/types/collection";
+
+@customElement("btrix-collections-grid-with-edit-dialog")
+@localized()
+export class CollectionsGridWithEditDialog extends BtrixElement {
+  @property({ type: Array })
+  collections?: PublicCollection[];
+
+  @state()
+  collectionBeingEdited: string | null = null;
+
+  @property({ type: String })
+  collectionRefreshing: string | null = null;
+
+  @property({ type: Boolean })
+  showVisibility = false;
+
+  render() {
+    const showActions = !this.navigate.isPublicPage && this.appState.isCrawler;
+    return html`
+      <btrix-collections-grid
+        slug=${this.orgSlugState || ""}
+        .collections=${this.collections}
+        .collectionRefreshing=${this.collectionRefreshing}
+        ?showVisibility=${this.showVisibility}
+        @btrix-edit-collection=${(e: CustomEvent<string>) => {
+          this.collectionBeingEdited = e.detail;
+        }}
+      >
+        <slot name="empty-actions" slot="empty-actions"></slot>
+        <slot name="pagination" slot="pagination"></slot>
+      </btrix-collections-grid>
+      ${when(
+        showActions,
+        () =>
+          html`<btrix-collection-edit-dialog
+            .collectionId=${this.collectionBeingEdited ?? undefined}
+            ?open=${!!this.collectionBeingEdited}
+            @sl-after-hide=${() => {
+              this.collectionBeingEdited = null;
+            }}
+          ></btrix-collection-edit-dialog>`,
+      )}
+    `;
+  }
+}
diff --git a/frontend/src/features/collections/collections-grid.ts b/frontend/src/features/collections/collections-grid.ts
index 017384758e..93dce950d3 100644
--- a/frontend/src/features/collections/collections-grid.ts
+++ b/frontend/src/features/collections/collections-grid.ts
@@ -1,12 +1,7 @@
 import { localized, msg } from "@lit/localize";
 import clsx from "clsx";
 import { html, nothing } from "lit";
-import {
-  customElement,
-  property,
-  queryAssignedNodes,
-  state,
-} from "lit/decorators.js";
+import { customElement, property, queryAssignedNodes } from "lit/decorators.js";
 import { choose } from "lit/directives/choose.js";
 import { ifDefined } from "lit/directives/if-defined.js";
 import { when } from "lit/directives/when.js";
@@ -35,9 +30,6 @@ export class CollectionsGrid extends BtrixElement {
   @property({ type: Array })
   collections?: PublicCollection[];
 
-  @state()
-  collectionBeingEdited: string | null = null;
-
   @property({ type: String })
   collectionRefreshing: string | null = null;
 
@@ -212,18 +204,6 @@ export class CollectionsGrid extends BtrixElement {
         class=${clsx("justify-center flex", this.pagination.length && "mt-10")}
         name="pagination"
       ></slot>
-
-      ${when(
-        showActions,
-        () =>
-          html`<btrix-collection-edit-dialog
-            .collectionId=${this.collectionBeingEdited ?? undefined}
-            ?open=${!!this.collectionBeingEdited}
-            @sl-after-hide=${() => {
-              this.collectionBeingEdited = null;
-            }}
-          ></btrix-collection-edit-dialog>`,
-      )}
     `;
   }
 
@@ -235,7 +215,11 @@ export class CollectionsGrid extends BtrixElement {
             raised
             size="small"
             @click=${() => {
-              this.collectionBeingEdited = collection.id;
+              this.dispatchEvent(
+                new CustomEvent<string>("btrix-edit-collection", {
+                  detail: collection.id,
+                }),
+              );
             }}
           >
             <sl-icon name="pencil"></sl-icon>
diff --git a/frontend/src/features/collections/index.ts b/frontend/src/features/collections/index.ts
index 2ed846c3b7..a4fbc66b0a 100644
--- a/frontend/src/features/collections/index.ts
+++ b/frontend/src/features/collections/index.ts
@@ -1,5 +1,6 @@
 import("./collections-add");
 import("./collections-grid");
+import("./collections-grid-with-edit-dialog");
 import("./collection-items-dialog");
 import("./collection-edit-dialog");
 import("./collection-create-dialog");
diff --git a/frontend/src/pages/org/dashboard.ts b/frontend/src/pages/org/dashboard.ts
index 3354cec1ca..10fc0441d4 100644
--- a/frontend/src/pages/org/dashboard.ts
+++ b/frontend/src/pages/org/dashboard.ts
@@ -376,33 +376,35 @@ export class Dashboard extends BtrixElement {
                     ? msg("Public Collections")
                     : msg("All Collections"),
               })}
-              ${this.collectionsView === CollectionGridView.Public
-                ? html` <span class="text-sm text-neutral-400"
-                    >—
-                    <a
-                      href=${`/${RouteNamespace.PublicOrgs}/${this.orgSlugState}`}
-                      class="inline-flex h-8 items-center text-sm font-medium text-primary-500 transition hover:text-primary-600"
-                      @click=${this.navigate.link}
-                    >
+              ${
+                this.collectionsView === CollectionGridView.Public
+                  ? html` <span class="text-sm text-neutral-400"
+                      >—
+                      <a
+                        href=${`/${RouteNamespace.PublicOrgs}/${this.orgSlugState}`}
+                        class="inline-flex h-8 items-center text-sm font-medium text-primary-500 transition hover:text-primary-600"
+                        @click=${this.navigate.link}
+                      >
+                        ${this.org?.enablePublicProfile
+                          ? msg("Visit public collections gallery")
+                          : msg("Preview public collections gallery")}
+                      </a>
+                      <!-- TODO Refactor clipboard code, get URL in a nicer way? -->
                       ${this.org?.enablePublicProfile
-                        ? msg("Visit public collections gallery")
-                        : msg("Preview public collections gallery")}
-                    </a>
-                    <!-- TODO Refactor clipboard code, get URL in a nicer way? -->
-                    ${this.org?.enablePublicProfile
-                      ? html`<btrix-copy-button
-                          value=${new URL(
-                            `/${RouteNamespace.PublicOrgs}/${this.orgSlugState}`,
-                            window.location.toString(),
-                          ).toString()}
-                          content=${msg(
-                            "Copy Link to Public Collections Gallery",
-                          )}
-                          class="inline-block"
-                        ></btrix-copy-button>`
-                      : nothing}
-                  </span>`
-                : nothing}
+                        ? html`<btrix-copy-button
+                            value=${new URL(
+                              `/${RouteNamespace.PublicOrgs}/${this.orgSlugState}`,
+                              window.location.toString(),
+                            ).toString()}
+                            content=${msg(
+                              "Copy Link to Public Collections Gallery",
+                            )}
+                            class="inline-block"
+                          ></btrix-copy-button>`
+                        : nothing}
+                    </span>`
+                  : nothing
+              }
             </div>
             <div class="flex items-center gap-2">
               ${when(
@@ -446,8 +448,7 @@ export class Dashboard extends BtrixElement {
             </div>
           </header>
           <div class="relative rounded-lg border p-10">
-            <btrix-collections-grid
-              slug=${this.orgSlugState || ""}
+            <btrix-collections-grid-with-edit-dialog
               .collections=${this.collections.value?.items}
               .collectionRefreshing=${this.collectionRefreshing}
               ?showVisibility=${this.collectionsView === CollectionGridView.All}
@@ -463,34 +464,41 @@ export class Dashboard extends BtrixElement {
             >
               ${this.renderNoPublicCollections()}
               <span slot="empty-text"
-                >${this.collectionsView === CollectionGridView.Public
-                  ? msg("No public collections yet.")
-                  : msg("No collections yet.")}</span
+                >${
+                  this.collectionsView === CollectionGridView.Public
+                    ? msg("No public collections yet.")
+                    : msg("No collections yet.")
+                }</span
               >
-              ${this.collections.value &&
-              this.collections.value.total > this.collections.value.items.length
-                ? html`
-                    <btrix-pagination
-                      page=${this.collectionPage}
-                      size=${PAGE_SIZE}
-                      totalCount=${this.collections.value.total}
-                      @page-change=${(e: PageChangeEvent) => {
-                        this.collectionPage = e.detail.page;
-                      }}
-                      slot="pagination"
-                    >
-                    </btrix-pagination>
-                  `
-                : nothing}
+              ${
+                this.collections.value &&
+                this.collections.value.total >
+                  this.collections.value.items.length
+                  ? html`
+                      <btrix-pagination
+                        page=${this.collectionPage}
+                        size=${PAGE_SIZE}
+                        totalCount=${this.collections.value.total}
+                        @page-change=${(e: PageChangeEvent) => {
+                          this.collectionPage = e.detail.page;
+                        }}
+                        slot="pagination"
+                      >
+                      </btrix-pagination>
+                    `
+                  : nothing
+              }
             </btrix-collections-grid>
-            ${this.collections.status === TaskStatus.PENDING &&
-            this.collections.value
-              ? html`<div
-                  class="absolute inset-0 rounded-lg bg-stone-50/75 p-24 text-center text-4xl"
-                >
-                  <sl-spinner></sl-spinner>
-                </div>`
-              : nothing}
+            ${
+              this.collections.status === TaskStatus.PENDING &&
+              this.collections.value
+                ? html`<div
+                    class="absolute inset-0 rounded-lg bg-stone-50/75 p-24 text-center text-4xl"
+                  >
+                    <sl-spinner></sl-spinner>
+                  </div>`
+                : nothing
+            }
           </div>
         </section>
       </main>

From 022141eb7b3a51f0a0fe63e411eed14bc9fbebc6 Mon Sep 17 00:00:00 2001
From: emma <hi@emma.cafe>
Date: Thu, 17 Apr 2025 15:21:22 -0400
Subject: [PATCH 5/9] add getter/setter for page in pagination component

---
 frontend/src/components/ui/pagination.ts | 63 +++++++++++++++---------
 1 file changed, 39 insertions(+), 24 deletions(-)

diff --git a/frontend/src/components/ui/pagination.ts b/frontend/src/components/ui/pagination.ts
index af57c7b34e..59a88256cd 100644
--- a/frontend/src/components/ui/pagination.ts
+++ b/frontend/src/components/ui/pagination.ts
@@ -141,19 +141,30 @@ export class Pagination extends LitElement {
 
   searchParams = new SearchParamsController(this, (params) => {
     const page = parsePage(params.get(this.name));
-    if (this.page !== page) {
+    if (this._page !== page) {
       this.dispatchEvent(
         new CustomEvent<PageChangeDetail>("page-change", {
           detail: { page: page, pages: this.pages },
           composed: true,
         }),
       );
-      this.page = page;
+      this._page = page;
     }
   });
 
   @state()
-  page = 1;
+  private _page = 1;
+
+  @property({ type: Number })
+  set page(page: number) {
+    if (page !== this._page) {
+      this.setPage(page);
+    }
+  }
+
+  get page() {
+    return this._page;
+  }
 
   @property({ type: String })
   name = "page";
@@ -174,7 +185,7 @@ export class Pagination extends LitElement {
   private pages = 0;
 
   connectedCallback() {
-    this.inputValue = `${this.page}`;
+    this.inputValue = `${this._page}`;
     super.connectedCallback();
   }
 
@@ -186,14 +197,14 @@ export class Pagination extends LitElement {
     const parsedPage = parseFloat(
       this.searchParams.searchParams.get(this.name) ?? "1",
     );
-    if (parsedPage != this.page) {
+    if (parsedPage != this._page) {
       const page = parsePage(this.searchParams.searchParams.get(this.name));
       const constrainedPage = Math.max(1, Math.min(this.pages, page));
       this.onPageChange(constrainedPage);
     }
 
-    if (changedProperties.get("page") && this.page) {
-      this.inputValue = `${this.page}`;
+    if (changedProperties.get("page") && this._page) {
+      this.inputValue = `${this._page}`;
     }
   }
 
@@ -208,7 +219,7 @@ export class Pagination extends LitElement {
           <li>
             <button
               class="navButton"
-              ?disabled=${this.page === 1}
+              ?disabled=${this._page === 1}
               @click=${this.onPrev}
             >
               <img class="chevron" src=${chevronLeft} />
@@ -221,7 +232,7 @@ export class Pagination extends LitElement {
           <li>
             <button
               class="navButton"
-              ?disabled=${this.page === this.pages}
+              ?disabled=${this._page === this.pages}
               @click=${this.onNext}
             >
               <span class=${classMap({ srOnly: this.compact })}
@@ -250,7 +261,7 @@ export class Pagination extends LitElement {
           inputmode="numeric"
           size="small"
           value=${this.inputValue}
-          aria-label=${msg(str`Current page, page ${this.page}`)}
+          aria-label=${msg(str`Current page, page ${this._page}`)}
           aria-current="page"
           autocomplete="off"
           min="1"
@@ -302,7 +313,7 @@ export class Pagination extends LitElement {
     const middleEnd = middleVisible * 2 - 1;
     const endsVisible = 2;
     if (this.pages > middleVisible + middleEnd) {
-      const currentPageIdx = pages.indexOf(this.page);
+      const currentPageIdx = pages.indexOf(this._page);
       const firstPages = pages.slice(0, endsVisible);
       const lastPages = pages.slice(-1 * endsVisible);
       let middlePages = pages.slice(endsVisible, middleEnd);
@@ -331,7 +342,7 @@ export class Pagination extends LitElement {
   };
 
   private readonly renderPageButton = (page: number) => {
-    const isCurrent = page === this.page;
+    const isCurrent = page === this._page;
     return html`<li aria-current=${ifDefined(isCurrent ? "page" : undefined)}>
       <btrix-navigation-button
         icon
@@ -346,23 +357,16 @@ export class Pagination extends LitElement {
   };
 
   private onPrev() {
-    this.onPageChange(this.page > 1 ? this.page - 1 : 1);
+    this.onPageChange(this._page > 1 ? this._page - 1 : 1);
   }
 
   private onNext() {
-    this.onPageChange(this.page < this.pages ? this.page + 1 : this.pages);
+    this.onPageChange(this._page < this.pages ? this._page + 1 : this.pages);
   }
 
   private onPageChange(page: number) {
-    if (this.page !== page) {
-      this.searchParams.set((params) => {
-        if (page === 1) {
-          params.delete(this.name);
-        } else {
-          params.set(this.name, page.toString());
-        }
-        return params;
-      });
+    if (this._page !== page) {
+      this.setPage(page);
       this.dispatchEvent(
         new CustomEvent<PageChangeDetail>("page-change", {
           detail: { page: page, pages: this.pages },
@@ -370,7 +374,18 @@ export class Pagination extends LitElement {
         }),
       );
     }
-    this.page = page;
+    this._page = page;
+  }
+
+  private setPage(page: number) {
+    this.searchParams.set((params) => {
+      if (page === 1) {
+        params.delete(this.name);
+      } else {
+        params.set(this.name, page.toString());
+      }
+      return params;
+    });
   }
 
   private calculatePages() {

From 835dd0a138c94ce8478daf4ef82ebd0693253a77 Mon Sep 17 00:00:00 2001
From: emma <hi@emma.cafe>
Date: Thu, 17 Apr 2025 15:22:12 -0400
Subject: [PATCH 6/9] key collection thumbnails to collection ids to prevent
 flash of previous thumbnail when switching pages

---
 .../features/collections/collections-grid.ts  | 27 +++++++++++++------
 1 file changed, 19 insertions(+), 8 deletions(-)

diff --git a/frontend/src/features/collections/collections-grid.ts b/frontend/src/features/collections/collections-grid.ts
index 93dce950d3..be55e54f02 100644
--- a/frontend/src/features/collections/collections-grid.ts
+++ b/frontend/src/features/collections/collections-grid.ts
@@ -4,6 +4,7 @@ import { html, nothing } from "lit";
 import { customElement, property, queryAssignedNodes } from "lit/decorators.js";
 import { choose } from "lit/directives/choose.js";
 import { ifDefined } from "lit/directives/if-defined.js";
+import { keyed } from "lit/directives/keyed.js";
 import { when } from "lit/directives/when.js";
 
 import { CollectionThumbnail } from "./collection-thumbnail";
@@ -88,14 +89,24 @@ export class CollectionsGrid extends BtrixElement {
                 <div
                   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
-                    src=${ifDefined(
-                      Object.entries(CollectionThumbnail.Variants).find(
-                        ([name]) => name === collection.defaultThumbnailName,
-                      )?.[1].path || collection.thumbnail?.path,
-                    )}
-                    collectionName=${collection.name}
-                  ></btrix-collection-thumbnail>
+                  ${
+                    // When swapping images, the previous image is retained until the new one is loaded,
+                    // which leads to the wrong image briefly being displayed when switching pages.
+                    // This removes and replaces the image instead, which prevents this at the cost of the
+                    // occasional flash of white while loading, but overall this feels more responsive.
+                    keyed(
+                      collection.id,
+                      html` <btrix-collection-thumbnail
+                        src=${ifDefined(
+                          Object.entries(CollectionThumbnail.Variants).find(
+                            ([name]) =>
+                              name === collection.defaultThumbnailName,
+                          )?.[1].path || collection.thumbnail?.path,
+                        )}
+                        collectionName=${collection.name}
+                      ></btrix-collection-thumbnail>`,
+                    )
+                  }
                   ${this.renderDateBadge(collection)}
                 </div>
                 <div class="${showActions ? "mr-9" : ""} min-h-9 leading-tight">

From d7279789b161801a454961bae32f3183f27ff95f Mon Sep 17 00:00:00 2001
From: emma <hi@emma.cafe>
Date: Thu, 17 Apr 2025 15:39:44 -0400
Subject: [PATCH 7/9] add full action menu to collection list grid items

---
 .../src/components/ui/overflow-dropdown.ts    |  5 ++-
 .../features/collections/collections-grid.ts  | 41 +++++++++++--------
 frontend/src/pages/org/collections-list.ts    | 12 +++++-
 3 files changed, 37 insertions(+), 21 deletions(-)

diff --git a/frontend/src/components/ui/overflow-dropdown.ts b/frontend/src/components/ui/overflow-dropdown.ts
index 1bc0ea8f78..fa9446924a 100644
--- a/frontend/src/components/ui/overflow-dropdown.ts
+++ b/frontend/src/components/ui/overflow-dropdown.ts
@@ -31,6 +31,9 @@ export class OverflowDropdown extends TailwindElement {
   @property({ type: Boolean })
   raised = false;
 
+  @property({ type: String })
+  size?: "x-small" | "small" | "medium";
+
   @state()
   private hasMenuItems?: boolean;
 
@@ -47,7 +50,7 @@ export class OverflowDropdown extends TailwindElement {
         hoist
         distance=${ifDefined(this.raised ? "4" : undefined)}
       >
-        <btrix-button slot="trigger" ?raised=${this.raised}>
+        <btrix-button slot="trigger" ?raised=${this.raised} size=${this.size}>
           <sl-icon
             label=${msg("Actions")}
             name="three-dots-vertical"
diff --git a/frontend/src/features/collections/collections-grid.ts b/frontend/src/features/collections/collections-grid.ts
index be55e54f02..f8a8ec5273 100644
--- a/frontend/src/features/collections/collections-grid.ts
+++ b/frontend/src/features/collections/collections-grid.ts
@@ -1,6 +1,6 @@
 import { localized, msg } from "@lit/localize";
 import clsx from "clsx";
-import { html, nothing } from "lit";
+import { html, nothing, type TemplateResult } from "lit";
 import { customElement, property, queryAssignedNodes } from "lit/decorators.js";
 import { choose } from "lit/directives/choose.js";
 import { ifDefined } from "lit/directives/if-defined.js";
@@ -37,6 +37,9 @@ export class CollectionsGrid extends BtrixElement {
   @property({ type: Boolean })
   showVisibility = false;
 
+  @property()
+  renderActions?: (collection: PublicCollection) => TemplateResult;
+
   @queryAssignedNodes({ slot: "pagination" })
   pagination!: Node[];
 
@@ -196,7 +199,7 @@ export class CollectionsGrid extends BtrixElement {
                   `}
                 </div>
               </a>
-              ${when(showActions, () => this.renderActions(collection))}
+              ${when(showActions, () => this._renderActions(collection))}
               ${when(
                 this.collectionRefreshing === collection.id,
                 () =>
@@ -218,24 +221,26 @@ export class CollectionsGrid extends BtrixElement {
     `;
   }
 
-  private readonly renderActions = (collection: PublicCollection) => html`
+  private readonly _renderActions = (collection: PublicCollection) => html`
     <div class="pointer-events-none absolute left-0 right-0 top-0 aspect-video">
       <div class="pointer-events-auto absolute bottom-2 right-2">
-        <sl-tooltip content=${msg("Edit Collection Settings")}>
-          <btrix-button
-            raised
-            size="small"
-            @click=${() => {
-              this.dispatchEvent(
-                new CustomEvent<string>("btrix-edit-collection", {
-                  detail: collection.id,
-                }),
-              );
-            }}
-          >
-            <sl-icon name="pencil"></sl-icon>
-          </btrix-button>
-        </sl-tooltip>
+        ${this.renderActions
+          ? this.renderActions(collection)
+          : html`<sl-tooltip content=${msg("Edit Collection Settings")}>
+              <btrix-button
+                raised
+                size="small"
+                @click=${() => {
+                  this.dispatchEvent(
+                    new CustomEvent<string>("btrix-edit-collection", {
+                      detail: collection.id,
+                    }),
+                  );
+                }}
+              >
+                <sl-icon name="pencil"></sl-icon>
+              </btrix-button>
+            </sl-tooltip>`}
       </div>
     </div>
   `;
diff --git a/frontend/src/pages/org/collections-list.ts b/frontend/src/pages/org/collections-list.ts
index d570fcd410..c2762de76a 100644
--- a/frontend/src/pages/org/collections-list.ts
+++ b/frontend/src/pages/org/collections-list.ts
@@ -440,6 +440,8 @@ export class CollectionsList extends BtrixElement {
       slug=${this.orgSlugState || ""}
       .collections=${this.collections?.items}
       .collectionRefreshing=${this.collectionRefreshing}
+      .renderActions=${(col: Collection) =>
+        this.renderActions(col, { renderOnGridItem: true })}
       showVisibility
       class="mt-8 block"
       @btrix-collection-saved=${async ({ detail }: CollectionSavedEvent) => {
@@ -661,11 +663,17 @@ export class CollectionsList extends BtrixElement {
     </btrix-table-row>
   `;
 
-  private readonly renderActions = (col: Collection) => {
+  private readonly renderActions = (
+    col: Collection,
+    { renderOnGridItem } = { renderOnGridItem: false },
+  ) => {
     const authToken = this.authState?.headers.Authorization.split(" ")[1];
 
     return html`
-      <btrix-overflow-dropdown>
+      <btrix-overflow-dropdown
+        ?raised=${renderOnGridItem}
+        size=${renderOnGridItem ? "small" : "medium"}
+      >
         <sl-menu>
           <sl-menu-item @click=${() => void this.manageCollection(col, "edit")}>
             <sl-icon name="gear" slot="prefix"></sl-icon>

From 31ef5a0832349fb50a56dc0f72573a9599aad0b8 Mon Sep 17 00:00:00 2001
From: emma <hi@emma.cafe>
Date: Thu, 17 Apr 2025 15:54:28 -0400
Subject: [PATCH 8/9] simplify collection visibility icon rendering

---
 .../features/collections/collections-grid.ts  | 81 +++++--------------
 1 file changed, 18 insertions(+), 63 deletions(-)

diff --git a/frontend/src/features/collections/collections-grid.ts b/frontend/src/features/collections/collections-grid.ts
index f8a8ec5273..0add88e1d6 100644
--- a/frontend/src/features/collections/collections-grid.ts
+++ b/frontend/src/features/collections/collections-grid.ts
@@ -2,7 +2,6 @@ import { localized, msg } from "@lit/localize";
 import clsx from "clsx";
 import { html, nothing, type TemplateResult } from "lit";
 import { customElement, property, queryAssignedNodes } from "lit/decorators.js";
-import { choose } from "lit/directives/choose.js";
 import { ifDefined } from "lit/directives/if-defined.js";
 import { keyed } from "lit/directives/keyed.js";
 import { when } from "lit/directives/when.js";
@@ -114,68 +113,24 @@ export class CollectionsGrid extends BtrixElement {
                 </div>
                 <div class="${showActions ? "mr-9" : ""} min-h-9 leading-tight">
                   ${this.showVisibility
-                    ? choose(collection.access, [
-                        [
-                          CollectionAccess.Private,
-                          () => html`
-                            <sl-tooltip
-                              content=${SelectCollectionAccess.Options[
-                                CollectionAccess.Private
-                              ].label}
-                            >
-                              <sl-icon
-                                class="mr-[5px] inline-block align-[-1px] text-neutral-600"
-                                name=${SelectCollectionAccess.Options[
-                                  CollectionAccess.Private
-                                ].icon}
-                              ></sl-icon>
-                            </sl-tooltip>
-                          `,
-                        ],
-                        [
-                          CollectionAccess.Unlisted,
-                          () => html`
-                            <sl-tooltip
-                              content=${SelectCollectionAccess.Options[
-                                CollectionAccess.Unlisted
-                              ].label}
-                            >
-                              <sl-icon
-                                class="mr-[5px] inline-block align-[-1px] text-neutral-600"
-                                name=${SelectCollectionAccess.Options[
-                                  CollectionAccess.Unlisted
-                                ].icon}
-                              ></sl-icon>
-                            </sl-tooltip>
-                          `,
-                        ],
-                        [
-                          CollectionAccess.Public,
-                          () => html`
-                            <sl-tooltip
-                              content=${SelectCollectionAccess.Options[
-                                CollectionAccess.Public
-                              ].label}
-                            >
-                              <sl-icon
-                                class="mr-[5px] inline-block align-[-1px] text-success-600"
-                                name=${SelectCollectionAccess.Options[
-                                  CollectionAccess.Public
-                                ].icon}
-                              ></sl-icon>
-                            </sl-tooltip>
-                          `,
-                        ],
-                      ])
-                    : // ? html`<sl-icon
-                      //     class="mr-[5px] align-[-1px] text-sm"
-                      //     name=${SelectCollectionAccess.Options[collection.access]
-                      //       .icon}
-                      //     label=${SelectCollectionAccess.Options[
-                      //       collection.access
-                      //     ].label}
-                      //   ></sl-icon>`
-                      nothing}
+                    ? html`<sl-tooltip
+                        content=${SelectCollectionAccess.Options[
+                          collection.access
+                        ].label}
+                      >
+                        <sl-icon
+                          class=${clsx(
+                            "mr-[5px] inline-block align-[-1px]",
+                            collection.access === CollectionAccess.Public
+                              ? "text-success-600"
+                              : "text-neutral-600",
+                          )}
+                          name=${SelectCollectionAccess.Options[
+                            collection.access
+                          ].icon}
+                        ></sl-icon>
+                      </sl-tooltip>`
+                    : nothing}
                   <strong
                     class="text-base font-medium leading-tight text-stone-800 transition-colors group-hover:text-cyan-600"
                   >

From fe44872c571ba8b300fb7f2b442aed6ea07a4535 Mon Sep 17 00:00:00 2001
From: emma <hi@emma.cafe>
Date: Mon, 21 Apr 2025 21:48:17 -0400
Subject: [PATCH 9/9] add description for `collectionRefreshing` property

---
 frontend/src/pages/org/collections-list.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/frontend/src/pages/org/collections-list.ts b/frontend/src/pages/org/collections-list.ts
index c2762de76a..a9fc0e3bec 100644
--- a/frontend/src/pages/org/collections-list.ts
+++ b/frontend/src/pages/org/collections-list.ts
@@ -128,8 +128,9 @@ export class CollectionsList extends BtrixElement {
   @state()
   private selectedCollection?: Collection;
 
+  /** ID of the collection currently being refreshed */
   @state()
-  collectionRefreshing: string | null = null;
+  private collectionRefreshing: string | null = null;
 
   @state()
   private fetchErrorStatusCode?: number;