From 737e83f629f424a434339c511312e38751666696 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Sun, 15 Feb 2026 17:40:39 -0800 Subject: [PATCH] Restyle admin and trip UI with wa-tag, wa-details expandos, and visual hierarchy --- static/admin.html | 55 +++++++++++++++++++++++---------- static/admin.js | 77 ++++++++++++++++++++++++++--------------------- static/trip.html | 58 +++++++++++++++++++++++------------ static/trip.js | 77 +++++++++++++++++++++++++++-------------------- 4 files changed, 164 insertions(+), 103 deletions(-) diff --git a/static/admin.html b/static/admin.html index f082faf..2c1fca9 100644 --- a/static/admin.html +++ b/static/admin.html @@ -13,6 +13,7 @@ opacity: 0; font-size: 0.9rem; } + a { color: inherit; text-decoration: none; } .header { display: flex; align-items: center; @@ -21,18 +22,39 @@ } .header img { width: 32px; height: 32px; border-radius: 50%; } .header .spacer { flex: 1; } - h2 { margin: 0.75rem 0 0.25rem; font-size: 1.1rem; } - wa-card { margin-bottom: 0.5rem; display: block; --spacing: 0.6rem; } - wa-button::part(base) { padding: 0.15rem 0.4rem; min-height: 0; } - wa-button[size="small"]::part(base) { font-size: 0.8rem; } + h2 { margin: 0.75rem 0 0.5rem; font-size: 1.1rem; } + wa-card { margin-bottom: 0.5rem; display: block; --spacing: 0.6rem; --border-width: 0; --border-radius: 0.5rem; } + wa-card::part(base) { background: var(--wa-color-neutral-50); } wa-input::part(base) { min-height: 0; } - .trip-header { display: flex; align-items: center; gap: 0.25rem; } - .trip-header h3 { flex: 1; margin: 0; font-size: 1rem; } - .admin-row { display: flex; align-items: center; gap: 0.25rem; } - .add-row { display: flex; gap: 0.25rem; margin-top: 0.25rem; } - .add-row wa-input { flex: 1; } - .create-trip { display: flex; gap: 0.25rem; margin-bottom: 0.75rem; } - .create-trip wa-input { flex: 1; } + wa-details { margin-top: 0.5rem; font-size: 1rem; --spacing: 0.6rem; } + wa-card wa-details { font-size: 0.8rem; } + wa-details::part(summary) { font-weight: bold; } + wa-details::part(content) { padding-top: 0.2rem; } + .trip-name { font-weight: bold; font-size: 1rem; color: var(--wa-color-brand-60); } + .tags { display: flex; flex-wrap: wrap; gap: 0.25rem; } + wa-tag { transition: opacity var(--wa-transition-normal); } + wa-input.email { max-width: 20rem; } + .add-form { display: flex; flex-direction: column; gap: 0.3rem; max-width: 20rem; } + .add-form wa-button { align-self: flex-start; } + .input-action { + background: none; + border: none; + cursor: pointer; + font-size: 1rem; + opacity: 0.4; + padding: 0 0.2rem; + } + .input-action:hover { opacity: 1; } + .close-btn { + all: unset; + cursor: pointer; + opacity: 0; + padding: 0 0.3rem; + font-size: 1rem; + line-height: 1; + } + wa-card:hover .close-btn { opacity: 0.25; } + .close-btn:hover { opacity: 1 !important; color: var(--wa-color-danger-50, #dc3545); } @@ -44,13 +66,14 @@ Switch User -

Create Trip

-
- - + -

Trips

+ +
+ + Add Trip +
+
diff --git a/static/admin.js b/static/admin.js index 474508a..a8cf58a 100644 --- a/static/admin.js +++ b/static/admin.js @@ -25,75 +25,82 @@ async function loadTrips() { for (const trip of trips) { const card = document.createElement('wa-card'); - const header = document.createElement('div'); - header.className = 'trip-header'; - const h3 = document.createElement('h3'); + const nameRow = document.createElement('div'); + nameRow.style.display = 'flex'; + nameRow.style.alignItems = 'center'; const tripLink = document.createElement('a'); tripLink.href = '/trip/' + trip.id; tripLink.textContent = trip.name; - h3.appendChild(tripLink); - const deleteBtn = document.createElement('wa-button'); - deleteBtn.size = 'small'; - deleteBtn.variant = 'danger'; + tripLink.className = 'trip-name'; + tripLink.style.flex = '1'; + const deleteBtn = document.createElement('button'); + deleteBtn.className = 'close-btn'; deleteBtn.textContent = '\u00d7'; deleteBtn.addEventListener('click', async () => { if (!confirm('Delete trip "' + trip.name + '"?')) return; await api('DELETE', '/api/trips/' + trip.id); loadTrips(); }); - header.appendChild(h3); - header.appendChild(deleteBtn); - card.appendChild(header); + nameRow.appendChild(tripLink); + nameRow.appendChild(deleteBtn); + card.appendChild(nameRow); + const details = document.createElement('wa-details'); + details.summary = 'Admins'; + + const tags = document.createElement('div'); + tags.className = 'tags'; for (const admin of trip.admins) { - const row = document.createElement('div'); - row.className = 'admin-row'; - const span = document.createElement('span'); - span.textContent = admin.email; - const removeBtn = document.createElement('wa-button'); - removeBtn.size = 'small'; - removeBtn.variant = 'text'; - removeBtn.textContent = '\u00d7'; - removeBtn.addEventListener('click', async () => { + const tag = document.createElement('wa-tag'); + tag.size = 'small'; + tag.variant = 'brand'; + tag.setAttribute('with-remove', ''); + tag.textContent = admin.email; + tag.addEventListener('wa-remove', async () => { await api('DELETE', '/api/trips/' + trip.id + '/admins/' + admin.id); loadTrips(); }); - row.appendChild(span); - row.appendChild(removeBtn); - card.appendChild(row); + tags.appendChild(tag); } + details.appendChild(tags); - const addRow = document.createElement('div'); - addRow.className = 'add-row'; const input = document.createElement('wa-input'); - input.placeholder = 'Admin email'; + input.placeholder = 'Add admin email'; input.size = 'small'; - const addBtn = document.createElement('wa-button'); - addBtn.size = 'small'; + input.className = 'email'; + input.style.marginTop = '0.3rem'; + const addBtn = document.createElement('button'); + addBtn.slot = 'end'; + addBtn.className = 'input-action'; addBtn.textContent = '+'; - addBtn.addEventListener('click', async () => { + const doAdd = async () => { const email = input.value.trim(); if (!email) return; await api('POST', '/api/trips/' + trip.id + '/admins', { email }); loadTrips(); - }); - addRow.appendChild(input); - addRow.appendChild(addBtn); - card.appendChild(addRow); + }; + addBtn.addEventListener('click', doAdd); + input.addEventListener('keydown', (e) => { if (e.key === 'Enter') doAdd(); }); + input.appendChild(addBtn); + details.appendChild(input); + card.appendChild(details); container.appendChild(card); } } -document.getElementById('create-trip-btn').addEventListener('click', async () => { +async function createTrip() { const input = document.getElementById('new-trip-name'); const name = input.value.trim(); if (!name) return; await api('POST', '/api/trips', { name }); input.value = ''; loadTrips(); -}); +} + +document.getElementById('create-trip-btn').addEventListener('click', createTrip); +document.getElementById('new-trip-name').addEventListener('keydown', (e) => { if (e.key === 'Enter') createTrip(); }); await loadTrips(); -await customElements.whenDefined('wa-button'); +await customElements.whenDefined('wa-card'); document.body.style.opacity = 1; diff --git a/static/trip.html b/static/trip.html index fba7da0..1e2c0ed 100644 --- a/static/trip.html +++ b/static/trip.html @@ -21,19 +21,39 @@ } .header img { width: 32px; height: 32px; border-radius: 50%; } .header .spacer { flex: 1; } - h2 { margin: 0.75rem 0 0.25rem; font-size: 1.1rem; } - h3 { margin: 0.5rem 0 0.25rem; font-size: 1rem; } - wa-card { margin-bottom: 0.5rem; display: block; --spacing: 0.6rem; } - wa-button::part(base) { padding: 0.15rem 0.4rem; min-height: 0; } - wa-button[size="small"]::part(base) { font-size: 0.8rem; } + h2 { margin: 0.75rem 0 0.5rem; font-size: 1.1rem; color: var(--wa-color-brand-60); } + wa-card { margin-bottom: 0.5rem; display: block; --spacing: 0.6rem; --border-width: 0; --border-radius: 0.5rem; } + wa-card::part(base) { background: var(--wa-color-neutral-50); } wa-input::part(base) { min-height: 0; } - .student-header { display: flex; align-items: center; gap: 0.25rem; } - .student-header span { flex: 1; font-weight: bold; } - .parent-row { display: flex; align-items: center; gap: 0.25rem; margin-left: 0.75rem; } - .add-row { display: flex; gap: 0.25rem; margin-top: 0.25rem; margin-left: 0.75rem; } - .add-row wa-input { flex: 1; } - .add-student { display: flex; gap: 0.25rem; margin-bottom: 0.75rem; } - .add-student wa-input { flex: 1; } + wa-details { margin-top: 0.5rem; font-size: 1rem; --spacing: 0.6rem; } + wa-card wa-details { font-size: 0.8rem; } + wa-details::part(summary) { font-weight: bold; } + wa-details::part(content) { padding-top: 0.2rem; } + .student-name { font-weight: bold; display: block; margin-bottom: 0.3rem; } + .tags { display: flex; flex-wrap: wrap; gap: 0.25rem; margin-bottom: 0.3rem; } + wa-tag { transition: opacity var(--wa-transition-normal); } + wa-input.email { max-width: 20rem; } + .add-form { display: flex; flex-direction: column; gap: 0.3rem; max-width: 20rem; } + .add-form wa-button { align-self: flex-start; } + .close-btn { + all: unset; + cursor: pointer; + opacity: 0; + padding: 0 0.3rem; + font-size: 1rem; + line-height: 1; + } + wa-card:hover .close-btn { opacity: 0.25; } + .close-btn:hover { opacity: 1 !important; color: var(--wa-color-danger-50, #dc3545); } + .input-action { + background: none; + border: none; + cursor: pointer; + font-size: 1rem; + opacity: 0.4; + padding: 0 0.2rem; + } + .input-action:hover { opacity: 1; } @@ -46,14 +66,14 @@ Switch User

-

Add Student

-
- - - + -
-

Students

+ +
+ + + Add Student +
+
diff --git a/static/trip.js b/static/trip.js index 8ed9b17..88d9ee7 100644 --- a/static/trip.js +++ b/static/trip.js @@ -24,64 +24,71 @@ async function loadStudents() { for (const student of students) { const card = document.createElement('wa-card'); - const header = document.createElement('div'); - header.className = 'student-header'; + const nameRow = document.createElement('div'); + nameRow.style.display = 'flex'; + nameRow.style.alignItems = 'center'; const label = document.createElement('span'); + label.className = 'student-name'; + label.style.flex = '1'; label.textContent = student.name + ' (' + student.email + ')'; - const deleteBtn = document.createElement('wa-button'); - deleteBtn.size = 'small'; - deleteBtn.variant = 'danger'; + const deleteBtn = document.createElement('button'); + deleteBtn.className = 'close-btn'; deleteBtn.textContent = '\u00d7'; deleteBtn.addEventListener('click', async () => { if (!confirm('Remove student "' + student.name + '"?')) return; await api('DELETE', '/api/trips/' + tripID + '/students/' + student.id); loadStudents(); }); - header.appendChild(label); - header.appendChild(deleteBtn); - card.appendChild(header); + nameRow.appendChild(label); + nameRow.appendChild(deleteBtn); + card.appendChild(nameRow); + const details = document.createElement('wa-details'); + details.summary = 'Parents'; + + const tags = document.createElement('div'); + tags.className = 'tags'; for (const parent of student.parents) { - const row = document.createElement('div'); - row.className = 'parent-row'; - const span = document.createElement('span'); - span.textContent = parent.email; - const removeBtn = document.createElement('wa-button'); - removeBtn.size = 'small'; - removeBtn.variant = 'text'; - removeBtn.textContent = '\u00d7'; - removeBtn.addEventListener('click', async () => { + const tag = document.createElement('wa-tag'); + tag.size = 'small'; + tag.variant = 'success'; + tag.setAttribute('with-remove', ''); + tag.textContent = parent.email; + tag.addEventListener('wa-remove', async () => { await api('DELETE', '/api/trips/' + tripID + '/students/' + student.id + '/parents/' + parent.id); loadStudents(); }); - row.appendChild(span); - row.appendChild(removeBtn); - card.appendChild(row); + tags.appendChild(tag); } + details.appendChild(tags); - const addRow = document.createElement('div'); - addRow.className = 'add-row'; const input = document.createElement('wa-input'); - input.placeholder = 'Parent email'; + input.placeholder = 'Add parent email'; input.size = 'small'; - const addBtn = document.createElement('wa-button'); - addBtn.size = 'small'; + input.className = 'email'; + input.style.marginTop = '0.3rem'; + const addBtn = document.createElement('button'); + addBtn.slot = 'end'; + addBtn.className = 'input-action'; addBtn.textContent = '+'; - addBtn.addEventListener('click', async () => { + const doAdd = async () => { const email = input.value.trim(); if (!email) return; await api('POST', '/api/trips/' + tripID + '/students/' + student.id + '/parents', { email }); loadStudents(); - }); - addRow.appendChild(input); - addRow.appendChild(addBtn); - card.appendChild(addRow); + }; + addBtn.addEventListener('click', doAdd); + input.addEventListener('keydown', (e) => { if (e.key === 'Enter') doAdd(); }); + input.appendChild(addBtn); + details.appendChild(input); + + card.appendChild(details); container.appendChild(card); } } -document.getElementById('add-student-btn').addEventListener('click', async () => { +async function addStudent() { const nameInput = document.getElementById('new-student-name'); const emailInput = document.getElementById('new-student-email'); const name = nameInput.value.trim(); @@ -91,8 +98,12 @@ document.getElementById('add-student-btn').addEventListener('click', async () => nameInput.value = ''; emailInput.value = ''; loadStudents(); -}); +} + +document.getElementById('add-student-btn').addEventListener('click', addStudent); +document.getElementById('new-student-name').addEventListener('keydown', (e) => { if (e.key === 'Enter') addStudent(); }); +document.getElementById('new-student-email').addEventListener('keydown', (e) => { if (e.key === 'Enter') addStudent(); }); await loadStudents(); -await customElements.whenDefined('wa-button'); +await customElements.whenDefined('wa-card'); document.body.style.opacity = 1;