Files
LEDMatrix/web_interface/templates/v3/partials/plugins.html
T
ChuckandClaude Opus 5 d1e821c625 fix(web): harden, polish and optimize the web UI per the Sept 2026 audit (#568)
* fix(web): harden, polish and optimize the web UI per the September 2026 audit

Works through docs/archive/WEB_UI_AUDIT_2026-09.md (health 8/20).

Implementation integrity (P0)
- app.css now defines every utility class the templates and JS use,
  including .hidden, so the ~145 JS show/hide toggles work. Button reset,
  and base component rules (.btn, .form-control) wrapped in :where() so
  utility classes on the same element win. New static-audit test fails
  when a used utility class has no rule.

Accessibility
- Focus rings render (the old ring rule referenced undefined variables);
  one :focus-visible outline everywhere; skip link; labelled nav landmarks.
- Shared dialog helper (js/utils/dialog.js): role/aria-modal, focus trap,
  Escape, focus return, applied to every modal.
- Named icon-only buttons and labelled ~70 form fields.
- Toasts announced once; errors persist >= 10s; one showNotification.
- Captive WiFi page: live region, timeouts, dark mode, 16px inputs.

Performance (Pi Zero 2 W)
- SSE streams and tab timers pause when hidden or off-tab; the display
  stream only runs while a preview is visible. app-shell.js deferred.
- Widget scripts served as one versioned bundle (/assets/widgets.js):
  52 -> 21 script tags, 66 -> 35 requests on first load.
- Stdlib gzip fallback when flask-compress is missing: first-load JS/CSS
  1358 KB -> 291 KB on the wire. SSE untouched.

Theming and responsive
- File managers, form fields and Fonts upload on theme tokens; bare
  inputs themed in dark mode; no more white surfaces.
- No horizontal overflow at 375px on any tab; 44px touch targets on
  coarse pointers; reduced-motion respected; header title truncates.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(web): clear Codacy findings on #568

- json-file-manager: focus-trap releases kept in a Map (no dynamic
  property access or delete; no value-returning forEach callback)
- notification / schedule-picker: style and day-label lookups via Map
- app.js: move the pending-queue assignment out of the expression
- diff_viewer / error_handler: named function declarations instead of
  arrow consts

No behavior change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: check the OAuth widget ships in the widget bundle

base.html no longer tags widget scripts one by one; they load through
/assets/widgets.js. Assert the page requests the bundle and the bundle
contains google-oauth.js, which is what the test was protecting.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(web): address review feedback on #568

- widget bundle version fingerprints every file (name, mtime_ns, size)
- gzip fallback appends Accept-Encoding to an existing Vary header
- dialog helper: releasing a non-top dialog no longer moves focus out of
  the dialog the user is in
- labels: file-upload targets its file input; fallback config fields get
  label for/id pairs; native color input has a fallback name
- utility audit also reads class names inside bound :class expressions

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(web): give the native color-picker input an accessible name

CodeRabbit flagged this on PR #568 as an outside-diff finding (never
posted inline, so it was missed in the round of fixes that addressed
the other 6 review comments). The <input type="color"> only carried a
title attribute; screen readers don't reliably announce title, and
there's no other label naming the control when showHexInput is false.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(web): clear Codacy findings in app-shell.js

- drop the unused catch binding on the SSE JSON parse
- move the pending-notification queue assignment out of the expression

No behavior change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(web): contain plugin widgets/ dir and bound style-editor retries

From CodeRabbit review on #568 (code that arrived with the main merge):
- serve_plugin_widget resolves widgets/ with resolve_under before
  resolving the manifest script under it, so a symlinked widgets
  directory can't become the containment base (CWE-22). New test.
- style-editor init stops polling after ~10s when the widget never
  registers and leaves the plain fallback fields in place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 09:42:24 -04:00

669 lines
36 KiB
HTML

<div class="bg-white rounded-lg shadow-md p-6" data-plugins-loaded="true">
<div class="section-header">
<h2 class="text-xl font-bold text-gray-900 mb-1">Plugin Management</h2>
<p class="text-sm text-gray-600">Manage installed plugins, configure settings, and browse the plugin store.</p>
</div>
<!-- Plugin Controls -->
<div class="flex flex-wrap items-center justify-between gap-4 mb-6">
<div class="flex flex-wrap items-center gap-2 sm:gap-4">
<button type="button" id="refresh-plugins-btn" class="btn bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md">
<i class="fas fa-sync-alt mr-2"></i>Refresh Plugins
</button>
<button type="button" id="update-all-plugins-btn" class="btn bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md flex items-center">
<i class="fas fa-cloud-download-alt mr-2"></i>Check &amp; Update All
</button>
<button type="button" id="restart-display-btn" class="btn bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md">
<i class="fas fa-redo mr-2"></i>Restart Display
</button>
</div>
</div>
<!-- Plugin Content Area -->
<div class="space-y-8">
<!-- Installed Plugins Section (Always visible at top) -->
<div id="installed-plugins-section" class="mb-8">
<div class="flex items-center justify-between mb-5 pb-3 border-b border-gray-200">
<div class="flex items-center gap-3">
<h3 class="text-lg font-bold text-gray-900">Installed Plugins</h3>
<span id="installed-count" class="text-sm text-gray-500 font-medium">0 installed</span>
</div>
</div>
<!-- Search / Filter / Sort Bar -->
<div id="installed-filter-bar" class="flex flex-wrap items-center gap-3 mb-4 p-3 bg-gray-50 rounded-lg border border-gray-200">
<!-- Search -->
<div class="relative flex-1 min-w-[12rem]">
<i class="fas fa-search absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 text-sm pointer-events-none" aria-hidden="true"></i>
<input type="text"
id="installed-search"
class="form-control text-sm pl-9 pr-8 py-2"
placeholder="Search installed plugins&hellip;"
autocomplete="off"
aria-label="Search installed plugins">
<button type="button" id="installed-search-clear"
class="hidden absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
aria-label="Clear search">
<i class="fas fa-times-circle" aria-hidden="true"></i>
</button>
</div>
<!-- Status filter pills -->
<div id="installed-filter-pills" class="flex items-center gap-1.5" role="group" aria-label="Filter installed plugins by status">
<button type="button" class="filter-pill text-sm px-3 py-1.5 rounded-md border border-gray-300 bg-white"
data-installed-filter="all" data-active="true" aria-pressed="true">All</button>
<button type="button" class="filter-pill text-sm px-3 py-1.5 rounded-md border border-gray-300 bg-white"
data-installed-filter="enabled" data-active="false" aria-pressed="false">Enabled</button>
<button type="button" class="filter-pill text-sm px-3 py-1.5 rounded-md border border-gray-300 bg-white"
data-installed-filter="disabled" data-active="false" aria-pressed="false">Disabled</button>
<button type="button" class="filter-pill text-sm px-3 py-1.5 rounded-md border border-gray-300 bg-white"
data-installed-filter="updates" data-active="false" aria-pressed="false"
title="Show only plugins with a newer version available">
Updates<span id="installed-updates-count" class="badge badge-info ml-1.5 hidden">0</span>
</button>
</div>
<!-- Sort -->
<select id="installed-sort" class="text-sm px-3 py-1.5 border border-gray-300 rounded-md bg-white" aria-label="Sort installed plugins">
<option value="a-z">A &rarr; Z</option>
<option value="z-a">Z &rarr; A</option>
<option value="status">Updates first</option>
<option value="recent">Recently updated</option>
<option value="category">Category</option>
</select>
<div class="flex-1"></div>
<!-- Clear -->
<button type="button" id="installed-clear-filters"
class="hidden text-sm px-3 py-1.5 rounded-md border border-red-300 bg-white text-red-600 hover:bg-red-50 transition-colors">
<i class="fas fa-times mr-1"></i>Clear
</button>
</div>
<div id="installed-plugins-content" class="block">
<div id="installed-plugins-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6">
<!-- Skeleton cards shown while installed plugins load -->
<div class="installed-skeleton plugin-card animate-pulse">
<div class="flex items-start justify-between mb-4">
<div class="flex-1 min-w-0 space-y-2">
<div class="h-4 bg-gray-200 rounded w-3/4"></div>
<div class="h-3 bg-gray-200 rounded w-1/2"></div>
<div class="h-3 bg-gray-200 rounded w-2/3"></div>
</div>
<div class="h-6 w-10 bg-gray-200 rounded-full ml-4 flex-shrink-0"></div>
</div>
<div class="space-y-2 mb-4">
<div class="h-3 bg-gray-200 rounded w-full"></div>
<div class="h-3 bg-gray-200 rounded w-5/6"></div>
</div>
<div class="h-8 bg-gray-200 rounded w-full mt-auto"></div>
</div>
<div class="installed-skeleton plugin-card animate-pulse hidden md:block">
<div class="flex items-start justify-between mb-4">
<div class="flex-1 min-w-0 space-y-2">
<div class="h-4 bg-gray-200 rounded w-2/3"></div>
<div class="h-3 bg-gray-200 rounded w-1/3"></div>
<div class="h-3 bg-gray-200 rounded w-1/2"></div>
</div>
<div class="h-6 w-10 bg-gray-200 rounded-full ml-4 flex-shrink-0"></div>
</div>
<div class="space-y-2 mb-4">
<div class="h-3 bg-gray-200 rounded w-full"></div>
<div class="h-3 bg-gray-200 rounded w-4/5"></div>
</div>
<div class="h-8 bg-gray-200 rounded w-full mt-auto"></div>
</div>
<div class="installed-skeleton plugin-card animate-pulse hidden lg:block">
<div class="flex items-start justify-between mb-4">
<div class="flex-1 min-w-0 space-y-2">
<div class="h-4 bg-gray-200 rounded w-4/5"></div>
<div class="h-3 bg-gray-200 rounded w-2/5"></div>
<div class="h-3 bg-gray-200 rounded w-3/5"></div>
</div>
<div class="h-6 w-10 bg-gray-200 rounded-full ml-4 flex-shrink-0"></div>
</div>
<div class="space-y-2 mb-4">
<div class="h-3 bg-gray-200 rounded w-full"></div>
<div class="h-3 bg-gray-200 rounded w-3/4"></div>
</div>
<div class="h-8 bg-gray-200 rounded w-full mt-auto"></div>
</div>
</div>
</div>
</div>
<!-- Plugin Store Section (Always visible at bottom) -->
<div id="plugin-store-section" class="border-t border-gray-200 pt-8 mt-8">
<div class="flex items-center justify-between mb-5 pb-3 border-b border-gray-200">
<div class="flex items-center gap-3">
<h3 class="text-lg font-bold text-gray-900">Plugin Store</h3>
<span id="store-count" class="text-sm text-gray-500 font-medium">
<i class="fas fa-spinner fa-spin mr-1"></i>Loading...
</span>
</div>
</div>
<div id="plugin-store-content" class="block">
<!-- GitHub Token Configuration (Combined Warning + Settings) -->
<div id="github-token-container" class="mb-5">
<!-- Warning Banner (shown when no token configured) -->
<div id="github-auth-warning" class="hidden bg-yellow-50 border border-yellow-300 p-4 rounded-lg">
<div class="flex">
<div class="flex-shrink-0">
<i class="fas fa-exclamation-triangle text-yellow-400"></i>
</div>
<div class="ml-3 flex-1">
<p class="text-sm text-yellow-700">
<strong>Limited API Access:</strong> GitHub API requests are limited to <span id="rate-limit-count">60</span> per hour without authentication.
Add a GitHub token to increase this to 5,000 requests/hour and get real-time plugin stats.
</p>
<p class="mt-2 text-sm text-yellow-700">
<a href="https://github.com/settings/tokens/new?description=LEDMatrix%20Plugin%20Manager&scopes=" target="_blank" class="font-medium underline hover:text-yellow-800">
Create a GitHub Token →
</a>
<span class="mx-2">|</span>
<a href="#" onclick="event.preventDefault(); openGithubTokenSettings()" class="font-medium underline hover:text-yellow-800">
Configure Token
</a>
</p>
</div>
<div class="flex-shrink-0">
<button type="button" onclick="dismissGithubWarning()" class="text-yellow-700 hover:text-yellow-900" aria-label="Dismiss GitHub token warning">
<i class="fas fa-times" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<!-- Settings Panel (expandable configuration form) -->
<div id="github-token-settings" class="hidden bg-blue-50 border border-blue-200 rounded-lg p-5 shadow-sm mt-3">
<div class="flex items-start justify-between mb-4">
<div class="flex items-center space-x-2">
<i class="fab fa-github text-blue-600 text-xl"></i>
<h4 class="font-bold text-gray-900">GitHub API Configuration</h4>
</div>
<div class="flex items-center gap-2">
<button id="toggle-github-token-collapse" class="text-gray-600 hover:text-gray-900 text-sm flex items-center font-medium transition-colors">
<i class="fas fa-chevron-up mr-1" id="github-token-icon-collapse"></i>
<span>Collapse</span>
</button>
</div>
</div>
<div id="github-token-content" class="block">
<p class="text-sm text-gray-600 mb-3">
Configure your GitHub Personal Access Token to increase API rate limits and get real-time plugin statistics.
</p>
<div class="space-y-3">
<div>
<label for="github-token-input" class="block text-sm font-medium text-gray-700 mb-1">
GitHub Personal Access Token
</label>
<div class="relative">
<input type="password" id="github-token-input"
class="w-full px-3 py-2 pr-20 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500"
placeholder="ghp_xxxxxxxxxxxxxxxxxxxx">
<button type="button" onclick="toggleGithubTokenVisibility()"
class="absolute inset-y-0 right-0 pr-3 flex items-center text-gray-500 hover:text-gray-700"
aria-label="Show token" aria-controls="github-token-input">
<i id="github-token-icon" class="fas fa-eye" aria-hidden="true"></i>
</button>
</div>
<p class="text-xs text-gray-500 mt-1">
Token is stored in config_secrets.json. No scopes required for public repositories.
</p>
</div>
<div class="flex items-center justify-between">
<a href="https://github.com/settings/tokens/new?description=LEDMatrix%20Plugin%20Manager&scopes="
target="_blank"
class="text-sm text-blue-600 hover:text-blue-800">
<i class="fas fa-external-link-alt mr-1"></i>Create Token on GitHub
</a>
<div class="flex gap-2">
<button onclick="loadGithubToken()" class="px-3 py-1.5 text-sm bg-gray-600 hover:bg-gray-700 text-white rounded-md">
<i class="fas fa-sync mr-1"></i>Load Current
</button>
<button onclick="saveGithubToken()" class="px-4 py-1.5 text-sm bg-blue-600 hover:bg-blue-700 text-white rounded-md">
<i class="fas fa-save mr-1"></i>Save Token
</button>
</div>
</div>
<div class="bg-white border border-blue-200 rounded p-3">
<p class="text-xs text-gray-600">
<strong>Rate Limits:</strong><br>
Without token: 60 requests/hour<br>
With token: 5,000 requests/hour
</p>
</div>
</div>
</div>
</div>
</div>
<!-- Search Row -->
<div class="flex gap-3 mb-4">
<input type="text" id="plugin-search" placeholder="Search plugins by name, description, or tags..." aria-label="Search the Plugin Store" class="form-control text-sm flex-[3] min-w-0 px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:shadow-md transition-shadow">
<select id="plugin-category" aria-label="Filter the Plugin Store by category" class="form-control text-sm flex-1 px-3 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:shadow-md transition-shadow">
<option value="">All Categories</option>
<option value="sports">Sports</option>
<option value="content">Content</option>
<option value="time">Time</option>
<option value="weather">Weather</option>
<option value="financial">Financial</option>
<option value="media">Media</option>
<option value="demo">Demo</option>
</select>
</div>
<!-- Sort & Filter Bar -->
<div id="store-filter-bar" class="flex flex-wrap items-center gap-3 mb-4 p-3 bg-gray-50 rounded-lg border border-gray-200">
<!-- Sort -->
<select id="store-sort" aria-label="Sort the Plugin Store" class="text-sm px-3 py-1.5 border border-gray-300 rounded-md bg-white">
<option value="a-z">A → Z</option>
<option value="z-a">Z → A</option>
<option value="category">Category</option>
<option value="author">Author</option>
<option value="newest">Newest</option>
</select>
<div class="w-px h-6 bg-gray-300"></div>
<!-- Installed filter toggle -->
<button id="store-filter-installed" class="text-sm px-3 py-1.5 rounded-md border border-gray-300 bg-white hover:bg-gray-100 transition-colors" title="Cycle: All → Installed → Not Installed">
<i class="fas fa-filter mr-1 text-gray-400"></i>All
</button>
<div class="flex-1"></div>
<!-- Active filter count + clear -->
<span id="store-active-filters" class="hidden text-xs text-blue-600 font-medium"></span>
<button id="store-clear-filters" class="hidden text-sm px-3 py-1.5 rounded-md border border-red-300 bg-white text-red-600 hover:bg-red-50 transition-colors">
<i class="fas fa-times mr-1"></i>Clear Filters
</button>
</div>
<!-- Results Bar (top pagination) -->
<div class="flex flex-wrap items-center justify-between gap-3 mb-4">
<span id="store-results-info" class="text-sm text-gray-600"></span>
<div class="flex flex-wrap items-center gap-3">
<select id="store-per-page" aria-label="Plugins per page" class="text-sm px-2 py-1 border border-gray-300 rounded-md bg-white">
<option value="12">12 per page</option>
<option value="24">24 per page</option>
<option value="48">48 per page</option>
</select>
<div id="store-pagination-top" class="flex flex-wrap items-center gap-1"></div>
</div>
</div>
<div id="plugin-store-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6">
<!-- Loading skeleton — hidden by showStoreLoading(false) when data arrives -->
<div class="store-loading col-span-full">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6">
{% for _ in range(10) %}
<div class="plugin-card animate-pulse">
<div class="flex items-start justify-between mb-4">
<div class="flex-1 min-w-0 space-y-2">
<div class="h-4 bg-gray-200 rounded w-3/4"></div>
<div class="h-3 bg-gray-200 rounded w-1/2"></div>
</div>
<div class="h-5 w-14 bg-gray-200 rounded-full ml-3 flex-shrink-0"></div>
</div>
<div class="space-y-2 mb-4">
<div class="h-3 bg-gray-200 rounded w-full"></div>
<div class="h-3 bg-gray-200 rounded w-5/6"></div>
<div class="h-3 bg-gray-200 rounded w-4/6"></div>
</div>
<div class="flex gap-2 mt-auto">
<div class="h-3 bg-gray-200 rounded w-12"></div>
<div class="h-3 bg-gray-200 rounded w-16"></div>
</div>
<div class="h-8 bg-gray-200 rounded w-full mt-3"></div>
</div>
{% endfor %}
</div>
</div>
</div>
<!-- Bottom Pagination -->
<div class="flex flex-wrap items-center justify-between gap-3 mt-4">
<span id="store-results-info-bottom" class="text-sm text-gray-600"></span>
<div id="store-pagination-bottom" class="flex items-center gap-1"></div>
</div>
</div>
<!-- Starlark Apps Section (Tronbyte Community Apps) -->
<div id="starlark-apps-section" class="border-t border-gray-200 pt-8 mt-8">
<div class="flex items-center justify-between mb-5 pb-3 border-b border-gray-200">
<div class="flex items-center gap-3">
<h3 class="text-lg font-bold text-gray-900"><i class="fas fa-star text-yellow-500 mr-2"></i>Starlark Apps</h3>
<span id="starlark-apps-count" class="text-sm text-gray-500 font-medium"></span>
</div>
<button id="toggle-starlark-section" class="text-sm text-blue-600 hover:text-blue-800 flex items-center font-medium transition-colors">
<i class="fas fa-chevron-down mr-1" id="starlark-section-icon"></i>
<span>Show</span>
</button>
</div>
<div id="starlark-section-content" class="hidden">
<p class="text-sm text-gray-600 mb-4">Browse and install Starlark apps from the <a href="https://github.com/tronbyt/apps" target="_blank" class="text-blue-600 hover:text-blue-800 underline">Tronbyte community repository</a>. Requires <strong>Pixlet</strong> binary.</p>
<!-- Pixlet Status Banner -->
<div id="starlark-pixlet-status" class="mb-4"></div>
<!-- Search Row -->
<div class="flex gap-3 mb-4">
<input type="text" id="starlark-search" placeholder="Search by name, description, author..." aria-label="Search Starlark apps" class="form-control text-sm flex-[3] min-w-0 px-4 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:shadow-md transition-shadow">
<select id="starlark-category" aria-label="Filter Starlark apps by category" class="form-control text-sm flex-1 px-3 py-2.5 border border-gray-300 rounded-lg shadow-sm focus:shadow-md transition-shadow">
<option value="">All Categories</option>
</select>
</div>
<!-- Sort & Filter Bar -->
<div id="starlark-filter-bar" class="flex flex-wrap items-center gap-3 mb-4 p-3 bg-gray-50 rounded-lg border border-gray-200">
<!-- Sort -->
<select id="starlark-sort" aria-label="Sort Starlark apps" class="text-sm px-3 py-1.5 border border-gray-300 rounded-md bg-white">
<option value="a-z">A → Z</option>
<option value="z-a">Z → A</option>
<option value="category">Category</option>
<option value="author">Author</option>
</select>
<div class="w-px h-6 bg-gray-300"></div>
<!-- Installed filter toggle -->
<button id="starlark-filter-installed" class="text-sm px-3 py-1.5 rounded-md border border-gray-300 bg-white hover:bg-gray-100 transition-colors" title="Cycle: All → Installed → Not Installed">
<i class="fas fa-filter mr-1 text-gray-400"></i>All
</button>
<!-- Author filter -->
<select id="starlark-filter-author" aria-label="Filter Starlark apps by author" class="text-sm px-3 py-1.5 border border-gray-300 rounded-md bg-white">
<option value="">All Authors</option>
</select>
<div class="flex-1"></div>
<!-- Active filter count + clear -->
<span id="starlark-active-filters" class="hidden text-xs text-blue-600 font-medium"></span>
<button id="starlark-clear-filters" class="hidden text-sm px-3 py-1.5 rounded-md border border-red-300 bg-white text-red-600 hover:bg-red-50 transition-colors">
<i class="fas fa-times mr-1"></i>Clear Filters
</button>
</div>
<!-- Results Bar (top pagination) -->
<div class="flex flex-wrap items-center justify-between gap-3 mb-4">
<span id="starlark-results-info" class="text-sm text-gray-600"></span>
<div class="flex flex-wrap items-center gap-3">
<select id="starlark-per-page" aria-label="Starlark apps per page" class="text-sm px-2 py-1 border border-gray-300 rounded-md bg-white">
<option value="24">24 per page</option>
<option value="48">48 per page</option>
<option value="96">96 per page</option>
</select>
<div id="starlark-pagination-top" class="flex flex-wrap items-center gap-1"></div>
</div>
</div>
<!-- Starlark Apps Grid -->
<div id="starlark-apps-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6">
</div>
<!-- Bottom Pagination -->
<div class="flex flex-wrap items-center justify-between gap-3 mt-4">
<span id="starlark-results-info-bottom" class="text-sm text-gray-600"></span>
<div id="starlark-pagination-bottom" class="flex items-center gap-1"></div>
</div>
<!-- Upload .star file -->
<div class="flex gap-3 mt-6 pt-4 border-t border-gray-200">
<button id="starlark-upload-btn" class="btn bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md text-sm">
<i class="fas fa-upload mr-2"></i>Upload .star File
</button>
</div>
</div>
</div>
<!-- Install from GitHub URL Section (Separate section, always visible) -->
<div class="border-t border-gray-200 pt-8 mt-8">
<div class="flex items-center justify-between mb-5 pb-3 border-b border-gray-200">
<div>
<h3 class="text-lg font-bold text-gray-900">Install from GitHub</h3>
<p class="text-sm text-gray-600 mt-1">Install plugins directly from GitHub repositories</p>
</div>
<button id="toggle-github-install" class="text-sm text-blue-600 hover:text-blue-800 flex items-center font-medium transition-colors">
<i class="fas fa-chevron-down mr-1" id="github-install-icon"></i>
<span>Show</span>
</button>
</div>
<div id="github-install-section" class="hidden space-y-4">
<!-- Saved Repositories Section -->
<div class="bg-blue-50 rounded-lg p-5 border border-blue-200 shadow-sm">
<div class="flex items-center justify-between mb-4">
<h4 class="text-base font-bold text-gray-900">
<i class="fas fa-bookmark mr-2 text-blue-600"></i>Saved Repositories
</h4>
<span id="saved-repos-count" class="text-xs text-gray-600 font-medium">0 saved</span>
</div>
<p class="text-xs text-gray-600 mb-3">Saved repositories are automatically loaded and their plugins appear in the Plugin Store above.</p>
<div id="saved-repositories-list" class="space-y-2 mb-3">
<!-- Saved repositories will be loaded here -->
</div>
<button id="refresh-saved-repos" class="text-xs text-blue-600 hover:text-blue-800">
<i class="fas fa-sync mr-1"></i>Refresh
</button>
</div>
<!-- Direct Plugin Installation -->
<div class="bg-gray-50 rounded-lg p-5 border border-gray-200 shadow-sm">
<h4 class="text-base font-bold text-gray-900 mb-3">
<i class="fas fa-code-branch mr-2 text-blue-600"></i>Install Single Plugin
</h4>
<p class="text-xs text-gray-600 mb-3">Install a plugin directly from its GitHub repository URL</p>
<div class="space-y-2">
<div class="flex gap-2">
<input type="text" id="github-plugin-url" aria-label="Plugin GitHub repository URL"
placeholder="https://github.com/user/ledmatrix-plugin-name"
class="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
<button type="button" id="install-plugin-from-url"
onclick="if(window.handleGitHubPluginInstall){window.handleGitHubPluginInstall()}else{alert('Function not loaded yet, please refresh the page')}"
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-md whitespace-nowrap">
<i class="fas fa-download mr-2"></i>Install
</button>
</div>
<div class="flex items-center gap-2">
<label for="plugin-branch-input" class="text-xs text-gray-600 whitespace-nowrap">
<i class="fas fa-code-branch mr-1"></i>Branch (optional):
</label>
<input type="text" id="plugin-branch-input"
placeholder="main, test, etc. (default: main)"
class="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
</div>
</div>
<div id="github-plugin-status" class="mt-2 text-sm"></div>
</div>
<!-- Registry-Style Monorepo Installation -->
<div class="bg-gray-50 rounded-lg p-5 border border-gray-200 shadow-sm">
<h4 class="text-base font-bold text-gray-900 mb-3">
<i class="fas fa-folder-open mr-2 text-green-600"></i>Browse Plugin Registry
</h4>
<p class="text-xs text-gray-600 mb-3">Load a registry-style monorepo (like the official ledmatrix-plugins repo) to browse and install plugins</p>
<div class="flex gap-2 mb-2">
<input type="text" id="github-registry-url" aria-label="Plugin registry GitHub URL"
placeholder="https://github.com/user/ledmatrix-plugins"
class="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500">
<button id="load-registry-from-url" class="px-4 py-2 bg-green-600 hover:bg-green-700 text-white text-sm rounded-md whitespace-nowrap">
<i class="fas fa-search mr-2"></i>Load Registry
</button>
</div>
<div class="flex gap-2 mb-3">
<button id="save-registry-url" class="px-3 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm rounded-md whitespace-nowrap">
<i class="fas fa-bookmark mr-2"></i>Save Repository
</button>
<div id="registry-status" class="flex-1 text-sm"></div>
</div>
<div id="custom-registry-plugins" class="hidden">
<div class="border-t border-gray-300 pt-3 mt-3">
<p class="text-xs font-medium text-gray-700 mb-2">Available Plugins:</p>
<div id="custom-registry-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<!-- Custom registry plugins will be loaded here -->
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- On-Demand Modal moved to base.html so it's always available -->
<style>
/* View toggle button styles */
.view-toggle-btn {
transition: all 0.2s ease;
}
.view-toggle-btn.active {
background-color: #2563eb;
color: white;
}
.view-toggle-btn:not(.active) {
color: #374151;
}
.view-toggle-btn:not(.active):hover {
background-color: #e5e7eb;
}
/* CodeMirror editor styles */
.CodeMirror {
border: 1px solid #d1d5db;
border-radius: 0.375rem;
font-size: 14px;
height: auto;
min-height: 400px;
}
.CodeMirror.cm-error {
border-color: #ef4444;
}
/* Plugin config view styles */
.plugin-config-view {
transition: opacity 0.2s ease;
}
/* Nested config section styles */
.nested-section {
position: relative;
margin-bottom: 1.5rem;
transition: all 0.2s ease;
z-index: 1;
clear: both;
/* Contain content but allow expansion */
overflow: visible;
/* Ensure proper stacking context */
isolation: isolate;
}
.nested-section button {
position: relative;
z-index: 2;
}
.nested-section button:hover {
background-color: #f3f4f6;
}
.nested-section .nested-content {
position: relative;
transition: max-height 0.3s ease, opacity 0.3s ease;
overflow: hidden;
z-index: 1;
/* Ensure content doesn't get clipped by parent */
min-height: 0;
/* Contain content properly */
contain: layout style;
}
/* When expanded, allow content to flow naturally */
.nested-content.expanded {
overflow: visible;
/* Ensure expanded content is fully visible */
min-height: auto;
}
.nested-section i {
transition: transform 0.3s ease;
}
/* Smooth toggle animation */
.nested-content.collapsed {
opacity: 0;
max-height: 0;
overflow: hidden;
}
.nested-content.expanded {
opacity: 1;
max-height: none !important; /* Remove height constraint to allow natural expansion */
padding-bottom: 1rem !important; /* Ensure proper padding at bottom to prevent cutoff */
overflow: visible; /* Allow content to flow naturally when expanded */
margin-bottom: 0.5rem; /* Add spacing at bottom when expanded */
}
/* Nested sections within nested sections - add indentation and spacing */
.nested-content .nested-section {
margin-left: 1rem;
margin-bottom: 1.5rem; /* Increased spacing to prevent overlap */
margin-top: 0.5rem;
}
/* Deeply nested sections need even more spacing */
.nested-content .nested-content .nested-section {
margin-bottom: 2rem;
margin-top: 0.5rem;
}
/* Form group spacing within nested sections */
.nested-content .form-group {
margin-bottom: 0.75rem;
}
/* Ensure form-groups that come after nested sections have proper spacing */
.nested-section + .form-group {
margin-top: 1.5rem !important;
position: relative;
z-index: 0;
clear: both;
/* Ensure form-group doesn't overlap */
display: block;
width: 100%;
/* Ensure it's in normal document flow */
float: none;
}
/* Ensure any content after nested sections is properly spaced */
.nested-section ~ .form-group {
clear: both !important;
position: relative;
z-index: 0;
display: block;
/* Prevent overlap */
margin-top: 1.5rem !important;
width: 100%;
float: none;
}
/* Make nested section headers slightly smaller for hierarchy */
.nested-content .nested-section h4 {
font-size: 0.95em;
}
</style>