lyrics

lyrics
git clone git://brookjeynes.dev/bjeynes/lyrics.git
Log | Files | Refs

commit af0dcec372cd34daf5d1c50f6fce7eaf1e0b34b1
parent 38f4cc890fb8c3359cbf28632acf6ef178c3760e
Author: brookjeynes <me@brookjeynes.dev>
Date:   Sat, 13 Jun 2026 13:58:49 +1000

ui: (new) album page ui refresh

Signed-off-by: brookjeynes <me@brookjeynes.dev>

Diffstat:
Minternal/server/ui/js/lucide-utils.js | 15+++++++++------
Minternal/server/ui/layouts/base/base.templ | 4+++-
Minternal/server/ui/partials/similar-albums-list/similar-albums-list.templ | 29++++++++++++++++-------------
Minternal/server/ui/style.css | 14++++++++++++--
Ainternal/server/ui/views/album/album.css | 25+++++++++++++++++++++++++
Minternal/server/ui/views/album/album.templ | 74++++++++++++++++++++++++++++++++++++++++++++++----------------------------
Minternal/server/ui/views/artist/artist.templ | 2+-
Ainternal/server/ui/views/new-album/new-album.css | 48++++++++++++++++++++++++++++++++++++++++++++++++
Minternal/server/ui/views/new-album/new-album.templ | 137+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Minternal/server/ui/views/new-song/new-song.templ | 84++++++++++++++++++++++++++++++++++++++++----------------------------------------
Minternal/server/ui/views/song/song.css | 10----------
Mscripts/build-assets.sh | 2++
Mstatic/minimal.css | 2+-
13 files changed, 286 insertions(+), 160 deletions(-)

diff --git a/internal/server/ui/js/lucide-utils.js b/internal/server/ui/js/lucide-utils.js @@ -1,8 +1,11 @@ function initLucideIcons() { - lucide.createIcons({ - icons: { - X: lucide.X, - Info: lucide.Info, - } - }); + lucide.createIcons({ + icons: { + X: lucide.X, + Plus: lucide.Plus, + ChevronUp: lucide.ChevronUp, + ChevronDown: lucide.ChevronDown, + Info: lucide.Info, + } + }); } diff --git a/internal/server/ui/layouts/base/base.templ b/internal/server/ui/layouts/base/base.templ @@ -12,7 +12,7 @@ templ Header() { } templ Footer() { - <footer> + <footer role="contentinfo"> <a href="/">lyrics</a> </footer> } @@ -32,6 +32,8 @@ templ Base(params BaseParams) { <link rel="stylesheet" href="/static/style.css" type="text/css"/> <link rel="stylesheet" href="/static/song.css" type="text/css"/> <link rel="stylesheet" href="/static/artist.css" type="text/css"/> + <link rel="stylesheet" href="/static/album.css" type="text/css"/> + <link rel="stylesheet" href="/static/new-album.css" type="text/css"/> <link rel="stylesheet" href="/static/minimal.css" type="text/css"/> <link rel="stylesheet" href="/static/toast.css" type="text/css"/> <title>{ params.Title }</title> diff --git a/internal/server/ui/partials/similar-albums-list/similar-albums-list.templ b/internal/server/ui/partials/similar-albums-list/similar-albums-list.templ @@ -3,18 +3,21 @@ package similaralbumslist import "fmt" templ SimilarAlbumsList(params SimilarAlbumsListParams) { - <ul id="similar-albums"> - for _, album := range params.Albums { - {{ - value := album.Title + if len(params.Albums) >= 1 { + <p class="section-label -muted">Similar albums</p> + <ul> + for _, album := range params.Albums { + {{ + value := album.Title - if album.LatinTitle != nil { - value = fmt.Sprintf("%s (%s)", album.Title, *album.LatinTitle) - } - }} - <li title={ value }> - { value } - </li> - } - </ul> + if album.LatinTitle != nil { + value = fmt.Sprintf("%s (%s)", album.Title, *album.LatinTitle) + } + }} + <li title={ value }> + { value } + </li> + } + </ul> + } } diff --git a/internal/server/ui/style.css b/internal/server/ui/style.css @@ -1,4 +1,14 @@ svg[data-lucide] { - width: 12px; - height: 12px; + width: 12px; + height: 12px; +} + +dl { + dd { + margin-bottom: 0.5rem; + + ul { + padding-left: 0; + } + } } diff --git a/internal/server/ui/views/album/album.css b/internal/server/ui/views/album/album.css @@ -0,0 +1,25 @@ +.album-layout { + display: flex; + gap: 2rem; + align-items: flex-start; +} + +.album-content { + flex: 1; + min-width: 0; +} + +.album-details { + flex: 0 0 18rem; +} + +@media (max-width: 700px) { + .album-layout { + flex-direction: column; + } + + .album-details { + width: 100%; + flex-basis: auto; + } +} diff --git a/internal/server/ui/views/album/album.templ b/internal/server/ui/views/album/album.templ @@ -8,42 +8,60 @@ import ( templ Album(params AlbumParams) { @base.Base(base.BaseParams{Title: params.Album.Title}) { - <article> - <header> - <hgroup> + <article class="album-layout"> + <section class="album-content"> + <header> <h1> { params.Album.Title } if params.Album.LatinTitle != nil { <span>({ *params.Album.LatinTitle })</span> } </h1> - </hgroup> - <ul> - for _, artist := range params.Album.Artists { - <li> - <a href={ routes.Artist(artist.Artist) }> - { artist.Artist.Name } - if artist.Artist.LatinName != nil { - <span>({ *artist.Artist.LatinName })</span> - } - </a> - </li> + </header> + <section aria-label="Track list"> + if len(params.Album.Songs) == 0 { + <p class="section-label -muted">No songs yet.</p> + } else { + <ol> + for _, song := range params.Album.Songs { + <li> + <a href={ routes.Song(song) }> + { song.Title } + if song.LatinTitle != nil { + <span>({ *song.LatinTitle })</span> + } + </a> + </li> + } + </ol> } - </ul> - <time datetime={ params.Album.ReleaseDate.Format(time.DateOnly) }>{ params.Album.ReleaseDate.Format("Jan 2, 2006") }</time> - </header> - <ol> - for _, song := range params.Album.Songs { - <li> - <a href={ routes.Song(song) }> - { song.Title } - if song.LatinTitle != nil { - <span>({ *song.LatinTitle })</span> + </section> + </section> + <aside class="album-details"> + <dl> + <dt>Artist(s)</dt> + <dd> + <ul> + for _, artist := range params.Album.Artists { + <li> + <a href={ routes.Artist(artist.Artist) }> + { artist.Artist.Name } + if artist.Artist.LatinName != nil { + <span>({ *artist.Artist.LatinName })</span> + } + </a> + </li> } - </a> - </li> - } - </ol> + </ul> + </dd> + <dt>Released</dt> + <dd> + <time datetime={ params.Album.ReleaseDate.Format(time.DateOnly) }> + { params.Album.ReleaseDate.Format("Jan 2, 2006") } + </time> + </dd> + </dl> + </aside> </article> } } diff --git a/internal/server/ui/views/artist/artist.templ b/internal/server/ui/views/artist/artist.templ @@ -18,7 +18,7 @@ templ Artist(params ArtistParams) { } </h1> if params.Artist.Disambiguation != nil { - <p>{ *params.Artist.Disambiguation }</p> + <p class="section-label -muted">{ *params.Artist.Disambiguation }</p> } </hgroup> if params.Artist.Bio != nil { diff --git a/internal/server/ui/views/new-album/new-album.css b/internal/server/ui/views/new-album/new-album.css @@ -0,0 +1,48 @@ +.album-form { + max-width: 42rem; + + fieldset { + margin: 1.5rem 0; + } +} + +.similar-albums { + margin: 1rem 0 1.5rem; + + p { + margin-bottom: 0.5rem; + } +} + +.artist-picker-list { + padding-left: 1.5rem; + + li { + margin-bottom: 0.75rem; + } +} + +.artist-picker-row { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.artist-picker-main { + flex: 1; + min-width: 0; +} + +.artist-picker-actions { + display: flex; + gap: 0.5rem; + align-items: center; + margin-top: 0.5rem; +} + +.selected-picker-item { + display: flex; + align-items: baseline; + gap: 0.75rem; + margin: 0.5rem 0; +} diff --git a/internal/server/ui/views/new-album/new-album.templ b/internal/server/ui/views/new-album/new-album.templ @@ -11,18 +11,20 @@ templ NewAlbum(params NewAlbumParams) { @base.Base(base.BaseParams{Title: "new album"}) { <h1>Add an album</h1> <form + class="album-form" hx-post="/album/new" hx-swap="none" hx-disable-elt="#add-album-btn" > <fieldset x-data="{ showLatinTitle: false }"> + <legend>Album</legend> <label> - album: + title: <input hx-get="/album/search?format=similar" - hx-trigger="input changed delay:500ms, load" + hx-trigger="input changed delay:500ms" hx-target="#similar-albums" - hx-swap="outerHTML" + hx-swap="innerHTML" name="album" type="text" pattern={ domain.NoTrailingOrLeadingSpaceRegex.String() } @@ -39,73 +41,96 @@ templ NewAlbum(params NewAlbumParams) { pattern={ domain.IsValidSortNamePattern } /> </label> - <div> - <span>similar albums</span> - <ul id="similar-albums"></ul> - </div> </fieldset> + <section id="similar-albums" class="similar-albums" aria-label="Similar albums"></section> <fieldset x-data="{ items: [{ id: 0 }], uid: 1 }"> - <legend> - artists - <button type="button" @click="items.push({ id: uid++ })">+ add</button> - </legend> - <ol> + <legend>Artists</legend> + <ol class="artist-picker-list"> <template x-for="(item, i) in items" :key="item.id"> <li x-data="{ selected: false, artistId: '', artistName: '' }" x-init="htmx.process($el); initLucideIcons()" > - <label x-show="!selected"> - <span>artist</span> - <input - type="search" - hx-get="/artist/search?format=suggest" - hx-trigger="input changed delay:500ms" - hx-target="next [role='radiogroup']" - hx-swap="outerHTML" - name="artist" - autocomplete="off" - :required="!selected" - /> - </label> - <p x-show="selected"> - <span x-text="artistName"></span> - <button - type="button" - @click="selected = false; artistId = ''; artistName = ''" - > - change - </button> - </p> - <button - type="button" - aria-label="Remove" - x-show="items.length > 1" - @click="items.splice(i, 1)" - > - <i data-lucide="x"></i> - </button> + <div class="artist-picker-row"> + <div class="artist-picker-main"> + <label x-show="!selected"> + <span>artist:</span> + <input + type="search" + hx-get="/artist/search?format=suggest" + hx-trigger="input changed delay:500ms" + hx-target="next [role='radiogroup']" + hx-swap="outerHTML" + name="artist" + autocomplete="off" + :required="!selected" + /> + </label> + <div class="selected-picker-item" x-show="selected"> + <strong x-text="artistName"></strong> + <button + type="button" + class="-secondary" + @click="selected = false; artistId = ''; artistName = ''" + > + change + </button> + </div> + </div> + <div class="artist-picker-actions"> + <button + type="button" + aria-label="Remove" + x-show="items.length > 1" + @click="items.splice(i, 1)" + > + <i data-lucide="x"></i> + </button> + <button + type="button" + aria-label="Move up" + x-show="selected && items.length > 1 && i >= 1" + @click="[items[i - 1], items[i]] = [items[i], items[i - 1]]" + > + <i data-lucide="chevron-up"></i> + </button> + <button + type="button" + aria-label="Move down" + x-show="selected && items.length > 1 && i + 1 < items.length" + @click="[items[i + 1], items[i]] = [items[i], items[i + 1]]" + > + <i data-lucide="chevron-down"></i> + </button> + </div> + </div> <div x-show="!selected"> <fieldset role="radiogroup" x-show="false"></fieldset> </div> - <input type="hidden" name="artist-id" :value="artistId"/> + <input type="hidden" :name="selected ? 'artist-id' : ''" :value="artistId"/> </li> </template> </ol> + <button class="-secondary" type="button" @click="items.push({ id: uid++ })"> + add additional artist + </button> + </fieldset> + <fieldset> + <legend>Release</legend> + <label> + release date: + <input + name="release-date" + type="date" + min="1000-01-01" + max={ fmt.Sprintf("%s", time.Now().AddDate(3, 0, 0).Format("2006-01-02")) } + required + /> + </label> </fieldset> - <label> - release date: - <input - name="release-date" - type="date" - min="1000-01-01" - max={ fmt.Sprintf("%s", time.Now().AddDate(3, 0, 0).Format("2006-01-02")) } - required - /> - </label> - <button id="add-album-btn" type="submit"> - add album - </button> + <footer aria-label="Form actions"> + <button class="-primary" id="add-album-btn" type="submit">add album</button> + </footer> </form> <script src="/static/js/new-album.js" defer></script> } diff --git a/internal/server/ui/views/new-song/new-song.templ b/internal/server/ui/views/new-song/new-song.templ @@ -15,35 +15,59 @@ templ NewSong(params NewSongParams) { hx-swap="none" hx-disable-elt="#add-song-btn" > + <fieldset x-data="{ showLatinTitle: false }"> + <legend> + title + </legend> + <label> + title: + <input + name="title" + type="text" + pattern={ domain.NoTrailingOrLeadingSpaceRegex.String() } + required + @input="showLatinTitle = isNonAscii($event.target.value)" + /> + </label> + <label x-show="showLatinTitle"> + latin name: + <input + name="latin-title" + type="text" + :required="showLatinTitle" + pattern={ domain.IsValidSortNamePattern } + /> + </label> + </fieldset> <fieldset x-data="{ items: [{ id: 0 }], uid: 1 }"> <legend> - artists + albums <button type="button" @click="items.push({ id: uid++ })">+ add</button> </legend> <ol> <template x-for="(item, i) in items" :key="item.id"> <li - x-data="{ selected: false, artistId: '', artistName: '' }" + x-data="{ selected: false, albumId: '', albumTitle: '' }" x-init="htmx.process($el); initLucideIcons()" > <label x-show="!selected"> - <span>artist</span> + <span>album</span> <input type="search" - hx-get="/artist/search?format=suggest" + hx-get="/album/search?format=suggest" hx-trigger="input changed delay:500ms" hx-target="next [role='radiogroup']" hx-swap="outerHTML" - name="artist" + name="album" autocomplete="off" - :required="!selected" + :required="!selected && id > 0" /> </label> <p x-show="selected"> - <span x-text="artistName"></span> + <span x-text="albumTitle"></span> <button type="button" - @click="selected = false; artistId = ''; artistName = ''" + @click="selected = false; albumId = ''; albumTitle = ''" > change </button> @@ -59,64 +83,40 @@ templ NewSong(params NewSongParams) { <div x-show="!selected"> <fieldset role="radiogroup" x-show="false"></fieldset> </div> - <input type="hidden" name="artist-id" :value="artistId"/> + <input type="hidden" name="album-id" :value="albumId"/> </li> </template> </ol> </fieldset> - <fieldset x-data="{ showLatinTitle: false }"> - <legend> - title - </legend> - <label> - title: - <input - name="title" - type="text" - pattern={ domain.NoTrailingOrLeadingSpaceRegex.String() } - required - @input="showLatinTitle = isNonAscii($event.target.value)" - /> - </label> - <label x-show="showLatinTitle"> - latin name: - <input - name="latin-title" - type="text" - :required="showLatinTitle" - pattern={ domain.IsValidSortNamePattern } - /> - </label> - </fieldset> <fieldset x-data="{ items: [{ id: 0 }], uid: 1 }"> <legend> - albums + artists <button type="button" @click="items.push({ id: uid++ })">+ add</button> </legend> <ol> <template x-for="(item, i) in items" :key="item.id"> <li - x-data="{ selected: false, albumId: '', albumTitle: '' }" + x-data="{ selected: false, artistId: '', artistName: '' }" x-init="htmx.process($el); initLucideIcons()" > <label x-show="!selected"> - <span>album</span> + <span>artist</span> <input type="search" - hx-get="/album/search?format=suggest" + hx-get="/artist/search?format=suggest" hx-trigger="input changed delay:500ms" hx-target="next [role='radiogroup']" hx-swap="outerHTML" - name="album" + name="artist" autocomplete="off" - :required="!selected && id > 0" + :required="!selected" /> </label> <p x-show="selected"> - <span x-text="albumTitle"></span> + <span x-text="artistName"></span> <button type="button" - @click="selected = false; albumId = ''; albumTitle = ''" + @click="selected = false; artistId = ''; artistName = ''" > change </button> @@ -132,7 +132,7 @@ templ NewSong(params NewSongParams) { <div x-show="!selected"> <fieldset role="radiogroup" x-show="false"></fieldset> </div> - <input type="hidden" name="album-id" :value="albumId"/> + <input type="hidden" name="artist-id" :value="artistId"/> </li> </template> </ol> diff --git a/internal/server/ui/views/song/song.css b/internal/server/ui/views/song/song.css @@ -11,16 +11,6 @@ .song-details { flex: 0 0 18rem; - - dl { - dd { - margin-bottom: 0.5rem; - - ul { - padding-left: 0; - } - } - } } @media (max-width: 700px) { diff --git a/scripts/build-assets.sh b/scripts/build-assets.sh @@ -16,6 +16,8 @@ minify internal/server/ui/style.css -o static/files/style.css minify internal/server/toast/toast.css -o static/files/toast.css minify internal/server/ui/views/song/song.css -o static/files/song.css minify internal/server/ui/views/artist/artist.css -o static/files/artist.css +minify internal/server/ui/views/album/album.css -o static/files/album.css +minify internal/server/ui/views/new-album/new-album.css -o static/files/new-album.css minify static/minimal.css -o static/files/minimal.css minify internal/server/ui/js/string-utils.js -o static/files/js/string-utils.js diff --git a/static/minimal.css b/static/minimal.css @@ -66,7 +66,7 @@ a:visited { } } -footer { +footer[role="contentinfo"] { margin: 2rem 0; font-size: 75%; color: var(--secondary);