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.
353 lines
20 KiB
HTML
353 lines
20 KiB
HTML
<!--
|
|
Bookmarklet detection logic
|
|
-->
|
|
{if="isset($_GET['source']) && $_GET['source'] == 'bookmarklet'"}
|
|
{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="{'Main navigation'|t}">
|
|
<div class="sidebar-header">
|
|
<div class="sidebar-logo" aria-hidden="true">
|
|
<i class="mdi mdi-bookmark-multiple"></i>
|
|
</div>
|
|
<a href="{$titleLink}" class="sidebar-brand">{$shaarlititle}</a>
|
|
</div>
|
|
|
|
<nav class="sidebar-nav" aria-label="{'Main navigation'|t}">
|
|
<div class="sidebar-section">
|
|
<div class="sidebar-section-title">{'Search'|t}</div>
|
|
<button type="button" class="sidebar-link sidebar-search-trigger" data-search-toggle
|
|
aria-label="{'Open search'|t}">
|
|
<i class="mdi mdi-magnify" aria-hidden="true"></i>
|
|
<span>{'Search'|t}</span>
|
|
</button>
|
|
</div>
|
|
|
|
{if="$is_logged_in"}
|
|
<div class="sidebar-section sidebar-add-section">
|
|
<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'|t}">
|
|
<i class="mdi mdi-bookmark-outline"></i>
|
|
</a>
|
|
<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'|t}" data-base-path="{$base_path}">
|
|
<i class="mdi mdi-check-circle-outline"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
<div class="sidebar-section">
|
|
<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="{'All bookmarks'|t}">
|
|
<i class="mdi mdi-bookmark-multiple-outline" aria-hidden="true"></i>
|
|
<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="{'Pinned'|t}">
|
|
<i class="mdi mdi-pin-outline" aria-hidden="true"></i>
|
|
<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="{'Tag cloud'|t}">
|
|
<i class="mdi mdi-tag-multiple-outline" aria-hidden="true"></i>
|
|
<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="{'Picture wall'|t}">
|
|
<i class="mdi mdi-image-multiple-outline" aria-hidden="true"></i>
|
|
<span>{'Picture wall'|t}</span>
|
|
</a>
|
|
<a href="{$base_path}/daily" class="sidebar-link{if="$pageName=='daily'"} active{/if}"
|
|
aria-label="{'Daily'|t}">
|
|
<i class="mdi mdi-calendar-today" aria-hidden="true"></i>
|
|
<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="{'My tasks'|t}">
|
|
<i class="mdi mdi-check-circle-outline" aria-hidden="true"></i>
|
|
<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'|t}">
|
|
<i class="mdi mdi-note-text-outline" aria-hidden="true"></i>
|
|
<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'|t}">
|
|
<i class="mdi mdi-archive-arrow-down-outline" aria-hidden="true"></i>
|
|
<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'|t}">
|
|
<i class="mdi mdi-bookmark-outline" aria-hidden="true"></i>
|
|
<span>{'Read It Later'|t}</span>
|
|
</a>
|
|
</div>
|
|
|
|
{if="$is_logged_in"}
|
|
<div class="sidebar-section">
|
|
<div class="sidebar-section-title">{'Administration'|t}</div>
|
|
<a href="{$base_path}/admin/tools" class="sidebar-link{if="$pageName=='tools'"} active{/if}"
|
|
aria-label="{'Tools'|t}">
|
|
<i class="mdi mdi-tools" aria-hidden="true"></i>
|
|
<span>{'Tools'|t}</span>
|
|
</a>
|
|
<a href="{$base_path}/admin/configure" class="sidebar-link{if="$pageName=='configure'"} active{/if}"
|
|
aria-label="{'Settings'|t}">
|
|
<i class="mdi mdi-cog-outline" aria-hidden="true"></i>
|
|
<span>{'Settings'|t}</span>
|
|
</a>
|
|
</div>
|
|
{/if}
|
|
</nav>
|
|
|
|
<div class="sidebar-footer">
|
|
<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>{'Light mode'|t}</span>
|
|
</span>
|
|
<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>
|
|
</div>
|
|
</aside>
|
|
|
|
<!-- Sidebar Overlay for Mobile -->
|
|
<div class="sidebar-overlay" id="sidebar-overlay" aria-hidden="true"></div>
|
|
|
|
<!-- Main Content Wrapper -->
|
|
<div class="main-content">
|
|
<!-- Top Header Bar -->
|
|
<header class="header-main">
|
|
<div class="header-inner">
|
|
<div class="header-left">
|
|
<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>
|
|
<!-- <a href="{$titleLink}" class="header-brand">{$shaarlititle}</a> -->
|
|
</div>
|
|
|
|
<!-- Main Navigation -->
|
|
<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="{'Home'|t}">
|
|
<i class="mdi mdi-home-outline" aria-hidden="true"></i>
|
|
<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="{'Pinned'|t}">
|
|
<i class="mdi mdi-pin-outline" aria-hidden="true"></i>
|
|
<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="{'Tag cloud'|t}">
|
|
<i class="mdi mdi-tag-multiple" aria-hidden="true"></i>
|
|
<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="{'Picture wall'|t}">
|
|
<i class="mdi mdi-image-multiple" aria-hidden="true"></i>
|
|
<span>{'Picture wall'|t}</span>
|
|
</a>
|
|
<a href="{$base_path}/daily" class="header-nav-link{if="$pageName=='daily'"} active{/if}"
|
|
aria-label="{'Daily'|t}">
|
|
<i class="mdi mdi-calendar" aria-hidden="true"></i>
|
|
<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="{'My tasks'|t}">
|
|
<i class="mdi mdi-check-circle-outline" aria-hidden="true"></i>
|
|
<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'|t}">
|
|
<i class="mdi mdi-note-text-outline" aria-hidden="true"></i>
|
|
<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'|t}">
|
|
<i class="mdi mdi-archive-arrow-down-outline" aria-hidden="true"></i>
|
|
<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'|t}">
|
|
<i class="mdi mdi-bookmark-outline" aria-hidden="true"></i>
|
|
<span>{'Read It Later'|t}</span>
|
|
</a>
|
|
<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>{'Search'|t}</span>
|
|
</button>
|
|
</nav>
|
|
|
|
<!-- Header Actions -->
|
|
<div class="header-actions">
|
|
<!-- Filter Button -->
|
|
<div class="filter-dropdown" id="filter-dropdown">
|
|
<button class="header-action-btn" id="filter-toggle-btn" aria-label="Filtres" aria-expanded="false"
|
|
aria-controls="filter-panel">
|
|
<i class="mdi mdi-filter-variant" aria-hidden="true"></i>
|
|
</button>
|
|
|
|
<!-- Filter Panel -->
|
|
<div class="filter-panel" id="filter-panel" role="dialog" aria-label="{'Filters'|t}"
|
|
aria-hidden="true">
|
|
<div class="filter-header">
|
|
<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">{'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">{'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="{'Number of links per page'|t}">
|
|
</form>
|
|
</div>
|
|
{if="$is_logged_in"}
|
|
<div class="filter-section">
|
|
<div class="filter-section-title">{'Visibility filters'|t}</div>
|
|
<div class="filter-toggle-row">
|
|
<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">{'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">{'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>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{if="$is_logged_in"}
|
|
<!-- Multi-select Toggle -->
|
|
<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="{'RSS feed'|t}">
|
|
<i class="mdi mdi-rss" aria-hidden="true"></i>
|
|
</a>
|
|
|
|
<!-- Plugins Support -->
|
|
{if="isset($plugins_header.buttons_toolbar)"}
|
|
{loop="$plugins_header.buttons_toolbar"}
|
|
{$value.html}
|
|
{/loop}
|
|
{/if}
|
|
|
|
{if="$is_logged_in"}
|
|
<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="{'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="{'Log in'|t}">
|
|
<i class="mdi mdi-login-variant" aria-hidden="true"></i>
|
|
</a>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- Search Overlay (Spotlight Style) -->
|
|
<div class="search-overlay" id="search-overlay">
|
|
<div class="search-modal">
|
|
<form method="GET" action="{$base_path}/" id="search-form">
|
|
<div class="search-modal-header">
|
|
<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="{'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="{'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>{'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'|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'|t}</span>
|
|
<kbd class="search-mode-kbd">N</kbd>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
<div class="search-separator"></div>
|
|
<div class="search-results" id="search-results">
|
|
<div class="search-results-empty">
|
|
<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'|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" id="content-container">
|
|
<!-- Feedback Messages -->
|
|
{if="!empty($plugin_errors) && $is_logged_in"}
|
|
{loop="plugin_errors"}
|
|
<div class="alert alert-danger">{$value}</div>
|
|
{/loop}
|
|
{/if}
|
|
{if="!empty($global_errors)"}
|
|
{loop="global_errors"}
|
|
<div class="alert alert-danger">{$value}</div>
|
|
{/loop}
|
|
{/if}
|
|
{if="!empty($global_successes)"}
|
|
{loop="global_successes"}
|
|
<div class="alert alert-success">{$value}</div>
|
|
{/loop}
|
|
{/if}
|
|
{/if}
|