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
+34 -34
View File
@@ -20,12 +20,12 @@
<div class="content-toolbar">
<div class="toolbar-left">{include="linklist.paging"}</div>
<div class="toolbar-right">
<div class="view-toggle" role="group" aria-label="Changer de vue">
<button class="view-toggle-btn active" id="view-grid-btn" aria-label="Vue grille"
<div class="view-toggle" role="group" aria-label="{'Change view'|t}">
<button class="view-toggle-btn active" id="view-grid-btn" aria-label="{'Grid view'|t}"
aria-pressed="true"><i class="mdi mdi-view-grid" aria-hidden="true"></i></button>
<button class="view-toggle-btn" id="view-list-btn" aria-label="Vue liste"
<button class="view-toggle-btn" id="view-list-btn" aria-label="{'List view'|t}"
aria-pressed="false"><i class="mdi mdi-view-list" aria-hidden="true"></i></button>
<button class="view-toggle-btn" id="view-compact-btn" aria-label="Vue compacte"
<button class="view-toggle-btn" id="view-compact-btn" aria-label="{'Compact view'|t}"
aria-pressed="false"><i class="mdi mdi-view-headline"
aria-hidden="true"></i></button>
</div>
@@ -35,11 +35,11 @@
<!-- {* ----- tag search results ----- *} -->
{if="!empty($search_tags)"}
<div class="search-results-header">
<span class="search-count">{$result_count} résultat(s) tagué(s)</span>
<span class="search-count">{$result_count} {'tagged result(s)'|t}</span>
{loop="$active_search_tags"}
<span class="search-tag-chip">{$value} <a href="{$base_path}/remove-tag/{function="
urlencode($value)"}" class="search-tag-close" title="Retirer le tag"
aria-label="Retirer le tag {$value}"><i class="mdi mdi-close"
urlencode($value)"}" class="search-tag-close" title="{'Remove tag'|t}"
aria-label="{'Remove tag'|t} {$value}"><i class="mdi mdi-close"
aria-hidden="true"></i></a></span>
{/loop}
</div>
@@ -53,39 +53,39 @@
{else}
<div class="empty-state" role="status" aria-live="polite">
<div class="empty-state-icon" aria-hidden="true"><i class="mdi mdi-bookmark-off-outline"></i></div>
<h2 class="empty-state-title">Aucun bookmark trouvé</h2>
<p class="empty-state-text">{if="!empty($search_term)"}Aucun résultat pour : <strong>{$search_term}</strong>{else}Commencez à ajouter des bookmarks pour les voir apparaître ici.{/if}</p>
<h2 class="empty-state-title">{'No bookmarks found'|t}</h2>
<p class="empty-state-text">{if="!empty($search_term)"}{'No result for:'|t} <strong>{$search_term}</strong>{else}{'Start adding bookmarks to see them appear here.'|t}{/if}</p>
</div>
{/if}
{else}
<!-- {* ----- at least one link ----- *} -->
<div class="links-list view-grid" id="links-list">
<div class="pinned-section-head" aria-hidden="true">📌 Bookmarks épinglés</div>
<div class="normal-section-head" aria-hidden="true">Bookmarks</div>
<div class="pinned-section-head" aria-hidden="true"><i class="mdi mdi-pin" aria-hidden="true"></i> {'Pinned bookmarks'|t}</div>
<div class="normal-section-head" aria-hidden="true">{'Bookmarks'|t}</div>
<div class="compact-table-head" aria-hidden="true">
<span class="compact-head-title">Titre &amp; URL</span>
<span class="compact-head-tags">Tags</span>
<span class="compact-head-date">Date</span>
<span class="compact-head-actions">Actions</span>
<span class="compact-head-title">{'Title & URL'|t}</span>
<span class="compact-head-tags">{'Tags'|t}</span>
<span class="compact-head-date">{'Date'|t}</span>
<span class="compact-head-actions">{'Actions'|t}</span>
</div>
{loop="$links"}
<div id="{$value.id}" class="link-outer{if="isset($value.sticky) && $value.sticky"} is-sticky{/if}{if="$value.class"} {$value.class}{/if}{if="$value.private"} private{else} public{/if}" data-id="{$value.id}">
{if="$is_logged_in"}
<div class="link-select-checkbox"><input type="checkbox" class="link-checkbox"
data-id="{$value.id}" aria-label="Sélectionner ce bookmark"></div>
data-id="{$value.id}" aria-label="{'Select this bookmark'|t}"></div>
{/if}
{if="$value.thumbnail !== false"}<div class="link-thumbnail"><img
src="{$root_path}/{$value.thumbnail}#" loading="lazy" alt=""
style="width:100%;height:100%;object-fit:cover;"
onerror="this.parentElement.style.display='none'"></div>{/if}
<div class="link-visibility-badge">{if="$value.private"}<i class="mdi mdi-lock" title="Privé"
aria-label="Bookmark privé"></i>{else}<i class="mdi mdi-lock-open-variant"
title="Public" aria-label="Bookmark public"></i>{/if}</div>
<div class="link-visibility-badge">{if="$value.private"}<i class="mdi mdi-lock" title="{'Private'|t}"
aria-label="{'Private bookmark'|t}"></i>{else}<i class="mdi mdi-lock-open-variant"
title="{'Public'|t}" aria-label="{'Public bookmark'|t}"></i>{/if}</div>
<div class="link-content">
<div class="link-header">
<a href="{$value.real_url}" class="link-title"
{if="strpos($value.url, $value.shorturl) !== false"} title="Note" {/if}>
{if="strpos($value.url, $value.shorturl) !== false"} title="{'Note'|t}" {/if}>
{if="isset($value.sticky) && $value.sticky"}<i class="mdi mdi-pin"
style="color:var(--primary);"
aria-hidden="true"></i>{/if}{$value.title_html}
@@ -94,7 +94,7 @@
<div class="link-meta">
<span class="link-date">{$value.created|format_date}</span>
<a href="{$base_path}/shaare/{$value.shorturl}" class="link-permalink"
title="Lien permanent" aria-label="Lien permanent"><i
title="{'Permalink'|t}" aria-label="{'Permalink'|t}"><i
class="mdi mdi-link-variant" aria-hidden="true"></i></a>
</div>
</div>
@@ -103,33 +103,33 @@
<div class="link-footer">
<div class="link-tag-list">
{loop="$value.taglist"}<span class="link-tag{if="in_array($value, $active_search_tags)"} is-search-match{/if}" data-tag="{$value}"><a
class="link-tag-link" href="{$base_path}/add-tag/{$value|urlencode}">{$value}</a>{if="$is_logged_in"}<button type="button" class="tag-remove-btn" data-tag="{$value}" aria-label="Supprimer le tag {$value}" title="Supprimer">&times;</button>{/if}</span>{/loop}
class="link-tag-link" href="{$base_path}/add-tag/{$value|urlencode}">{$value}</a>{if="$is_logged_in"}<button type="button" class="tag-remove-btn" data-tag="{$value}" aria-label="{'Delete tag'|t} {$value}" title="{'Delete'|t}">&times;</button>{/if}</span>{/loop}
</div>
<div class="link-actions">
{if="$is_logged_in"}
<a href="{$base_path}/admin/shaare/{$value.id}" title="Modifier"
aria-label="Modifier ce bookmark"><i class="mdi mdi-pencil"
<a href="{$base_path}/admin/shaare/{$value.id}" title="{'Edit'|t}"
aria-label="{'Edit this bookmark'|t}"><i class="mdi mdi-pencil"
aria-hidden="true"></i></a>
<a href="{$base_path}/admin/shaare/{$value.id}/pin?token={$token}"
title="Épingler/Désépingler" aria-label="Épingler ou désépingler"><i
title="{'Pin/Unpin'|t}" aria-label="{'Pin or unpin'|t}"><i
class="mdi mdi-pin" aria-hidden="true"></i></a>
<a href="{$base_path}/admin/shaare/delete?id={$value.id}&token={$token}"
title="Supprimer" aria-label="Supprimer ce bookmark"
onclick="return confirm('Supprimer ce bookmark ?');"><i
title="{'Delete'|t}" aria-label="{'Delete this bookmark'|t}"
onclick="return confirm('{'Delete this bookmark?'|t}');"><i
class="mdi mdi-delete" aria-hidden="true"></i></a>
<button type="button" class="readitlater-toggle-btn" data-tag="readitlater" data-active="0" title="Ajouter à Read It Later" aria-label="Ajouter à Read It Later">
<button type="button" class="readitlater-toggle-btn" data-tag="readitlater" data-active="0" title="{'Add to Read It Later'|t}" aria-label="{'Add to Read It Later'|t}">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"/></svg>
</button>
{/if}
<!-- fullscreen button (in all 3 views) -->
<a href="#" class="view-desc-btn" data-id="{$value.id}"
title="Voir la description" aria-label="Voir la description complète"><i
title="{'View description'|t}" aria-label="{'View full description'|t}"><i
class="mdi mdi-fullscreen" aria-hidden="true"></i></a>
{loop="$value.link_plugin"}
<span class="link-plugin">{$value}</span>
{/loop}
<a href="{$value.real_url}" target="_blank" rel="noopener"
title="Ouvrir le lien" aria-label="Ouvrir le lien dans un nouvel onglet"><i
title="{'Open link'|t}" aria-label="{'Open link in new tab'|t}"><i
class="mdi mdi-open-in-new" aria-hidden="true"></i></a>
</div>
</div>
@@ -146,18 +146,18 @@
<!-- {* ----- description modal ----- *} -->
<div id="desc-modal" class="modal-overlay" role="dialog" aria-modal="true"
aria-label="Description du bookmark" aria-hidden="true">
aria-label="{'Bookmark description'|t}" aria-hidden="true">
<div class="modal-content">
<button class="modal-close" id="desc-modal-close" aria-label="Fermer">&times;</button>
<button class="modal-close" id="desc-modal-close" aria-label="{'Close'|t}">&times;</button>
<div class="modal-body" id="desc-modal-body"></div>
</div>
</div>
<!-- {* ----- QR Code Modal ----- *} -->
<div id="qrcode-modal" class="qrcode-modal-overlay" role="dialog" aria-modal="true" aria-label="Code QR"
<div id="qrcode-modal" class="qrcode-modal-overlay" role="dialog" aria-modal="true" aria-label="{'QR Code'|t}"
aria-hidden="true">
<div class="qrcode-modal-content">
<button class="qrcode-modal-close" id="qrcode-modal-close" aria-label="Fermer">&times;</button>
<button class="qrcode-modal-close" id="qrcode-modal-close" aria-label="{'Close'|t}">&times;</button>
<div id="qrcode-modal-body"></div>
</div>
</div>