Add asset pipeline and professionalize theme
CI / build (push) Failing after 6m2s

Introduce a Node build system for minified CSS/JS with content hashes,
add PWA support, i18n helpers, accessibility improvements, and more
translatable strings. Move theme configuration from bookmarks to
localStorage, add Playwright tests, and set up CI workflows.
This commit is contained in:
2026-08-09 09:25:03 -04:00
parent 10af62dd6c
commit f753b27542
68 changed files with 4743 additions and 1989 deletions
+98 -96
View File
@@ -5,8 +5,10 @@ Bookmarklet detection logic
{ignore} When called as a popup from bookmarklet, do not display menu. {/ignore}
{else}
<a href="#content-container" class="skip-link">{'Skip to content'|t}</a>
<!-- Sidebar -->
<aside class="sidebar" id="sidebar" aria-label="Navigation principale">
<aside class="sidebar" id="sidebar" aria-label="{'Main navigation'|t}">
<div class="sidebar-header">
<div class="sidebar-logo" aria-hidden="true">
<i class="mdi mdi-bookmark-multiple"></i>
@@ -14,27 +16,27 @@ Bookmarklet detection logic
<a href="{$titleLink}" class="sidebar-brand">{$shaarlititle}</a>
</div>
<nav class="sidebar-nav" aria-label="Menu latéral">
<nav class="sidebar-nav" aria-label="{'Main navigation'|t}">
<div class="sidebar-section">
<div class="sidebar-section-title">Recherche</div>
<div class="sidebar-section-title">{'Search'|t}</div>
<button type="button" class="sidebar-link sidebar-search-trigger" data-search-toggle
aria-label="Ouvrir la recherche">
aria-label="{'Open search'|t}">
<i class="mdi mdi-magnify" aria-hidden="true"></i>
<span>Rechercher</span>
<span>{'Search'|t}</span>
</button>
</div>
{if="$is_logged_in"}
<div class="sidebar-section sidebar-add-section">
<div class="sidebar-section-title">Création rapide</div>
<div class="sidebar-section-title">{'Quick create'|t}</div>
<div class="sidebar-add-segmented">
<a href="{$base_path}/admin/add-shaare" class="sidebar-add-segment" title="Bookmark">
<a href="{$base_path}/admin/add-shaare" class="sidebar-add-segment" title="{'Bookmark'|t}">
<i class="mdi mdi-bookmark-outline"></i>
</a>
<a href="#" class="sidebar-add-segment sidebar-add-note" title="Note" data-base-path="{$base_path}">
<a href="#" class="sidebar-add-segment sidebar-add-note" title="{'Note'|t}" data-base-path="{$base_path}">
<i class="mdi mdi-note-text-outline"></i>
</a>
<a href="#" class="sidebar-add-segment sidebar-add-todo" title="Todo" data-base-path="{$base_path}">
<a href="#" class="sidebar-add-segment sidebar-add-todo" title="{'Todo'|t}" data-base-path="{$base_path}">
<i class="mdi mdi-check-circle-outline"></i>
</a>
</div>
@@ -42,60 +44,60 @@ Bookmarklet detection logic
{/if}
<div class="sidebar-section">
<div class="sidebar-section-title">Navigation</div>
<div class="sidebar-section-title">{'Navigation'|t}</div>
<a href="{$base_path}/" class="sidebar-link{if="$pageName=='linklist' && empty($search_tags)"} active{/if}"
aria-label="Tous les bookmarks">
aria-label="{'All bookmarks'|t}">
<i class="mdi mdi-bookmark-multiple-outline" aria-hidden="true"></i>
<span>Tous les bookmarks</span>
<span>{'All bookmarks'|t}</span>
</a>
<a href="{$base_path}/?searchtags=shaarli-pin" class="sidebar-link{if="isset($search_tags) && preg_match('/(^|[\s,])shaarli-pin([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="Épinglés">
<a href="{$base_path}/?searchtags=shaarli-pin" class="sidebar-link{if="isset($search_tags) && preg_match('/(^|[\s,])shaarli-pin([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="{'Pinned'|t}">
<i class="mdi mdi-pin-outline" aria-hidden="true"></i>
<span>Épinglés</span>
<span>{'Pinned'|t}</span>
</a>
<a href="{$base_path}/tags/cloud" class="sidebar-link{if="$pageName=='tag.cloud' || $pageName=='tag.list'"} active{/if}"
aria-label="Nuage de tags">
aria-label="{'Tag cloud'|t}">
<i class="mdi mdi-tag-multiple-outline" aria-hidden="true"></i>
<span>Nuage de tags</span>
<span>{'Tag cloud'|t}</span>
</a>
<a href="{$base_path}/picture-wall?{function="ltrim($searchcrits, '&' )"}" class="sidebar-link{if="$pageName=='picwall'"} active{/if}" aria-label="Mur d'images">
<a href="{$base_path}/picture-wall?{function="ltrim($searchcrits, '&' )"}" class="sidebar-link{if="$pageName=='picwall'"} active{/if}" aria-label="{'Picture wall'|t}">
<i class="mdi mdi-image-multiple-outline" aria-hidden="true"></i>
<span>Mur d'images</span>
<span>{'Picture wall'|t}</span>
</a>
<a href="{$base_path}/daily" class="sidebar-link{if="$pageName=='daily'"} active{/if}"
aria-label="Vue quotidienne">
aria-label="{'Daily'|t}">
<i class="mdi mdi-calendar-today" aria-hidden="true"></i>
<span>Quotidien</span>
<span>{'Daily'|t}</span>
</a>
<a href="{$base_path}/?searchtags=todo" class="sidebar-link{if="isset($search_tags) && preg_match('/(^|[\s,])todo([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="Mes tâches">
<a href="{$base_path}/?searchtags=todo" class="sidebar-link{if="isset($search_tags) && preg_match('/(^|[\s,])todo([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="{'My tasks'|t}">
<i class="mdi mdi-check-circle-outline" aria-hidden="true"></i>
<span>Mes tâches</span>
<span>{'My tasks'|t}</span>
</a>
<a href="{$base_path}/?searchtags=note" class="sidebar-link{if="isset($search_tags) && preg_match('/(^|[\s,])note([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="Notes">
<a href="{$base_path}/?searchtags=note" class="sidebar-link{if="isset($search_tags) && preg_match('/(^|[\s,])note([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="{'Notes'|t}">
<i class="mdi mdi-note-text-outline" aria-hidden="true"></i>
<span>Notes</span>
<span>{'Notes'|t}</span>
</a>
<a href="{$base_path}/?searchtags=shaarli-archive" class="sidebar-link{if="isset($search_tags) && preg_match('/(^|[\s,])shaarli-archive([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="Archive">
<a href="{$base_path}/?searchtags=shaarli-archive" class="sidebar-link{if="isset($search_tags) && preg_match('/(^|[\s,])shaarli-archive([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="{'Archive'|t}">
<i class="mdi mdi-archive-arrow-down-outline" aria-hidden="true"></i>
<span>Archive</span>
<span>{'Archive'|t}</span>
</a>
<a href="{$base_path}/?searchtags=readitlater" class="sidebar-link{if="isset($search_tags) && preg_match('/(^|[\s,])readitlater([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="Read It Later">
<a href="{$base_path}/?searchtags=readitlater" class="sidebar-link{if="isset($search_tags) && preg_match('/(^|[\s,])readitlater([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="{'Read It Later'|t}">
<i class="mdi mdi-bookmark-outline" aria-hidden="true"></i>
<span>Read It Later</span>
<span>{'Read It Later'|t}</span>
</a>
</div>
{if="$is_logged_in"}
<div class="sidebar-section">
<div class="sidebar-section-title">Administration</div>
<div class="sidebar-section-title">{'Administration'|t}</div>
<a href="{$base_path}/admin/tools" class="sidebar-link{if="$pageName=='tools'"} active{/if}"
aria-label="Outils">
aria-label="{'Tools'|t}">
<i class="mdi mdi-tools" aria-hidden="true"></i>
<span>Outils</span>
<span>{'Tools'|t}</span>
</a>
<a href="{$base_path}/admin/configure" class="sidebar-link{if="$pageName=='configure'"} active{/if}"
aria-label="Paramètres">
aria-label="{'Settings'|t}">
<i class="mdi mdi-cog-outline" aria-hidden="true"></i>
<span>Paramètres</span>
<span>{'Settings'|t}</span>
</a>
</div>
{/if}
@@ -105,10 +107,10 @@ Bookmarklet detection logic
<div class="theme-toggle-wrapper">
<span class="theme-toggle-label">
<i class="mdi mdi-weather-sunny" id="theme-icon-light" aria-hidden="true"></i>
<span>Mode clair</span>
<span>{'Light mode'|t}</span>
</span>
<label class="theme-switch" aria-label="Basculer le mode sombre">
<input type="checkbox" id="theme-toggle-checkbox" aria-label="Activer/désactiver le mode sombre">
<label class="theme-switch" aria-label="{'Toggle dark mode'|t}">
<input type="checkbox" id="theme-toggle-checkbox" aria-label="{'Toggle dark mode'|t}">
<span class="theme-slider"></span>
</label>
</div>
@@ -124,7 +126,7 @@ Bookmarklet detection logic
<header class="header-main">
<div class="header-inner">
<div class="header-left">
<button class="mobile-menu-btn" id="mobile-menu-btn" aria-label="Ouvrir le menu" aria-expanded="false"
<button class="mobile-menu-btn" id="mobile-menu-btn" aria-label="{'Open menu'|t}" aria-expanded="false"
aria-controls="sidebar">
<i class="mdi mdi-menu" aria-hidden="true"></i>
</button>
@@ -132,50 +134,50 @@ Bookmarklet detection logic
</div>
<!-- Main Navigation -->
<nav class="header-nav" aria-label="Navigation principale">
<nav class="header-nav" aria-label="{'Main navigation'|t}">
<a href="{$base_path}/" class="header-nav-link{if="$pageName=='linklist' && empty($search_tags)"} active{/if}"
aria-label="Accueil">
aria-label="{'Home'|t}">
<i class="mdi mdi-home-outline" aria-hidden="true"></i>
<span>ACCUEIL</span>
<span>{'Home'|t}</span>
</a>
<a href="{$base_path}/?searchtags=shaarli-pin" class="header-nav-link{if="isset($search_tags) && preg_match('/(^|[\s,])shaarli-pin([\s,]|$)/i', (string) $search_tags)"} active{/if}"
aria-label="Épinglés">
aria-label="{'Pinned'|t}">
<i class="mdi mdi-pin-outline" aria-hidden="true"></i>
<span>ÉPINGLÉS</span>
<span>{'Pinned'|t}</span>
</a>
<a href="{$base_path}/tags/cloud" class="header-nav-link{if="$pageName=='tag.cloud' || $pageName=='tag.list'"} active{/if}"
aria-label="Nuage de tags">
aria-label="{'Tag cloud'|t}">
<i class="mdi mdi-tag-multiple" aria-hidden="true"></i>
<span>TAGS</span>
<span>{'Tag cloud'|t}</span>
</a>
<a href="{$base_path}/picture-wall?{function="ltrim($searchcrits, '&' )"}" class="header-nav-link{if="$pageName=='picwall'"} active{/if}" aria-label="Mur d'images">
<a href="{$base_path}/picture-wall?{function="ltrim($searchcrits, '&' )"}" class="header-nav-link{if="$pageName=='picwall'"} active{/if}" aria-label="{'Picture wall'|t}">
<i class="mdi mdi-image-multiple" aria-hidden="true"></i>
<span>IMAGES</span>
<span>{'Picture wall'|t}</span>
</a>
<a href="{$base_path}/daily" class="header-nav-link{if="$pageName=='daily'"} active{/if}"
aria-label="Vue quotidienne">
aria-label="{'Daily'|t}">
<i class="mdi mdi-calendar" aria-hidden="true"></i>
<span>QUOTIDIEN</span>
<span>{'Daily'|t}</span>
</a>
<a href="{$base_path}/?searchtags=todo" class="header-nav-link{if="isset($search_tags) && preg_match('/(^|[\s,])todo([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="Mes tâches">
<a href="{$base_path}/?searchtags=todo" class="header-nav-link{if="isset($search_tags) && preg_match('/(^|[\s,])todo([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="{'My tasks'|t}">
<i class="mdi mdi-check-circle-outline" aria-hidden="true"></i>
<span>TÂCHES</span>
<span>{'My tasks'|t}</span>
</a>
<a href="{$base_path}/?searchtags=note" class="header-nav-link{if="isset($search_tags) && preg_match('/(^|[\s,])note([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="Notes">
<a href="{$base_path}/?searchtags=note" class="header-nav-link{if="isset($search_tags) && preg_match('/(^|[\s,])note([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="{'Notes'|t}">
<i class="mdi mdi-note-text-outline" aria-hidden="true"></i>
<span>NOTES</span>
<span>{'Notes'|t}</span>
</a>
<a href="{$base_path}/?searchtags=shaarli-archive" class="header-nav-link{if="isset($search_tags) && preg_match('/(^|[\s,])shaarli-archive([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="Archive">
<a href="{$base_path}/?searchtags=shaarli-archive" class="header-nav-link{if="isset($search_tags) && preg_match('/(^|[\s,])shaarli-archive([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="{'Archive'|t}">
<i class="mdi mdi-archive-arrow-down-outline" aria-hidden="true"></i>
<span>ARCHIVE</span>
<span>{'Archive'|t}</span>
</a>
<a href="{$base_path}/?searchtags=readitlater" class="header-nav-link{if="isset($search_tags) && preg_match('/(^|[\s,])readitlater([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="Read It Later">
<a href="{$base_path}/?searchtags=readitlater" class="header-nav-link{if="isset($search_tags) && preg_match('/(^|[\s,])readitlater([\s,]|$)/i', (string) $search_tags)"} active{/if}" aria-label="{'Read It Later'|t}">
<i class="mdi mdi-bookmark-outline" aria-hidden="true"></i>
<span>READ IT LATER</span>
<span>{'Read It Later'|t}</span>
</a>
<button class="header-nav-link" id="search-toggle-btn" data-search-toggle aria-label="Rechercher (raccourci S)">
<button class="header-nav-link" id="search-toggle-btn" data-search-toggle aria-label="{'Search (S shortcut)'|t}">
<i class="mdi mdi-magnify" aria-hidden="true"></i>
<span>RECHERCHE</span>
<span>{'Search'|t}</span>
</button>
</nav>
@@ -189,49 +191,49 @@ Bookmarklet detection logic
</button>
<!-- Filter Panel -->
<div class="filter-panel" id="filter-panel" role="dialog" aria-label="Panneau de filtres"
<div class="filter-panel" id="filter-panel" role="dialog" aria-label="{'Filters'|t}"
aria-hidden="true">
<div class="filter-header">
<span class="filter-title">Filtres</span>
<button class="filter-close" id="filter-close-btn" aria-label="Fermer les filtres">
<span class="filter-title">{'Filters'|t}</span>
<button class="filter-close" id="filter-close-btn" aria-label="{'Close filters'|t}">
<i class="mdi mdi-close" aria-hidden="true"></i>
</button>
</div>
<div class="filter-body">
<div class="filter-section">
<div class="filter-section-title">Liens par page</div>
<a href="{$base_path}/links-per-page?nb=20" class="filter-option">20 liens</a>
<a href="{$base_path}/links-per-page?nb=50" class="filter-option">50 liens</a>
<a href="{$base_path}/links-per-page?nb=100" class="filter-option">100 liens</a>
<div class="filter-section-title">{'Links per page'|t}</div>
<a href="{$base_path}/links-per-page?nb=20" class="filter-option">{'20 links'|t}</a>
<a href="{$base_path}/links-per-page?nb=50" class="filter-option">{'50 links'|t}</a>
<a href="{$base_path}/links-per-page?nb=100" class="filter-option">{'100 links'|t}</a>
</div>
<div class="filter-section">
<div class="filter-section-title">Valeur personnalisée</div>
<div class="filter-section-title">{'Custom value'|t}</div>
<form method="GET" action="{$base_path}/">
<input type="number" name="nb" class="filter-input" placeholder="20" min="1"
max="500" aria-label="Nombre de liens par page">
max="500" aria-label="{'Number of links per page'|t}">
</form>
</div>
{if="$is_logged_in"}
<div class="filter-section">
<div class="filter-section-title">Filtres de visibilité</div>
<div class="filter-section-title">{'Visibility filters'|t}</div>
<div class="filter-toggle-row">
<span class="filter-toggle-label">Liens privés uniquement</span>
<label class="toggle-switch" aria-label="Filtrer les liens privés">
<input type="checkbox" id="filter-private" aria-label="Liens privés uniquement">
<span class="filter-toggle-label">{'Private links only'|t}</span>
<label class="toggle-switch" aria-label="{'Filter private links'|t}">
<input type="checkbox" id="filter-private" aria-label="{'Private links only'|t}">
<span class="toggle-slider"></span>
</label>
</div>
<div class="filter-toggle-row">
<span class="filter-toggle-label">Liens publics uniquement</span>
<label class="toggle-switch" aria-label="Filtrer les liens publics">
<input type="checkbox" id="filter-public" aria-label="Liens publics uniquement">
<span class="filter-toggle-label">{'Public links only'|t}</span>
<label class="toggle-switch" aria-label="{'Filter public links'|t}">
<input type="checkbox" id="filter-public" aria-label="{'Public links only'|t}">
<span class="toggle-slider"></span>
</label>
</div>
<div class="filter-toggle-row">
<span class="filter-toggle-label">Liens sans tag</span>
<label class="toggle-switch" aria-label="Filtrer les liens sans tag">
<input type="checkbox" id="filter-untagged" aria-label="Liens sans tag">
<span class="filter-toggle-label">{'Untagged links'|t}</span>
<label class="toggle-switch" aria-label="{'Filter untagged links'|t}">
<input type="checkbox" id="filter-untagged" aria-label="{'Untagged links'|t}">
<span class="toggle-slider"></span>
</label>
</div>
@@ -243,14 +245,14 @@ Bookmarklet detection logic
{if="$is_logged_in"}
<!-- Multi-select Toggle -->
<button class="header-action-btn" id="select-mode-btn" aria-label="Sélection multiple"
<button class="header-action-btn" id="select-mode-btn" aria-label="{'Multi-select'|t}"
aria-pressed="false">
<i class="mdi mdi-checkbox-multiple-marked-outline" aria-hidden="true"></i>
</button>
{/if}
<!-- RSS Feed -->
<a href="{$base_path}/feed/rss" class="header-action-btn" aria-label="Flux RSS">
<a href="{$base_path}/feed/rss" class="header-action-btn" aria-label="{'RSS feed'|t}">
<i class="mdi mdi-rss" aria-hidden="true"></i>
</a>
@@ -262,14 +264,14 @@ Bookmarklet detection logic
{/if}
{if="$is_logged_in"}
<a href="{$base_path}/admin/tools" class="header-action-btn" aria-label="Outils">
<a href="{$base_path}/admin/tools" class="header-action-btn" aria-label="{'Tools'|t}">
<i class="mdi mdi-cog-outline" aria-hidden="true"></i>
</a>
<a href="{$base_path}/admin/logout" class="header-action-btn" aria-label="Déconnexion">
<a href="{$base_path}/admin/logout" class="header-action-btn" aria-label="{'Log out'|t}">
<i class="mdi mdi-logout-variant" aria-hidden="true"></i>
</a>
{else}
<a href="{$base_path}/login" class="header-action-btn" aria-label="Connexion">
<a href="{$base_path}/login" class="header-action-btn" aria-label="{'Log in'|t}">
<i class="mdi mdi-login-variant" aria-hidden="true"></i>
</a>
{/if}
@@ -285,26 +287,26 @@ Bookmarklet detection logic
<div class="search-input-row">
<i class="mdi mdi-magnify search-input-icon" aria-hidden="true"></i>
<input type="text" name="searchterm" class="search-modal-input" id="search-modal-input"
placeholder="Rechercher des notes, tags..." autocomplete="off" value="{if="isset($search_term)"}{$search_term}{/if}">
<button type="submit" class="search-submit-btn" id="search-submit-btn" aria-label="Lancer la recherche">
placeholder="{'Search notes, tags...'|t}" autocomplete="off" value="{if="isset($search_term)"}{$search_term}{/if}">
<button type="submit" class="search-submit-btn" id="search-submit-btn" aria-label="{'Run search'|t}">
<i class="mdi mdi-arrow-right"></i>
</button>
</div>
<div class="search-mode-bar" role="tablist" aria-label="Modes de recherche">
<div class="search-mode-bar" role="tablist" aria-label="{'Search modes'|t}">
<button type="button" class="search-mode-btn active" id="search-all-btn" aria-pressed="true" role="tab">
<i class="mdi mdi-magnify"></i>
<span>Globale</span>
<span>{'Global'|t}</span>
<kbd class="search-mode-kbd">S</kbd>
</button>
<button type="button" class="search-mode-btn" id="search-tags-btn" aria-pressed="false" role="tab">
<i class="mdi mdi-pound"></i>
<span>Tags</span>
<span>{'Tags'|t}</span>
<kbd class="search-mode-kbd">T</kbd>
</button>
<button type="button" class="search-mode-btn" id="search-notes-btn" aria-pressed="false" role="tab">
<i class="mdi mdi-note-text-outline"></i>
<span>Notes</span>
<span>{'Notes'|t}</span>
<kbd class="search-mode-kbd">N</kbd>
</button>
</div>
@@ -313,24 +315,24 @@ Bookmarklet detection logic
<div class="search-separator"></div>
<div class="search-results" id="search-results">
<div class="search-results-empty">
<span class="search-results-hint">Tapez pour voir des suggestions...</span>
<span class="search-results-hint">{'Type to see suggestions...'|t}</span>
</div>
</div>
<div class="search-footer">
<span class="search-footer-hint">
<kbd class="search-kbd">S</kbd> global
<kbd class="search-kbd">T</kbd> tags
<kbd class="search-kbd">N</kbd> notes
<kbd class="search-kbd">↑</kbd><kbd class="search-kbd">↓</kbd> naviguer
<kbd class="search-kbd">Enter</kbd> sélectionner
<kbd class="search-kbd">Esc</kbd> fermer
<kbd class="search-kbd">S</kbd> {'global'|t}
<kbd class="search-kbd">T</kbd> {'tags'|t}
<kbd class="search-kbd">N</kbd> {'notes'|t}
<kbd class="search-kbd">↑</kbd><kbd class="search-kbd">↓</kbd> {'navigate'|t}
<kbd class="search-kbd">Enter</kbd> {'select'|t}
<kbd class="search-kbd">Esc</kbd> {'close'|t}
</span>
</div>
</div>
</div>
<!-- Content Container -->
<div class="content-container">
<div class="content-container" id="content-container">
<!-- Feedback Messages -->
{if="!empty($plugin_errors) && $is_logged_in"}
{loop="plugin_errors"}