From 4119391ef3e84a2611b4b858c8bd5e5436be684a Mon Sep 17 00:00:00 2001 From: Ricardo Date: Thu, 12 Feb 2026 19:18:37 +0100 Subject: [PATCH] refactor: align to upstream @indiekit/frontend patterns - Extract 177 lines inline CSS to assets/styles.css - Create intermediate layout (views/layouts/youtube.njk) - Use section() macro for dashboard sections - Rename CSS prefix from yt-* to youtube-* - Replace inline style for LIVE duration badge with CSS class - Use upstream CSS custom properties without fallbacks - Keep animation CSS (@keyframes youtube-pulse) - Add assets to package.json files array - Bump version to 1.2.2 Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 4 + assets/styles.css | 214 +++++++++++++++++++++++++++ package-lock.json | 4 +- package.json | 3 +- views/layouts/youtube.njk | 6 + views/youtube.njk | 299 ++++++++------------------------------ 6 files changed, 288 insertions(+), 242 deletions(-) create mode 100644 assets/styles.css create mode 100644 views/layouts/youtube.njk diff --git a/CLAUDE.md b/CLAUDE.md index a5f8a92..091d744 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -42,3 +42,7 @@ YouTube Data API has a 10,000 units/day default quota: - `search.list`: 100 units (used only for full live status check with `?full=true`) The plugin uses the playlist method by default for quota efficiency. + +## Workspace Context + +This plugin is part of the Indiekit development workspace at `/home/rick/code/indiekit-dev/`. See the workspace CLAUDE.md for the full repository map. It is deployed via `indiekit-cloudron/` and listed in its `indiekit.config.js` plugins array. diff --git a/assets/styles.css b/assets/styles.css new file mode 100644 index 0000000..c3fc6b9 --- /dev/null +++ b/assets/styles.css @@ -0,0 +1,214 @@ +/* YouTube endpoint styles */ + +/* Channel header */ +.youtube-channel { + align-items: center; + background: var(--color-offset); + border-radius: var(--radius-m); + display: flex; + gap: var(--space-m); + margin-block-end: var(--space-l); + padding: var(--space-m); +} + +.youtube-channel__avatar { + border-radius: 50%; + flex-shrink: 0; + height: 64px; + object-fit: cover; + width: 64px; +} + +.youtube-channel__info { + flex: 1; +} + +.youtube-channel__name { + font-size: var(--step-1); + font-weight: var(--font-weight-semibold); + margin: 0 0 var(--space-3xs) 0; +} + +.youtube-channel__stats { + color: var(--color-text-secondary); + display: flex; + font-size: var(--step--1); + gap: var(--space-m); +} + +/* Live status badge */ +.youtube-live-badge { + align-items: center; + border-radius: 2rem; + display: inline-flex; + font-size: var(--step--2); + font-weight: var(--font-weight-semibold); + gap: var(--space-xs); + padding: var(--space-3xs) var(--space-s); + text-transform: uppercase; +} + +.youtube-live-badge--live { + animation: youtube-pulse 2s infinite; + background: #ff0000; + color: white; +} + +.youtube-live-badge--upcoming { + background: #065fd4; + color: white; +} + +.youtube-live-badge--offline { + background: var(--color-offset); + color: var(--color-text-secondary); +} + +@keyframes youtube-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } +} + +.youtube-live-dot { + background: currentColor; + border-radius: 50%; + height: 8px; + width: 8px; +} + +/* Live stream card */ +.youtube-live-stream { + background: linear-gradient(135deg, #ff000010, #ff000005); + border: 1px solid #ff000030; + border-radius: var(--radius-m); + display: flex; + gap: var(--space-m); + margin-block-end: var(--space-l); + padding: var(--space-m); +} + +.youtube-live-stream__thumb { + border-radius: var(--radius-s); + flex-shrink: 0; + height: 90px; + object-fit: cover; + width: 160px; +} + +.youtube-live-stream__info { + flex: 1; +} + +.youtube-live-stream__title { + font-weight: var(--font-weight-semibold); + margin: 0 0 var(--space-xs) 0; +} + +.youtube-live-stream__title a { + color: inherit; + text-decoration: none; +} + +.youtube-live-stream__title a:hover { + text-decoration: underline; +} + +/* Video grid */ +.youtube-video-grid { + display: grid; + gap: var(--space-m); + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + list-style: none; + margin: 0; + padding: 0; +} + +.youtube-video { + background: var(--color-offset); + border-radius: var(--radius-m); + display: flex; + flex-direction: column; + overflow: hidden; +} + +.youtube-video__thumb-wrapper { + aspect-ratio: 16/9; + position: relative; +} + +.youtube-video__thumb { + height: 100%; + object-fit: cover; + width: 100%; +} + +.youtube-video__duration { + background: rgba(0, 0, 0, 0.8); + border-radius: var(--radius-s); + bottom: var(--space-2xs); + color: white; + font-size: var(--step--2); + font-weight: var(--font-weight-medium); + padding: var(--space-3xs) var(--space-2xs); + position: absolute; + right: var(--space-2xs); +} + +.youtube-video__duration--live { + background: #ff0000; +} + +.youtube-video__info { + display: flex; + flex: 1; + flex-direction: column; + padding: var(--space-s); +} + +.youtube-video__title { + display: -webkit-box; + font-size: var(--step--1); + font-weight: var(--font-weight-medium); + margin: 0 0 var(--space-xs) 0; + overflow: hidden; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; +} + +.youtube-video__title a { + color: inherit; + text-decoration: none; +} + +.youtube-video__title a:hover { + text-decoration: underline; +} + +.youtube-video__meta { + color: var(--color-text-secondary); + font-size: var(--step--2); + margin-top: auto; +} + +/* Multi-channel separator */ +.youtube-channel-section + .youtube-channel-section { + border-block-start: 1px solid var(--color-border); + margin-block-start: var(--space-xl); + padding-block-start: var(--space-xl); +} + +/* Public link banner */ +.youtube-public-link { + align-items: center; + background: var(--color-offset); + border-radius: var(--radius-m); + display: flex; + justify-content: space-between; + margin-block-start: var(--space-xl); + padding: var(--space-m); +} + +.youtube-public-link p { + color: var(--color-text-secondary); + margin: 0; +} diff --git a/package-lock.json b/package-lock.json index 49681d3..55ca34e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@rmdes/indiekit-endpoint-youtube", - "version": "1.0.0", + "version": "1.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@rmdes/indiekit-endpoint-youtube", - "version": "1.0.0", + "version": "1.2.1", "license": "MIT", "dependencies": { "@indiekit/error": "^1.0.0-beta.25", diff --git a/package.json b/package.json index 1826a7c..44803f4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@rmdes/indiekit-endpoint-youtube", - "version": "1.2.1", + "version": "1.2.2", "description": "YouTube channel endpoint for Indiekit. Display latest videos and live status from any YouTube channel.", "keywords": [ "indiekit", @@ -33,6 +33,7 @@ ".": "./index.js" }, "files": [ + "assets", "includes", "lib", "locales", diff --git a/views/layouts/youtube.njk b/views/layouts/youtube.njk new file mode 100644 index 0000000..51ffe0d --- /dev/null +++ b/views/layouts/youtube.njk @@ -0,0 +1,6 @@ +{% extends "document.njk" %} + +{% block content %} + +{% block youtube %}{% endblock %} +{% endblock %} diff --git a/views/youtube.njk b/views/youtube.njk index c098dbe..84e8346 100644 --- a/views/youtube.njk +++ b/views/youtube.njk @@ -1,218 +1,43 @@ -{% extends "document.njk" %} - -{% block content %} - +{% extends "layouts/youtube.njk" %} +{% block youtube %} {% if error %} {{ prose({ text: error.message }) }} {% elif isMultiChannel and channelsData %} {# Multi-channel mode: show all channels #} {% for chData in channelsData %} -
+
{# Channel Header #} {% if chData.error %} -
-
-

{{ chData.name }}

+
+
+

{{ chData.name }}

Error: {{ chData.error }}

{% elif chData.channel %} -
+
{% if chData.channel.thumbnail %} - + {% endif %} -
-

{{ chData.channel.title }}

-
+
+

{{ chData.channel.title }}

+
{{ chData.channel.subscriberCount }} {{ __("youtube.subscribers") }} {{ chData.channel.videoCount }} videos
{% if chData.isLive %} - - + + {{ __("youtube.live") }} {% elif chData.isUpcoming %} - + {{ __("youtube.upcoming") }} {% else %} - + {{ __("youtube.offline") }} {% endif %} @@ -220,14 +45,13 @@ {# Live Stream (if live) #} {% if chData.liveStatus and (chData.liveStatus.isLive or chData.liveStatus.isUpcoming) %} -
-

{% if chData.liveStatus.isLive %}{{ __("youtube.live") }}{% else %}{{ __("youtube.upcoming") }}{% endif %}

-
+ {% call section({ title: chData.liveStatus.isLive and __("youtube.live") or __("youtube.upcoming") }) %} +
{% if chData.liveStatus.thumbnail %} - + {% endif %} -
-

+
+

{{ chData.liveStatus.title }} @@ -239,31 +63,30 @@ }) }}

-
+ {% endcall %} {% endif %} {# Latest Videos #} -
-

{{ __("youtube.videos") }}

+ {% call section({ title: __("youtube.videos") }) %} {% if chData.videos and chData.videos.length > 0 %} -
    +
      {% for video in chData.videos %} -
    • -
      +
    • +
      - + {% if video.durationFormatted and not video.isLive %} - {{ video.durationFormatted }} + {{ video.durationFormatted }} {% elif video.isLive %} - LIVE + LIVE {% endif %}
      -
      -

      +
      +

      {{ video.title }}

      -
      +
      {{ video.viewCount }} {{ __("youtube.views") }}
      @@ -273,10 +96,10 @@ {% else %} {{ prose({ text: __("youtube.noVideos") }) }} {% endif %} -

+ {% endcall %} {# Link to YouTube channel #} -