playlists(#37): profile-modal "Playlists" tab — rename / delete / reorder / remove

A central management surface, a sibling tab to "My Videos" (gated by
FEATURE_PLAYLISTS — hidden when off).

- components/profile-modal.html: "Playlists" nav item + #profilePanePlaylists.
- js/ui/profileModal/ProfilePlaylistsController.js: lists the user's playlists
  (incl. empty) with a collapsible row each — Rename, Delete, and an expand to a
  video sublist with ↑/↓ reorder and Remove. Fetches the referenced creators'
  videos so the sublist shows real titles/thumbnails. Edits are optimistic and
  re-sync on failure; publishes via the facade; dispatches
  bitvid:playlists-changed so the channel section + this pane stay in sync.
- profileModalController.js: instantiate + bind nav/pane + cache DOM + register
  listeners + selectPane("playlists") refresh; hide the tab when the flag is off.
- promptDialog.js: showTextPrompt (plain text prompt) for rename.
- playlistFacade.js: deletePlaylist — NIP-09 kind-5 (a + e refs) plus an empty
  republish so kind-5-ignoring readers also drop it.
- tailwind.source.css: .playlist-row* styles (token-only).

Lint + build clean; profile-modal-controller tests 26/26.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-07-06 15:16:06 -04:00
1 parent 2a543c3ba9
commit 73a99358a2
6 files changed
+777

No files matched your search

+92
View File
@@ -3063,6 +3063,98 @@
overflow: hidden;
}
/* #37: profile modal "Playlists" management pane — collapsible rows. */
.playlist-row {
border: var(--border-width-hairline) solid var(--color-border-subtle);
border-radius: var(--radius-md);
overflow: hidden;
background-color: var(--color-page-alt);
}
.playlist-row + .playlist-row {
margin-block-start: var(--space-2xs);
}
.playlist-row__head {
display: flex;
align-items: center;
gap: var(--space-sm);
padding: var(--space-sm);
cursor: pointer;
}
.playlist-row__chevron svg {
color: var(--color-text-muted);
transition: transform 0.15s ease-out;
}
.playlist-row__chevron[data-open="true"] svg {
transform: rotate(90deg);
}
.playlist-row__meta {
flex: 1 1 auto;
min-inline-size: 0;
}
.playlist-row__title {
margin: 0;
font-weight: 600;
color: var(--color-text-strong);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.playlist-row__count {
margin: 0;
font-size: var(--font-size-2xs);
color: var(--color-text-muted);
}
.playlist-row__actions {
display: flex;
gap: var(--space-2xs);
flex-shrink: 0;
}
.playlist-row__videos {
border-block-start: var(--border-width-hairline) solid
var(--color-border-subtle);
padding: var(--space-2xs) var(--space-sm) var(--space-sm);
display: flex;
flex-direction: column;
gap: var(--space-2xs);
}
.playlist-row__empty {
padding-block: var(--space-2xs);
font-size: var(--font-size-sm);
color: var(--color-text-muted);
}
.playlist-video {
display: flex;
align-items: center;
gap: var(--space-sm);
}
.playlist-video__title {
flex: 1 1 auto;
min-inline-size: 0;
font-size: var(--font-size-sm);
color: var(--color-text);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.playlist-video__controls {
display: flex;
gap: var(--space-3xs);
flex-shrink: 0;
}
/* #43: always-visible active-identity pill. Rendered after the header in
flow and pulled up with a negative block-start margin so it sits half-on /
half-off the header divider — every pane shows who you're acting as. */