diff --git a/README.md b/README.md index 399e8f4..4a9f194 100644 --- a/README.md +++ b/README.md @@ -260,12 +260,14 @@ in three kinds: - `severity` is notice-only: `urgent`/`heads-up`/`info` tints the card edge, the chip, and the icon on Home. Setting it on a live widget is a validation error (live rows have no severity to sort by). -- Home renders one merged calendar card (every `calendar` widget, sorted by - date), one card per `recent` widget, then notices - urgent first, then heads-up - then info, each group in creation order. The next upcoming release that carries - artwork gets a full-width hero tile above the timeline (its relayed thumb plus - title and date); the timeline row keeps its own small thumb. Each widget also - gets its own sidebar button for a single-card view. +- Home renders one merged release group (every `calendar` widget, ascending by + date) and one group per `recent` widget (newest first) - each group is a header + (title, refresh stamp, per-widget errors) followed by one card per release, + laid out as a wrapping grid - then notices, urgent first, then heads-up then + info, each group in creation order. The next upcoming release that carries + artwork also gets a full-width hero tile above the groups (its relayed thumb + plus title and date). Each widget gets its own sidebar button for a + single-widget view. - Writes are admin-only when OIDC is on, reads any session - same gate as services. diff --git a/src/uwudash_mk3/static/app.js b/src/uwudash_mk3/static/app.js index 08bf03e..2e33d9e 100644 --- a/src/uwudash_mk3/static/app.js +++ b/src/uwudash_mk3/static/app.js @@ -233,30 +233,21 @@ function noticeCard(widget) { ); } -function itemRow(item) { +function releaseCard(item) { const thumb = item.thumb ? `${esc(item.label)}` : ""; return ( - `
` + + `
` + thumb + `${esc(item.label)}` + (item.sub ? `${esc(item.sub)}` : "") + + `${esc(dayLabel(item.date))}` + `${esc(item.source || "")}` + `
` ); } -function groupByDate(items) { - const days = []; - for (const item of items) { - const last = days[days.length - 1]; - if (last && last.date === item.date) last.items.push(item); - else days.push({ date: item.date, items: [item] }); - } - return days; -} - function dayLabel(date) { const parts = (date || "").split("-").map(Number); if (parts.length !== 3) return date || ""; @@ -278,11 +269,14 @@ function clockOf(iso) { return new Date(iso).toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" }); } -// One card per live widget group: every calendar widget merges into a single -// sorted timeline, each "recently added" widget gets its own card. -function liveCard(widgets, title) { +// A live group is a header plus one card per release: every calendar widget +// merges into one ascending timeline, each "recently added" widget keeps its +// own newest-first group (upstream order, no re-sorting). +function liveCard(widgets, title, ascending) { const items = widgets.flatMap((widget) => (widget.data || {}).items || []); - const days = groupByDate(items); + const rows = ascending + ? items.slice().sort((a, b) => ((a.date || "") < (b.date || "") ? -1 : 1)) + : items; const notes = widgets .map((widget) => { const data = widget.data || {}; @@ -293,24 +287,22 @@ function liveCard(widgets, title) { const stamp = refreshed.length ? `refreshed ${clockOf(refreshed.sort()[0])}` : ""; const meta = [...notes, stamp].filter(Boolean).join(" ยท "); return ( - `
` + + `
` + + `
` + `${widgetIcon(widgets[0])}` + `
${esc(title)}
` + `
${esc(meta)}
` + - days.map( - (day) => - `
` + - `
${esc(dayLabel(day.date))}
` + - day.items.map(itemRow).join("") + - `
` - ).join("") + - (days.length ? "" : `
${esc(meta || "nothing in the window")}
`) + + `
` + + (rows.length ? `
${rows.map(releaseCard).join("")}
` : "") + + (rows.length ? "" : `
${esc(meta || "nothing in the window")}
`) + `
` ); } function widgetCard(widget) { - return widget.kind === "notice" ? noticeCard(widget) : liveCard([widget], widget.name); + return widget.kind === "notice" + ? noticeCard(widget) + : liveCard([widget], widget.name, widget.kind === "calendar"); } const SEVERITY_RANK = { urgent: 0, "heads-up": 1, info: 2 }; @@ -348,8 +340,8 @@ function renderHome() { const notices = state.widgets.filter((widget) => widget.kind === "notice"); notices.sort((a, b) => SEVERITY_RANK[a.severity || "info"] - SEVERITY_RANK[b.severity || "info"]); const cards = []; - if (calendars.length) cards.push(liveCard(calendars, "Upcoming")); - for (const widget of recents) cards.push(liveCard([widget], widget.name)); + if (calendars.length) cards.push(liveCard(calendars, "Upcoming", true)); + for (const widget of recents) cards.push(liveCard([widget], widget.name, false)); for (const widget of notices) cards.push(noticeCard(widget)); const banner = `
` + diff --git a/src/uwudash_mk3/static/styles.css b/src/uwudash_mk3/static/styles.css index db3978f..f5e93bb 100644 --- a/src/uwudash_mk3/static/styles.css +++ b/src/uwudash_mk3/static/styles.css @@ -331,23 +331,39 @@ body { .widget-card.notice.heads-up .icon { color: var(--warning); } .widget-card.notice.urgent .icon { color: var(--error); } -/* live widgets: one merged timeline for calendars, own card for recents */ -.widget-card.live { border-left: 3px solid var(--accent); } -.widget-card.live .widget-meta { color: var(--muted); font-size: 12px; } -.widget-card.live .widget-day { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; } -.widget-card.live .widget-day-head { font-size: 12px; font-weight: 700; color: var(--muted); } -.widget-card.live .widget-item { display: flex; align-items: center; gap: 10px; } -.widget-card.live .widget-thumb { - width: 34px; - height: 50px; - object-fit: cover; - border-radius: 4px; - margin-right: 8px; +/* live widgets: a header plus one card per release */ +.live-group { display: flex; flex-direction: column; gap: 12px; margin-bottom: 12px; } +.live-head { display: flex; align-items: center; gap: 10px; } +.live-head .icon { font-size: 16px; } +.live-head .widget-title { font-weight: 700; } +.live-head .widget-meta { color: var(--muted); font-size: 12px; } +.release-cards { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); + gap: 12px; } -.widget-card.live .widget-label { font-weight: 600; font-size: 14px; } -.widget-card.live .widget-sub { color: var(--muted); font-size: 12px; } -.widget-card.live .widget-src { font-size: 11px; color: var(--accent); } -.widget-card.live .widget-empty { color: var(--muted); font-size: 13px; margin-top: 8px; } +.release-card { + display: flex; + flex-direction: column; + gap: 4px; + background: var(--panel); + border: 1px solid var(--border); + border-left: 3px solid var(--accent); + border-radius: 10px; + padding: 12px 14px; +} +.release-card .widget-thumb { + width: 56px; + height: 84px; + object-fit: cover; + border-radius: 6px; + margin-bottom: 8px; +} +.release-card .widget-label { font-weight: 600; font-size: 14px; } +.release-card .widget-sub { color: var(--muted); font-size: 12px; } +.release-card .widget-when { font-size: 12px; font-weight: 700; color: var(--muted); } +.release-card .widget-src { font-size: 11px; color: var(--accent); } +.widget-empty { color: var(--muted); font-size: 13px; } .home-empty { color: var(--muted); font-size: 14px; }